diff options
author | Clement Ho <clemmakesapps@gmail.com> | 2018-01-10 16:44:14 +0000 |
---|---|---|
committer | Clement Ho <clemmakesapps@gmail.com> | 2018-01-10 16:44:14 +0000 |
commit | 827761c1658ccc62a7883aba36dc35dd9cc3c5a1 (patch) | |
tree | 453582440bf9eeb8202c2390f8478e7c4adcf671 | |
parent | 2bd4453ca2c4bec527a264194cf12d164cd31ed7 (diff) | |
parent | 82007530cb08ccd8aff68aafdc506595f62a40a6 (diff) | |
download | gitlab-ce-827761c1658ccc62a7883aba36dc35dd9cc3c5a1.tar.gz |
Merge branch 'master' into 'explore-dispatcher-refactor'
# Conflicts:
# app/assets/javascripts/dispatcher.js
283 files changed, 7340 insertions, 5538 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml index d4905856e72..dc8ac60fb44 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -13,7 +13,8 @@ engines: exclude_paths: - "lib/api/v3/*" eslint: - enabled: true + # eslint-plugin-vue is locked to version 2 in codeclimate, we need version 4 + enabled: false rubocop: enabled: true channel: "gitlab-rubocop-0-52" diff --git a/.eslintrc b/.eslintrc index 44ad6a4896c..6dbe269e594 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,7 +4,10 @@ "browser": true, "es6": true }, - "extends": "airbnb-base", + "extends": [ + "airbnb-base", + "plugin:vue/recommended" + ], "globals": { "__webpack_public_path__": true, "_": false, @@ -12,7 +15,9 @@ "gon": false, "localStorage": false }, - "parser": "babel-eslint", + "parserOptions": { + "parser": "babel-eslint" + }, "plugins": [ "filenames", "import", @@ -20,7 +25,7 @@ "promise" ], "settings": { - "html/html-extensions": [".html", ".html.raw", ".vue"], + "html/html-extensions": [".html", ".html.raw"], "import/resolver": { "webpack": { "config": "./config/webpack.config.js" @@ -32,6 +37,15 @@ "import/no-commonjs": "error", "no-multiple-empty-lines": ["error", { "max": 1 }], "promise/catch-or-return": "error", - "no-underscore-dangle": ["error", { "allow": ["__"]}] + "no-underscore-dangle": ["error", { "allow": ["__"]}], + "vue/html-self-closing": ["error", { + "html": { + "void": "always", + "normal": "never", + "component": "always" + }, + "svg": "always", + "math": "always" + }] } } diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index e40e4fc339c..328185caaeb 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.66.0 +0.67.0 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index e030a0157c9..c68d476cc8e 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -5.10.3 +5.11.0 @@ -70,6 +70,10 @@ gem 'net-ldap' # Git Wiki # Required manually in config/initializers/gollum.rb to control load order gem 'gollum-lib', '~> 4.2', require: false + +# Before updating this gem, check if +# https://github.com/gollum/rugged_adapter/pull/28 has been merged. +# If it has, then remove the monkey patch for tree_entry in config/initializers/gollum.rb gem 'gollum-rugged_adapter', '~> 0.4.4', require: false # Language detection @@ -402,7 +406,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.64.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.69.0', require: 'gitaly' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index d10da1bd1c3..40c4f73b8a6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -284,7 +284,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.64.0) + gitaly-proto (0.69.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -1053,7 +1053,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly-proto (~> 0.64.0) + gitaly-proto (~> 0.69.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.6.2) diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js index 34e905222b4..8d021de7998 100644 --- a/app/assets/javascripts/behaviors/index.js +++ b/app/assets/javascripts/behaviors/index.js @@ -7,6 +7,7 @@ import installGlEmojiElement from './gl_emoji'; import './quick_submit'; import './requires_input'; import './toggler_behavior'; +import '../preview_markdown'; installGlEmojiElement(); initCopyAsGFM(); diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js index 57b031956e8..6f1350e80fc 100644 --- a/app/assets/javascripts/blob/notebook/index.js +++ b/app/assets/javascripts/blob/notebook/index.js @@ -8,6 +8,9 @@ export default () => { new Vue({ el, + components: { + notebookLab, + }, data() { return { error: false, @@ -16,8 +19,41 @@ export default () => { json: {}, }; }, - components: { - notebookLab, + mounted() { + if (gon.katex_css_url) { + const katexStyles = document.createElement('link'); + katexStyles.setAttribute('rel', 'stylesheet'); + katexStyles.setAttribute('href', gon.katex_css_url); + document.head.appendChild(katexStyles); + } + + if (gon.katex_js_url) { + const katexScript = document.createElement('script'); + katexScript.addEventListener('load', () => { + this.loadFile(); + }); + katexScript.setAttribute('src', gon.katex_js_url); + document.head.appendChild(katexScript); + } else { + this.loadFile(); + } + }, + methods: { + loadFile() { + axios.get(el.dataset.endpoint) + .then(res => res.data) + .then((data) => { + this.json = data; + this.loading = false; + }) + .catch((e) => { + if (e.status !== 200) { + this.loadError = true; + } + + this.error = true; + }); + }, }, template: ` <div class="container-fluid md prepend-top-default append-bottom-default"> @@ -46,41 +82,5 @@ export default () => { </p> </div> `, - methods: { - loadFile() { - axios.get(el.dataset.endpoint) - .then(res => res.data) - .then((data) => { - this.json = data; - this.loading = false; - }) - .catch((e) => { - if (e.status !== 200) { - this.loadError = true; - } - - this.error = true; - }); - }, - }, - mounted() { - if (gon.katex_css_url) { - const katexStyles = document.createElement('link'); - katexStyles.setAttribute('rel', 'stylesheet'); - katexStyles.setAttribute('href', gon.katex_css_url); - document.head.appendChild(katexStyles); - } - - if (gon.katex_js_url) { - const katexScript = document.createElement('script'); - katexScript.addEventListener('load', () => { - this.loadFile(); - }); - katexScript.setAttribute('src', gon.katex_js_url); - document.head.appendChild(katexScript); - } else { - this.loadFile(); - } - }, }); }; diff --git a/app/assets/javascripts/blob/pdf/index.js b/app/assets/javascripts/blob/pdf/index.js index 7109f356540..70136cc4087 100644 --- a/app/assets/javascripts/blob/pdf/index.js +++ b/app/assets/javascripts/blob/pdf/index.js @@ -7,6 +7,9 @@ export default () => { return new Vue({ el, + components: { + pdfLab, + }, data() { return { error: false, @@ -15,9 +18,6 @@ export default () => { pdf: el.dataset.endpoint, }; }, - components: { - pdfLab, - }, methods: { onLoad() { this.loading = false; diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 679c883cdcf..90166b3d3d1 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -171,19 +171,14 @@ $(() => { }); gl.IssueBoardsModalAddBtn = new Vue({ - mixins: [gl.issueBoards.ModalMixins], el: document.getElementById('js-add-issues-btn'), + mixins: [gl.issueBoards.ModalMixins], data() { return { modal: ModalStore.store, store: Store.state, }; }, - watch: { - disabled() { - this.updateTooltip(); - }, - }, computed: { disabled() { if (!this.store) { @@ -199,6 +194,14 @@ $(() => { return ''; }, }, + watch: { + disabled() { + this.updateTooltip(); + }, + }, + mounted() { + this.updateTooltip(); + }, methods: { updateTooltip() { const $tooltip = $(this.$refs.addIssuesButton); @@ -217,9 +220,6 @@ $(() => { } }, }, - mounted() { - this.updateTooltip(); - }, template: ` <div class="board-extra-actions"> <button diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 0b220a56e0b..23fec503586 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -10,12 +10,30 @@ export default { 'issue-card-inner': gl.issueBoards.IssueCardInner, }, props: { - list: Object, - issue: Object, - issueLinkBase: String, - disabled: Boolean, - index: Number, - rootPath: String, + list: { + type: Object, + default: () => ({}), + }, + issue: { + type: Object, + default: () => ({}), + }, + issueLinkBase: { + type: String, + default: '', + }, + disabled: { + type: Boolean, + default: false, + }, + index: { + type: Number, + default: 0, + }, + rootPath: { + type: String, + default: '', + }, }, data() { return { @@ -54,8 +72,13 @@ export default { </script> <template> - <li class="card" - :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }" + <li + class="card" + :class="{ + 'user-can-drag': !disabled && issue.id, + 'is-disabled': disabled || !issue.id, + 'is-active': issueDetailVisible + }" :index="index" :data-issue-id="issue.id" @mousedown="mouseDown" @@ -66,6 +89,7 @@ export default { :issue="issue" :issue-link-base="issueLinkBase" :root-path="rootPath" - :update-filters="true" /> + :update-filters="true" + /> </li> </template> diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js index 84b76a6f1b1..d8cf532fe78 100644 --- a/app/assets/javascripts/boards/components/board_list.js +++ b/app/assets/javascripts/boards/components/board_list.js @@ -187,7 +187,7 @@ export default { <li class="board-list-count text-center" v-if="showCount" - data-id="-1"> + data-issue-id="-1"> <loading-icon v-show="list.loadingMore" diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 872abf03ef1..c13bbcee863 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -1,108 +1,112 @@ <script> -import { s__, sprintf } from '../../locale'; -import eventHub from '../event_hub'; -import loadingButton from '../../vue_shared/components/loading_button.vue'; -import { - APPLICATION_NOT_INSTALLABLE, - APPLICATION_SCHEDULED, - APPLICATION_INSTALLABLE, - APPLICATION_INSTALLING, - APPLICATION_INSTALLED, - APPLICATION_ERROR, - REQUEST_LOADING, - REQUEST_SUCCESS, - REQUEST_FAILURE, -} from '../constants'; + /* eslint-disable vue/require-default-prop */ + import { s__, sprintf } from '../../locale'; + import eventHub from '../event_hub'; + import loadingButton from '../../vue_shared/components/loading_button.vue'; + import { + APPLICATION_NOT_INSTALLABLE, + APPLICATION_SCHEDULED, + APPLICATION_INSTALLABLE, + APPLICATION_INSTALLING, + APPLICATION_INSTALLED, + APPLICATION_ERROR, + REQUEST_LOADING, + REQUEST_SUCCESS, + REQUEST_FAILURE, + } from '../constants'; -export default { - props: { - id: { - type: String, - required: true, + export default { + components: { + loadingButton, }, - title: { - type: String, - required: true, + props: { + id: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + titleLink: { + type: String, + required: false, + }, + description: { + type: String, + required: true, + }, + status: { + type: String, + required: false, + }, + statusReason: { + type: String, + required: false, + }, + requestStatus: { + type: String, + required: false, + }, + requestReason: { + type: String, + required: false, + }, }, - titleLink: { - type: String, - required: false, - }, - description: { - type: String, - required: true, - }, - status: { - type: String, - required: false, - }, - statusReason: { - type: String, - required: false, - }, - requestStatus: { - type: String, - required: false, - }, - requestReason: { - type: String, - required: false, - }, - }, - components: { - loadingButton, - }, - computed: { - rowJsClass() { - return `js-cluster-application-row-${this.id}`; - }, - installButtonLoading() { - return !this.status || - this.status === APPLICATION_SCHEDULED || - this.status === APPLICATION_INSTALLING || - this.requestStatus === REQUEST_LOADING; - }, - installButtonDisabled() { - // Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but - // we already made a request to install and are just waiting for the real-time - // to sync up. - return (this.status !== APPLICATION_INSTALLABLE && this.status !== APPLICATION_ERROR) || - this.requestStatus === REQUEST_LOADING || - this.requestStatus === REQUEST_SUCCESS; - }, - installButtonLabel() { - let label; - if ( - this.status === APPLICATION_NOT_INSTALLABLE || - this.status === APPLICATION_INSTALLABLE || - this.status === APPLICATION_ERROR - ) { - label = s__('ClusterIntegration|Install'); - } else if (this.status === APPLICATION_SCHEDULED || this.status === APPLICATION_INSTALLING) { - label = s__('ClusterIntegration|Installing'); - } else if (this.status === APPLICATION_INSTALLED) { - label = s__('ClusterIntegration|Installed'); - } + computed: { + rowJsClass() { + return `js-cluster-application-row-${this.id}`; + }, + installButtonLoading() { + return !this.status || + this.status === APPLICATION_SCHEDULED || + this.status === APPLICATION_INSTALLING || + this.requestStatus === REQUEST_LOADING; + }, + installButtonDisabled() { + // Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but + // we already made a request to install and are just waiting for the real-time + // to sync up. + return (this.status !== APPLICATION_INSTALLABLE + && this.status !== APPLICATION_ERROR) || + this.requestStatus === REQUEST_LOADING || + this.requestStatus === REQUEST_SUCCESS; + }, + installButtonLabel() { + let label; + if ( + this.status === APPLICATION_NOT_INSTALLABLE || + this.status === APPLICATION_INSTALLABLE || + this.status === APPLICATION_ERROR + ) { + label = s__('ClusterIntegration|Install'); + } else if (this.status === APPLICATION_SCHEDULED || + this.status === APPLICATION_INSTALLING) { + label = s__('ClusterIntegration|Installing'); + } else if (this.status === APPLICATION_INSTALLED) { + label = s__('ClusterIntegration|Installed'); + } - return label; - }, - hasError() { - return this.status === APPLICATION_ERROR || this.requestStatus === REQUEST_FAILURE; - }, - generalErrorDescription() { - return sprintf( - s__('ClusterIntegration|Something went wrong while installing %{title}'), { - title: this.title, - }, - ); + return label; + }, + hasError() { + return this.status === APPLICATION_ERROR || + this.requestStatus === REQUEST_FAILURE; + }, + generalErrorDescription() { + return sprintf( + s__('ClusterIntegration|Something went wrong while installing %{title}'), { + title: this.title, + }, + ); + }, }, - }, - methods: { - installClicked() { - eventHub.$emit('installApplication', this.id); + methods: { + installClicked() { + eventHub.$emit('installApplication', this.id); + }, }, - }, -}; + }; </script> <template> diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index cd58b88db69..25cef44c1b8 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -1,84 +1,93 @@ <script> -import _ from 'underscore'; -import { s__, sprintf } from '../../locale'; -import applicationRow from './application_row.vue'; + import _ from 'underscore'; + import { s__, sprintf } from '../../locale'; + import applicationRow from './application_row.vue'; -export default { - props: { - applications: { - type: Object, - required: false, - default: () => ({}), + export default { + components: { + applicationRow, }, - helpPath: { - type: String, - required: false, + props: { + applications: { + type: Object, + required: false, + default: () => ({}), + }, + helpPath: { + type: String, + required: false, + default: '', + }, }, - }, - components: { - applicationRow, - }, - computed: { - generalApplicationDescription() { - return sprintf( - _.escape(s__('ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}')), { - helpLink: `<a href="${this.helpPath}"> - ${_.escape(s__('ClusterIntegration|installing applications'))} - </a>`, - }, - false, - ); - }, - helmTillerDescription() { - return _.escape(s__( - `ClusterIntegration|Helm streamlines installing and managing Kubernets applications. - Tiller runs inside of your Kubernetes Cluster, and manages - releases of your charts.`, - )); - }, - ingressDescription() { - const descriptionParagraph = _.escape(s__( - `ClusterIntegration|Ingress gives you a way to route requests to services based on the - request host or path, centralizing a number of services into a single entrypoint.`, - )); + computed: { + generalApplicationDescription() { + return sprintf( + _.escape(s__(`ClusterIntegration|Install applications on your cluster. + Read more about %{helpLink}`)), + { + helpLink: `<a href="${this.helpPath}"> + ${_.escape(s__('ClusterIntegration|installing applications'))} + </a>`, + }, + false, + ); + }, + helmTillerDescription() { + return _.escape(s__( + `ClusterIntegration|Helm streamlines installing and managing Kubernets applications. + Tiller runs inside of your Kubernetes Cluster, and manages + releases of your charts.`, + )); + }, + ingressDescription() { + const descriptionParagraph = _.escape(s__( + `ClusterIntegration|Ingress gives you a way to route requests to services based on the + request host or path, centralizing a number of services into a single entrypoint.`, + )); - const extraCostParagraph = sprintf( - _.escape(s__('ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}')), { - boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`, - pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer"> - ${_.escape(s__('ClusterIntegration|GKE pricing'))} - </a>`, - }, - false, - ); + const extraCostParagraph = sprintf( + _.escape(s__(`ClusterIntegration|%{boldNotice} This will add some +extra resources like a load balancer, +which incur additional costs. See %{pricingLink}`)), + { + boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`, + pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer"> + ${_.escape(s__('ClusterIntegration|GKE pricing'))} + </a>`, + }, + false, + ); - return ` - <p> - ${descriptionParagraph} - </p> - <p class="append-bottom-0"> - ${extraCostParagraph} - </p> - `; - }, - gitlabRunnerDescription() { - return _.escape(s__( - `ClusterIntegration|GitLab Runner is the open source project that is used to run your jobs - and send the results back to GitLab.`, - )); - }, - prometheusDescription() { - return sprintf( - _.escape(s__('ClusterIntegration|Prometheus is an open-source monitoring system with %{gitlabIntegrationLink} to monitor deployed applications.')), { - gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html", target="_blank" rel="noopener noreferrer"> - ${_.escape(s__('ClusterIntegration|Gitlab Integration'))} - </a>`, - }, - false, - ); + return ` + <p> + ${descriptionParagraph} + </p> + <p class="append-bottom-0"> + ${extraCostParagraph} + </p> + `; + }, + gitlabRunnerDescription() { + return _.escape(s__( + `ClusterIntegration|GitLab Runner is the open source project that is used to run your jobs + and send the results back to GitLab.`, + )); + }, + prometheusDescription() { + return sprintf( + _.escape(s__(`ClusterIntegration|Prometheus is an open-source monitoring system + with %{gitlabIntegrationLink} to monitor deployed applications.`)), + { + gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html" +target="_blank" rel="noopener noreferrer"> + ${_.escape(s__('ClusterIntegration|Gitlab Integration'))} + </a>`, + }, + false, + ); + }, }, - }, -}; + }; </script> <template> @@ -107,26 +116,29 @@ export default { :request-reason="applications.helm.requestReason" /> <application-row - id="ingress" - :title="applications.ingress.title" - title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" - :description="ingressDescription" - :status="applications.ingress.status" - :status-reason="applications.ingress.statusReason" - :request-status="applications.ingress.requestStatus" - :request-reason="applications.ingress.requestReason" - /> - <application-row - id="prometheus" - :title="applications.prometheus.title" - title-link="https://prometheus.io/docs/introduction/overview/" - :description="prometheusDescription" - :status="applications.prometheus.status" - :status-reason="applications.prometheus.statusReason" - :request-status="applications.prometheus.requestStatus" - :request-reason="applications.prometheus.requestReason" - /> - <!-- NOTE: Don't forget to update `clusters.scss` min-height for this block and uncomment `application_spec` tests --> + id="ingress" + :title="applications.ingress.title" + title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" + :description="ingressDescription" + :status="applications.ingress.status" + :status-reason="applications.ingress.statusReason" + :request-status="applications.ingress.requestStatus" + :request-reason="applications.ingress.requestReason" + /> + <application-row + id="prometheus" + :title="applications.prometheus.title" + title-link="https://prometheus.io/docs/introduction/overview/" + :description="prometheusDescription" + :status="applications.prometheus.status" + :status-reason="applications.prometheus.statusReason" + :request-status="applications.prometheus.requestStatus" + :request-reason="applications.prometheus.requestReason" + /> + <!-- + NOTE: Don't forget to update `clusters.scss` + min-height for this block and uncomment `application_spec` tests + --> <!-- Add GitLab Runner row, all other plumbing is complete --> </div> </div> diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index b6a0ece7907..525fbf9dac9 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -94,7 +94,7 @@ export default class ImageFile { }); return [maxWidth, maxHeight]; } - + // eslint-disable-next-line views = { 'two-up': function() { return $('.two-up.view .wrap', this.file).each((function(_this) { diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index e9a0dbaa59d..da0e8063ccb 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -4,6 +4,10 @@ import pipelinesMixin from '../../pipelines/mixins/pipelines'; export default { + mixins: [ + pipelinesMixin, + ], + props: { endpoint: { type: String, @@ -31,9 +35,6 @@ default: 'child', }, }, - mixins: [ - pipelinesMixin, - ], data() { const store = new PipelineStore(); @@ -95,28 +96,29 @@ label="Loading pipelines" size="3" v-if="isLoading" - /> + /> <empty-state v-if="shouldRenderEmptyState" :help-page-path="helpPagePath" :empty-state-svg-path="emptyStateSvgPath" - /> + /> <error-state v-if="shouldRenderErrorState" :error-state-svg-path="errorStateSvgPath" - /> + /> <div class="table-holder" - v-if="shouldRenderTable"> + v-if="shouldRenderTable" + > <pipelines-table-component :pipelines="state.pipelines" :update-graph-dropdown="updateGraphDropdown" :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" - /> + /> </div> </div> </template> diff --git a/app/assets/javascripts/cycle_analytics/components/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue index 732697c134e..3204b8dd8e7 100644 --- a/app/assets/javascripts/cycle_analytics/components/banner.vue +++ b/app/assets/javascripts/cycle_analytics/components/banner.vue @@ -26,28 +26,34 @@ class="js-ca-dismiss-button dismiss-button" type="button" :aria-label="__('Dismiss Cycle Analytics introduction box')" - @click="dismissOverviewDialog"> + @click="dismissOverviewDialog" + > <i class="fa fa-times" aria-hidden="true"> </i> </button> - <div class="svg-container" v-html="iconCycleAnalyticsSplash"> + <div + class="svg-container" + v-html="iconCycleAnalyticsSplash" + > </div> <div class="inner-content"> <h4> - {{__('Introducing Cycle Analytics')}} + {{ __('Introducing Cycle Analytics') }} </h4> <p> - {{ __('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.') }} + {{ __(`Cycle Analytics gives an overview +of how much time it takes to go from idea to production in your project.`) }} </p> <p> <a :href="documentationLink" target="_blank" rel="nofollow" - class="btn"> - {{__('Read more')}} + class="btn" + > + {{ __('Read more') }} </a> </p> </div> diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue index 6e94ba929b2..32ae0cc1476 100644 --- a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue @@ -2,25 +2,34 @@ import tooltip from '../../vue_shared/directives/tooltip'; export default { + directives: { + tooltip, + }, props: { count: { type: Number, required: true, }, }, - directives: { - tooltip, - }, }; </script> <template> - <span v-if="count === 50" class="events-info pull-right"> + <span + v-if="count === 50" + class="events-info pull-right" + > <i class="fa fa-warning" v-tooltip aria-hidden="true" - :title="n__('Limited to showing %d event at most', 'Limited to showing %d events at most', 50)" - data-placement="top"></i> + :title="n__( + 'Limited to showing %d event at most', + 'Limited to showing %d events at most', + 50 + )" + data-placement="top" + > + </i> {{ n__('Showing %d event', 'Showing %d events', 50) }} </span> </template> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue index 45930145b0a..a71dcf78103 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue @@ -4,15 +4,21 @@ import totalTime from './total_time_component.vue'; export default { - props: { - items: Array, - stage: Object, - }, components: { userAvatarImage, limitWarning, totalTime, }, + props: { + items: { + type: Array, + default: () => [], + }, + stage: { + type: Object, + default: () => ({}), + }, + }, }; </script> <template> @@ -22,28 +28,44 @@ <limit-warning :count="items.length" /> </div> <ul class="stage-event-list"> - <li v-for="mergeRequest in items" class="stage-event-item"> + <li + v-for="(mergeRequest, i) in items" + :key="i" + class="stage-event-item" + > <div class="item-details"> <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> + <user-avatar-image :img-src="mergeRequest.author.avatarUrl" /> <h5 class="item-title merge-merquest-title"> <a :href="mergeRequest.url"> {{ mergeRequest.title }} </a> </h5> - <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> + <a + :href="mergeRequest.url" + class="issue-link"> + !{{ mergeRequest.iid }} + </a> · <span> {{ s__('OpenedNDaysAgo|Opened') }} - <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> + <a + :href="mergeRequest.url" + class="issue-date"> + {{ mergeRequest.createdAt }} + </a> </span> <span> {{ s__('ByAuthor|by') }} - <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> + <a + :href="mergeRequest.author.webUrl" + class="issue-author-link"> + {{ mergeRequest.author.name }} + </a> </span> </div> <div class="item-time"> - <total-time :time="mergeRequest.totalTime"></total-time> + <total-time :time="mergeRequest.totalTime" /> </div> </li> </ul> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_component.vue index 8c98bd249a1..907638d798a 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_component.vue @@ -4,15 +4,21 @@ import totalTime from './total_time_component.vue'; export default { - props: { - items: Array, - stage: Object, - }, components: { userAvatarImage, limitWarning, totalTime, }, + props: { + items: { + type: Array, + default: () => [], + }, + stage: { + type: Object, + default: () => ({}), + }, + }, }; </script> <template> @@ -25,30 +31,43 @@ <li v-for="(issue, i) in items" :key="i" - class="stage-event-item"> + class="stage-event-item" + > <div class="item-details"> <!-- FIXME: Pass an alt attribute here for accessibility --> <user-avatar-image :img-src="issue.author.avatarUrl"/> <h5 class="item-title issue-title"> - <a class="issue-title" :href="issue.url"> + <a + class="issue-title" + :href="issue.url" + > {{ issue.title }} </a> </h5> - <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> + <a + :href="issue.url" + class="issue-link" + >#{{ issue.iid }}</a> · <span> {{ s__('OpenedNDaysAgo|Opened') }} - <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> + <a + :href="issue.url" + class="issue-date" + >{{ issue.createdAt }}</a> </span> <span> {{ s__('ByAuthor|by') }} - <a :href="issue.author.webUrl" class="issue-author-link"> + <a + :href="issue.author.webUrl" + class="issue-author-link" + > {{ issue.author.name }} </a> </span> </div> <div class="item-time"> - <total-time :time="issue.totalTime"/> + <total-time :time="issue.totalTime" /> </div> </li> </ul> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue index 75d2f1fd70c..cee294b4ac2 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue @@ -5,15 +5,21 @@ import totalTime from './total_time_component.vue'; export default { - props: { - items: Array, - stage: Object, - }, components: { userAvatarImage, totalTime, limitWarning, }, + props: { + items: { + type: Array, + default: () => [], + }, + stage: { + type: Object, + default: () => ({}), + }, + }, computed: { iconCommit() { return iconCommit; @@ -31,10 +37,11 @@ <li v-for="(commit, i) in items" :key="i" - class="stage-event-item"> + class="stage-event-item" + > <div class="item-details item-conmmit-component"> <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image :img-src="commit.author.avatarUrl"/> + <user-avatar-image :img-src="commit.author.avatarUrl" /> <h5 class="item-title commit-title"> <a :href="commit.commitUrl"> {{ commit.title }} @@ -42,10 +49,20 @@ </h5> <span> {{ s__('FirstPushedBy|First') }} - <span class="commit-icon" v-html="iconCommit"></span> - <a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a> + <span + class="commit-icon" + v-html="iconCommit" + > + </span> + <a + :href="commit.commitUrl" + class="commit-hash-link commit-sha" + >{{ commit.shortSha }}</a> {{ s__('FirstPushedBy|pushed by') }} - <a :href="commit.author.webUrl" class="commit-author-link"> + <a + :href="commit.author.webUrl" + class="commit-author-link" + > {{ commit.author.name }} </a> </span> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue index cbce9205e75..39b699a6395 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue @@ -5,16 +5,22 @@ import icon from '../../vue_shared/components/icon.vue'; export default { - props: { - items: Array, - stage: Object, - }, components: { userAvatarImage, totalTime, limitWarning, icon, }, + props: { + items: { + type: Array, + default: () => [], + }, + stage: { + type: Object, + default: () => ({}), + }, + }, }; </script> <template> @@ -27,7 +33,8 @@ <li v-for="(mergeRequest, i) in items" :key="i" - class="stage-event-item"> + class="stage-event-item" + > <div class="item-details"> <!-- FIXME: Pass an alt attribute here for accessibility --> <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> @@ -36,34 +43,52 @@ {{ mergeRequest.title }} </a> </h5> - <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> + <a + :href="mergeRequest.url" + class="issue-link" + >!{{ mergeRequest.iid }}</a> · <span> {{ s__('OpenedNDaysAgo|Opened') }} - <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> + <a + :href="mergeRequest.url" + class="issue-date" + >{{ mergeRequest.createdAt }}</a> </span> <span> {{ s__('ByAuthor|by') }} - <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> + <a + :href="mergeRequest.author.webUrl" + class="issue-author-link" + >{{ mergeRequest.author.name }}</a> </span> <template v-if="mergeRequest.state === 'closed'"> <span class="merge-request-state"> - <i class="fa fa-ban"></i> + <i + class="fa fa-ban" + aria-hidden="true" + > + </i> {{ mergeRequest.state.toUpperCase() }} </span> </template> <template v-else> - <span class="merge-request-branch" v-if="mergeRequest.branch"> + <span + class="merge-request-branch" + v-if="mergeRequest.branch" + > <icon name="fork" - :size="16"> - </icon> - <a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a> + :size="16" + /> + <a :href="mergeRequest.branch.url"> + {{ mergeRequest.branch.name }} + </a> </span> </template> </div> <div class="item-time"> - <total-time :time="mergeRequest.totalTime"/> + <total-time :time="mergeRequest.totalTime" /> </div> </li> </ul> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue index 508a411e599..92f2a95a66a 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue @@ -6,16 +6,22 @@ import icon from '../../vue_shared/components/icon.vue'; export default { - props: { - items: Array, - stage: Object, - }, components: { userAvatarImage, totalTime, limitWarning, icon, }, + props: { + items: { + type: Array, + default: () => [], + }, + stage: { + type: Object, + default: () => ({}), + }, + }, computed: { iconBranch() { return iconBranch; @@ -33,30 +39,58 @@ <li v-for="(build, i) in items" class="stage-event-item item-build-component" - :key="i"> + :key="i" + > <div class="item-details"> <!-- FIXME: Pass an alt attribute here for accessibility --> <user-avatar-image :img-src="build.author.avatarUrl"/> <h5 class="item-title"> - <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> + <a + :href="build.url" + class="pipeline-id" + > + #{{ build.id }} + </a> <icon name="fork" - :size="16"> - </icon> - <a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a> - <span class="icon-branch" v-html="iconBranch"></span> - <a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a> + :size="16" + /> + <a + :href="build.branch.url" + class="ref-name" + > + {{ build.branch.name }} + </a> + <span + class="icon-branch" + v-html="iconBranch" + > + </span> + <a + :href="build.commitUrl" + class="commit-sha" + > + {{ build.shortSha }} + </a> </h5> <span> - <a :href="build.url" class="build-date">{{ build.date }}</a> + <a + :href="build.url" + class="build-date" + > + {{ build.date }} + </a> {{ s__('ByAuthor|by') }} - <a :href="build.author.webUrl" class="issue-author-link"> + <a + :href="build.author.webUrl" + class="issue-author-link" + > {{ build.author.name }} </a> </span> </div> <div class="item-time"> - <total-time :time="build.totalTime"/> + <total-time :time="build.totalTime" /> </div> </li> </ul> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue index 88fa6b073ca..b84bb6ed792 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue @@ -6,15 +6,21 @@ import icon from '../../vue_shared/components/icon.vue'; export default { - props: { - items: Array, - stage: Object, - }, components: { totalTime, limitWarning, icon, }, + props: { + items: { + type: Array, + default: () => [], + }, + stage: { + type: Object, + default: () => ({}), + }, + }, computed: { iconBuildStatus() { return iconBuildStatus; @@ -35,29 +41,59 @@ <li v-for="(build, i) in items" :key="i" - class="stage-event-item item-build-component"> + class="stage-event-item item-build-component" + > <div class="item-details"> <h5 class="item-title"> - <span class="icon-build-status" v-html="iconBuildStatus"></span> - <a :href="build.url" class="item-build-name">{{ build.name }}</a> + <span + class="icon-build-status" + v-html="iconBuildStatus" + > + </span> + <a + :href="build.url" + class="item-build-name" + > + {{ build.name }} + </a> · - <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> + <a + :href="build.url" + class="pipeline-id" + > + #{{ build.id }} + </a> <icon name="fork" - :size="16"> - </icon> - <a :href="build.branch.url" class="ref-name">{{ build.branch.name }}</a> - <span class="icon-branch" v-html="iconBranch"></span> - <a :href="build.commitUrl" class="commit-sha">{{ build.shortSha }}</a> + :size="16" + /> + <a + :href="build.branch.url" + class="ref-name" + > + {{ build.branch.name }} + </a> + <span + class="icon-branch" + v-html="iconBranch" + > + </span> + <a + :href="build.commitUrl" + class="commit-sha"> + {{ build.shortSha }} + </a> </h5> <span> - <a :href="build.url" class="issue-date"> + <a + :href="build.url" + class="issue-date"> {{ build.date }} </a> </span> </div> <div class="item-time"> - <total-time :time="build.totalTime"/> + <total-time :time="build.totalTime" /> </div> </li> </ul> diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue index 62efd4f9c28..7758bf0cb3f 100644 --- a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue @@ -17,13 +17,33 @@ <template> <span class="total-time"> <template v-if="hasData"> - <template v-if="time.days">{{ time.days }} <span>{{ n__('day', 'days', time.days) }}</span></template> - <template v-if="time.hours">{{ time.hours }} <span>{{ n__('Time|hr', 'Time|hrs', time.hours) }}</span></template> - <template v-if="time.mins && !time.days">{{ time.mins }} <span>{{ n__('Time|min', 'Time|mins', time.mins) }}</span></template> - <template v-if="time.seconds && hasData === 1 || time.seconds === 0">{{ time.seconds }} <span>{{ s__('Time|s') }}</span></template> + <template v-if="time.days"> + {{ time.days }} + <span> + {{ n__('day', 'days', time.days) }} + </span> + </template> + <template v-if="time.hours"> + {{ time.hours }} + <span> + {{ n__('Time|hr', 'Time|hrs', time.hours) }} + </span> + </template> + <template v-if="time.mins && !time.days"> + {{ time.mins }} + <span> + {{ n__('Time|min', 'Time|mins', time.mins) }} + </span> + </template> + <template v-if="time.seconds && hasData === 1 || time.seconds === 0"> + {{ time.seconds }} + <span> + {{ s__('Time|s') }} + </span> + </template> </template> <template v-else> -- </template> - </span> + </span> </template> diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 49bb6c52180..034f2923b3b 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -20,6 +20,16 @@ $(() => { gl.cycleAnalyticsApp = new Vue({ el: '#cycle-analytics', name: 'CycleAnalytics', + components: { + banner, + 'stage-issue-component': stageComponent, + 'stage-plan-component': stagePlanComponent, + 'stage-code-component': stageCodeComponent, + 'stage-test-component': stageTestComponent, + 'stage-review-component': stageReviewComponent, + 'stage-staging-component': stageStagingComponent, + 'stage-production-component': stageComponent, + }, data() { const cycleAnalyticsEl = document.querySelector('#cycle-analytics'); const cycleAnalyticsService = new CycleAnalyticsService({ @@ -43,16 +53,6 @@ $(() => { return this.store.currentActiveStage(); }, }, - components: { - banner, - 'stage-issue-component': stageComponent, - 'stage-plan-component': stagePlanComponent, - 'stage-code-component': stageCodeComponent, - 'stage-test-component': stageTestComponent, - 'stage-review-component': stageReviewComponent, - 'stage-staging-component': stageStagingComponent, - 'stage-production-component': stageComponent, - }, created() { this.fetchCycleAnalyticsData(); }, diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue index f9f2f9bf693..b839b9f286f 100644 --- a/app/assets/javascripts/deploy_keys/components/action_btn.vue +++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue @@ -3,10 +3,8 @@ import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { - data() { - return { - isLoading: false, - }; + components: { + loadingIcon, }, props: { deployKey: { @@ -23,11 +21,16 @@ default: 'btn-default', }, }, - - components: { - loadingIcon, + data() { + return { + isLoading: false, + }; + }, + computed: { + text() { + return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`; + }, }, - methods: { doAction() { this.isLoading = true; @@ -37,11 +40,6 @@ }); }, }, - computed: { - text() { - return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`; - }, - }, }; </script> diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue index fe046449054..7b68b19de75 100644 --- a/app/assets/javascripts/deploy_keys/components/app.vue +++ b/app/assets/javascripts/deploy_keys/components/app.vue @@ -7,11 +7,9 @@ import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { - data() { - return { - isLoading: false, - store: new DeployKeysStore(), - }; + components: { + keysPanel, + loadingIcon, }, props: { endpoint: { @@ -19,6 +17,12 @@ required: true, }, }, + data() { + return { + isLoading: false, + store: new DeployKeysStore(), + }; + }, computed: { hasKeys() { return Object.keys(this.keys).length; @@ -27,9 +31,20 @@ return this.store.keys; }, }, - components: { - keysPanel, - loadingIcon, + created() { + this.service = new DeployKeysService(this.endpoint); + + eventHub.$on('enable.key', this.enableKey); + eventHub.$on('remove.key', this.disableKey); + eventHub.$on('disable.key', this.disableKey); + }, + mounted() { + this.fetchKeys(); + }, + beforeDestroy() { + eventHub.$off('enable.key', this.enableKey); + eventHub.$off('remove.key', this.disableKey); + eventHub.$off('disable.key', this.disableKey); }, methods: { fetchKeys() { @@ -59,21 +74,6 @@ } }, }, - created() { - this.service = new DeployKeysService(this.endpoint); - - eventHub.$on('enable.key', this.enableKey); - eventHub.$on('remove.key', this.disableKey); - eventHub.$on('disable.key', this.disableKey); - }, - mounted() { - this.fetchKeys(); - }, - beforeDestroy() { - eventHub.$off('enable.key', this.enableKey); - eventHub.$off('remove.key', this.disableKey); - eventHub.$off('disable.key', this.disableKey); - }, }; </script> diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index 2a05c6f001e..a9e819b8a3c 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -3,6 +3,9 @@ import { getTimeago } from '../../lib/utils/datetime_utility'; export default { + components: { + actionBtn, + }, props: { deployKey: { type: Object, @@ -17,9 +20,6 @@ required: true, }, }, - components: { - actionBtn, - }, computed: { timeagoDate() { return getTimeago().format(this.deployKey.created_at); @@ -61,9 +61,10 @@ </div> <div class="deploy-key-content prepend-left-default deploy-key-projects"> <a - v-for="project in deployKey.projects" + v-for="(project, i) in deployKey.projects" class="label deploy-project-label" :href="project.full_path" + :key="i" > {{ project.full_name }} </a> diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue index 9e6fb244af6..822b0323156 100644 --- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue +++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue @@ -2,6 +2,9 @@ import key from './key.vue'; export default { + components: { + key, + }, props: { title: { type: String, @@ -25,9 +28,6 @@ required: true, }, }, - components: { - key, - }, }; </script> @@ -37,12 +37,14 @@ {{ title }} ({{ keys.length }}) </h5> - <ul class="well-list" + <ul + class="well-list" v-if="keys.length" > <li v-for="deployKey in keys" - :key="deployKey.id"> + :key="deployKey.id" + > <key :deploy-key="deployKey" :store="store" diff --git a/app/assets/javascripts/deploy_keys/index.js b/app/assets/javascripts/deploy_keys/index.js index a5f232f950a..ca8798facc9 100644 --- a/app/assets/javascripts/deploy_keys/index.js +++ b/app/assets/javascripts/deploy_keys/index.js @@ -3,14 +3,14 @@ import deployKeysApp from './components/app.vue'; document.addEventListener('DOMContentLoaded', () => new Vue({ el: document.getElementById('js-deploy-keys'), + components: { + deployKeysApp, + }, data() { return { endpoint: this.$options.el.dataset.endpoint, }; }, - components: { - deployKeysApp, - }, render(createElement) { return createElement('deploy-keys-app', { props: { diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 11ec667fc07..08f12ba4d95 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -13,7 +13,6 @@ import groupAvatar from './group_avatar'; import GroupLabelSubscription from './group_label_subscription'; import LineHighlighter from './line_highlighter'; import BuildArtifacts from './build_artifacts'; -import CILintEditor from './ci_lint_editor'; import groupsSelect from './groups_select'; import Search from './search'; import initAdmin from './admin'; @@ -188,15 +187,25 @@ import Activities from './activities'; initIssuableSidebar(); break; case 'dashboard:milestones:index': - projectSelect(); + import('./pages/dashboard/milestones/index') + .then(callDefault) + .catch(fail); break; case 'projects:milestones:show': case 'groups:milestones:show': - case 'dashboard:milestones:show': new Milestone(); new Sidebar(); break; + case 'dashboard:milestones:show': + import('./pages/dashboard/milestones/show') + .then(callDefault) + .catch(fail); + break; case 'dashboard:issues': + import('./pages/dashboard/issues') + .then(callDefault) + .catch(fail); + break; case 'dashboard:merge_requests': projectSelect(); initLegacyFilters(); @@ -212,6 +221,12 @@ import Activities from './activities'; case 'dashboard:todos:index': import('./pages/dashboard/todos/index').then(callDefault).catch(fail); break; + case 'dashboard:projects:index': + case 'dashboard:projects:starred': + import('./pages/dashboard/projects') + .then(callDefault) + .catch(fail); + break; case 'explore:projects:index': case 'explore:projects:trending': case 'explore:projects:starred': @@ -535,22 +550,19 @@ import Activities from './activities'; break; case 'ci:lints:create': case 'ci:lints:show': - new CILintEditor(); + import('./pages/ci/lints').then(m => m.default()).catch(fail); break; case 'users:show': import('./pages/users/show').then(callDefault).catch(fail); break; case 'admin:conversational_development_index:show': - new UserCallout(); + import('./pages/admin/conversational_development_index/show').then(m => m.default()).catch(fail); break; case 'snippets:show': - new LineHighlighter(); - new BlobViewer(); - initNotes(); - new ZenMode(); + import('./pages/snippets/show').then(m => m.default()).catch(fail); break; case 'import:fogbugz:new_user_map': - new UsersSelect(); + import('./pages/import/fogbugz/new_user_map').then(m => m.default()).catch(fail); break; case 'profiles:personal_access_tokens:index': case 'admin:impersonation_tokens:index': diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue index 3236077c3cf..dbee81fa320 100644 --- a/app/assets/javascripts/environments/components/container.vue +++ b/app/assets/javascripts/environments/components/container.vue @@ -4,6 +4,11 @@ import environmentTable from '../components/environments_table.vue'; export default { + components: { + environmentTable, + loadingIcon, + tablePagination, + }, props: { isLoading: { type: Boolean, @@ -26,12 +31,6 @@ required: true, }, }, - components: { - environmentTable, - loadingIcon, - tablePagination, - }, - methods: { onChangePage(page) { this.$emit('onChangePage', page); @@ -47,7 +46,7 @@ label="Loading environments" v-if="isLoading" size="3" - /> + /> <slot name="emptyState"></slot> @@ -59,13 +58,13 @@ :environments="environments" :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" - /> + /> <table-pagination v-if="pagination && pagination.totalPages > 1" :change="onChangePage" - :pageInfo="pagination" - /> + :page-info="pagination" + /> </div> </div> </template> diff --git a/app/assets/javascripts/environments/components/empty_state.vue b/app/assets/javascripts/environments/components/empty_state.vue index 2646f08c8e6..00e63c3467a 100644 --- a/app/assets/javascripts/environments/components/empty_state.vue +++ b/app/assets/javascripts/environments/components/empty_state.vue @@ -1,6 +1,6 @@ <script> export default { - name: 'environmentsEmptyState', + name: 'EnvironmentsEmptyState', props: { newPath: { type: String, @@ -21,21 +21,23 @@ <div class="blank-state-row"> <div class="blank-state-center"> <h2 class="blank-state-title js-blank-state-title"> - {{s__("Environments|You don't have any environments right now.")}} + {{ s__("Environments|You don't have any environments right now.") }} </h2> <p class="blank-state-text"> - {{s__("Environments|Environments are places where code gets deployed, such as staging or production.")}} + {{ s__(`Environments|Environments are places where +code gets deployed, such as staging or production.`) }} <br /> <a :href="helpPath"> - {{s__("Environments|Read more about environments")}} + {{ s__("Environments|Read more about environments") }} </a> </p> <a v-if="canCreateEnvironment" :href="newPath" - class="btn btn-create js-new-environment-button"> - {{s__("Environments|New environment")}} + class="btn btn-create js-new-environment-button" + > + {{ s__("Environments|New environment") }} </a> </div> </div> diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index e7495677e7c..16bd2f5feb3 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -1,55 +1,54 @@ <script> -import playIconSvg from 'icons/_icon_play.svg'; -import eventHub from '../event_hub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; + import playIconSvg from 'icons/_icon_play.svg'; + import eventHub from '../event_hub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; -export default { - props: { - actions: { - type: Array, - required: false, - default: () => [], + export default { + directives: { + tooltip, }, - }, - directives: { - tooltip, - }, - - components: { - loadingIcon, - }, + components: { + loadingIcon, + }, + props: { + actions: { + type: Array, + required: false, + default: () => [], + }, + }, - data() { - return { - playIconSvg, - isLoading: false, - }; - }, + data() { + return { + playIconSvg, + isLoading: false, + }; + }, - computed: { - title() { - return 'Deploy to...'; + computed: { + title() { + return 'Deploy to...'; + }, }, - }, - methods: { - onClickAction(endpoint) { - this.isLoading = true; + methods: { + onClickAction(endpoint) { + this.isLoading = true; - eventHub.$emit('postAction', endpoint); - }, + eventHub.$emit('postAction', endpoint); + }, - isActionDisabled(action) { - if (action.playable === undefined) { - return false; - } + isActionDisabled(action) { + if (action.playable === undefined) { + return false; + } - return !action.playable; + return !action.playable; + }, }, - }, -}; + }; </script> <template> <div @@ -63,27 +62,33 @@ export default { data-toggle="dropdown" :title="title" :aria-label="title" - :disabled="isLoading"> + :disabled="isLoading" + > <span> <span v-html="playIconSvg"></span> <i class="fa fa-caret-down" - aria-hidden="true"/> + aria-hidden="true" + > + </i> <loading-icon v-if="isLoading" /> </span> </button> <ul class="dropdown-menu dropdown-menu-align-right"> - <li v-for="action in actions"> + <li + v-for="(action, i) in actions" + :key="i"> <button type="button" class="js-manual-action-link no-btn btn" @click="onClickAction(action.play_path)" :class="{ disabled: isActionDisabled(action) }" - :disabled="isActionDisabled(action)"> + :disabled="isActionDisabled(action)" + > <span v-html="playIconSvg"></span> <span> - {{action.name}} + {{ action.name }} </span> </button> </li> diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue index 520c3ac8ace..c9a68cface6 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.vue +++ b/app/assets/javascripts/environments/components/environment_external_url.vue @@ -1,28 +1,27 @@ <script> -import tooltip from '../../vue_shared/directives/tooltip'; -import { s__ } from '../../locale'; + import tooltip from '../../vue_shared/directives/tooltip'; + import { s__ } from '../../locale'; -/** - * Renders the external url link in environments table. - */ -export default { - props: { - externalUrl: { - type: String, - required: true, + /** + * Renders the external url link in environments table. + */ + export default { + directives: { + tooltip, + }, + props: { + externalUrl: { + type: String, + required: true, + }, }, - }, - - directives: { - tooltip, - }, - computed: { - title() { - return s__('Environments|Open'); + computed: { + title() { + return s__('Environments|Open'); + }, }, - }, -}; + }; </script> <template> <a @@ -33,9 +32,12 @@ export default { rel="noopener noreferrer nofollow" :title="title" :aria-label="title" - :href="externalUrl"> + :href="externalUrl" + > <i class="fa fa-external-link" - aria-hidden="true" /> + aria-hidden="true" + > + </i> </a> </template> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 2f0e397aa45..a9d554e549e 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -1,423 +1,424 @@ <script> -import Timeago from 'timeago.js'; -import _ from 'underscore'; -import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import { humanize } from '../../lib/utils/text_utility'; -import ActionsComponent from './environment_actions.vue'; -import ExternalUrlComponent from './environment_external_url.vue'; -import StopComponent from './environment_stop.vue'; -import RollbackComponent from './environment_rollback.vue'; -import TerminalButtonComponent from './environment_terminal_button.vue'; -import MonitoringButtonComponent from './environment_monitoring.vue'; -import CommitComponent from '../../vue_shared/components/commit.vue'; -import eventHub from '../event_hub'; - -/** - * Envrionment Item Component - * - * Renders a table row for each environment. - */ -const timeagoInstance = new Timeago(); - -export default { - components: { - userAvatarLink, - 'commit-component': CommitComponent, - 'actions-component': ActionsComponent, - 'external-url-component': ExternalUrlComponent, - 'stop-component': StopComponent, - 'rollback-component': RollbackComponent, - 'terminal-button-component': TerminalButtonComponent, - 'monitoring-button-component': MonitoringButtonComponent, - }, - - props: { - model: { - type: Object, - required: true, - default: () => ({}), + import Timeago from 'timeago.js'; + import _ from 'underscore'; + import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + import { humanize } from '../../lib/utils/text_utility'; + import ActionsComponent from './environment_actions.vue'; + import ExternalUrlComponent from './environment_external_url.vue'; + import StopComponent from './environment_stop.vue'; + import RollbackComponent from './environment_rollback.vue'; + import TerminalButtonComponent from './environment_terminal_button.vue'; + import MonitoringButtonComponent from './environment_monitoring.vue'; + import CommitComponent from '../../vue_shared/components/commit.vue'; + import eventHub from '../event_hub'; + + /** + * Envrionment Item Component + * + * Renders a table row for each environment. + */ + const timeagoInstance = new Timeago(); + + export default { + components: { + userAvatarLink, + 'commit-component': CommitComponent, + 'actions-component': ActionsComponent, + 'external-url-component': ExternalUrlComponent, + 'stop-component': StopComponent, + 'rollback-component': RollbackComponent, + 'terminal-button-component': TerminalButtonComponent, + 'monitoring-button-component': MonitoringButtonComponent, }, - canCreateDeployment: { - type: Boolean, - required: false, - default: false, + props: { + model: { + type: Object, + required: true, + default: () => ({}), + }, + + canCreateDeployment: { + type: Boolean, + required: false, + default: false, + }, + + canReadEnvironment: { + type: Boolean, + required: false, + default: false, + }, }, - canReadEnvironment: { - type: Boolean, - required: false, - default: false, - }, - }, - - computed: { - /** - * Verifies if `last_deployment` key exists in the current Envrionment. - * This key is required to render most of the html - this method works has - * an helper. - * - * @returns {Boolean} - */ - hasLastDeploymentKey() { - if (this.model && - this.model.last_deployment && - !_.isEmpty(this.model.last_deployment)) { - return true; - } - return false; - }, - - /** - * Verifies is the given environment has manual actions. - * Used to verify if we should render them or nor. - * - * @returns {Boolean|Undefined} - */ - hasManualActions() { - return this.model && - this.model.last_deployment && - this.model.last_deployment.manual_actions && - this.model.last_deployment.manual_actions.length > 0; - }, - - /** - * Returns the value of the `stop_action?` key provided in the response. - * - * @returns {Boolean} - */ - hasStopAction() { - return this.model && this.model['stop_action?']; - }, - - /** - * Verifies if the `deployable` key is present in `last_deployment` key. - * Used to verify whether we should or not render the rollback partial. - * - * @returns {Boolean|Undefined} - */ - canRetry() { - return this.model && - this.hasLastDeploymentKey && - this.model.last_deployment && - this.model.last_deployment.deployable; - }, - - /** - * Verifies if the date to be shown is present. - * - * @returns {Boolean|Undefined} - */ - canShowDate() { - return this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable !== undefined; - }, - - /** - * Human readable date. - * - * @returns {String} - */ - createdDate() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.created_at) { - return timeagoInstance.format(this.model.last_deployment.deployable.created_at); - } - return ''; - }, - - /** - * Returns the manual actions with the name parsed. - * - * @returns {Array.<Object>|Undefined} - */ - manualActions() { - if (this.hasManualActions) { - return this.model.last_deployment.manual_actions.map((action) => { - const parsedAction = { - name: humanize(action.name), - play_path: action.play_path, - playable: action.playable, - }; - return parsedAction; - }); - } - return []; - }, - - /** - * Builds the string used in the user image alt attribute. - * - * @returns {String} - */ - userImageAltDescription() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.user && - this.model.last_deployment.user.username) { - return `${this.model.last_deployment.user.username}'s avatar'`; - } - return ''; - }, - - /** - * If provided, returns the commit tag. - * - * @returns {String|Undefined} - */ - commitTag() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.tag) { - return this.model.last_deployment.tag; - } - return undefined; + computed: { + /** + * Verifies if `last_deployment` key exists in the current Envrionment. + * This key is required to render most of the html - this method works has + * an helper. + * + * @returns {Boolean} + */ + hasLastDeploymentKey() { + if (this.model && + this.model.last_deployment && + !_.isEmpty(this.model.last_deployment)) { + return true; + } + return false; + }, + + /** + * Verifies is the given environment has manual actions. + * Used to verify if we should render them or nor. + * + * @returns {Boolean|Undefined} + */ + hasManualActions() { + return this.model && + this.model.last_deployment && + this.model.last_deployment.manual_actions && + this.model.last_deployment.manual_actions.length > 0; + }, + + /** + * Returns the value of the `stop_action?` key provided in the response. + * + * @returns {Boolean} + */ + hasStopAction() { + return this.model && this.model['stop_action?']; + }, + + /** + * Verifies if the `deployable` key is present in `last_deployment` key. + * Used to verify whether we should or not render the rollback partial. + * + * @returns {Boolean|Undefined} + */ + canRetry() { + return this.model && + this.hasLastDeploymentKey && + this.model.last_deployment && + this.model.last_deployment.deployable; + }, + + /** + * Verifies if the date to be shown is present. + * + * @returns {Boolean|Undefined} + */ + canShowDate() { + return this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable !== undefined; + }, + + /** + * Human readable date. + * + * @returns {String} + */ + createdDate() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.created_at) { + return timeagoInstance.format(this.model.last_deployment.deployable.created_at); + } + return ''; + }, + + /** + * Returns the manual actions with the name parsed. + * + * @returns {Array.<Object>|Undefined} + */ + manualActions() { + if (this.hasManualActions) { + return this.model.last_deployment.manual_actions.map((action) => { + const parsedAction = { + name: humanize(action.name), + play_path: action.play_path, + playable: action.playable, + }; + return parsedAction; + }); + } + return []; + }, + + /** + * Builds the string used in the user image alt attribute. + * + * @returns {String} + */ + userImageAltDescription() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.user && + this.model.last_deployment.user.username) { + return `${this.model.last_deployment.user.username}'s avatar'`; + } + return ''; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {String|Undefined} + */ + commitTag() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.tag) { + return this.model.last_deployment.tag; + } + return undefined; + }, + + /** + * If provided, returns the commit ref. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.ref) { + return this.model.last_deployment.ref; + } + return undefined; + }, + + /** + * If provided, returns the commit url. + * + * @returns {String|Undefined} + */ + commitUrl() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.commit_path) { + return this.model.last_deployment.commit.commit_path; + } + return undefined; + }, + + /** + * If provided, returns the commit short sha. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.short_id) { + return this.model.last_deployment.commit.short_id; + } + return undefined; + }, + + /** + * If provided, returns the commit title. + * + * @returns {String|Undefined} + */ + commitTitle() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.title) { + return this.model.last_deployment.commit.title; + } + return undefined; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {Object|Undefined} + */ + commitAuthor() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.author) { + return this.model.last_deployment.commit.author; + } + + return undefined; + }, + + /** + * Verifies if the `retry_path` key is present and returns its value. + * + * @returns {String|Undefined} + */ + retryUrl() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.retry_path) { + return this.model.last_deployment.deployable.retry_path; + } + return undefined; + }, + + /** + * Verifies if the `last?` key is present and returns its value. + * + * @returns {Boolean|Undefined} + */ + isLastDeployment() { + return this.model && this.model.last_deployment && + this.model.last_deployment['last?']; + }, + + /** + * Builds the name of the builds needed to display both the name and the id. + * + * @returns {String} + */ + buildName() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable) { + const deployable = this.model.last_deployment.deployable; + return `${deployable.name} #${deployable.id}`; + } + return ''; + }, + + /** + * Builds the needed string to show the internal id. + * + * @returns {String} + */ + deploymentInternalId() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.iid) { + return `#${this.model.last_deployment.iid}`; + } + return ''; + }, + + /** + * Verifies if the user object is present under last_deployment object. + * + * @returns {Boolean} + */ + deploymentHasUser() { + return this.model && + !_.isEmpty(this.model.last_deployment) && + !_.isEmpty(this.model.last_deployment.user); + }, + + /** + * Returns the user object nested with the last_deployment object. + * Used to render the template. + * + * @returns {Object} + */ + deploymentUser() { + if (this.model && + !_.isEmpty(this.model.last_deployment) && + !_.isEmpty(this.model.last_deployment.user)) { + return this.model.last_deployment.user; + } + return {}; + }, + + /** + * Verifies if the build name column should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderBuildName() { + return !this.model.isFolder && + !_.isEmpty(this.model.last_deployment) && + !_.isEmpty(this.model.last_deployment.deployable); + }, + + /** + * Verifies the presence of all the keys needed to render the buil_path. + * + * @return {String} + */ + buildPath() { + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.build_path) { + return this.model.last_deployment.deployable.build_path; + } + + return ''; + }, + + /** + * Verifies the presence of all the keys needed to render the external_url. + * + * @return {String} + */ + externalURL() { + if (this.model && this.model.external_url) { + return this.model.external_url; + } + + return ''; + }, + + /** + * Verifies if deplyment internal ID should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderDeploymentID() { + return !this.model.isFolder && + !_.isEmpty(this.model.last_deployment) && + this.model.last_deployment.iid !== undefined; + }, + + environmentPath() { + if (this.model && this.model.environment_path) { + return this.model.environment_path; + } + + return ''; + }, + + monitoringUrl() { + if (this.model && this.model.metrics_path) { + return this.model.metrics_path; + } + + return ''; + }, + + displayEnvironmentActions() { + return this.hasManualActions || + this.externalURL || + this.monitoringUrl || + this.hasStopAction || + this.canRetry; + }, }, - /** - * If provided, returns the commit ref. - * - * @returns {Object|Undefined} - */ - commitRef() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.ref) { - return this.model.last_deployment.ref; - } - return undefined; + methods: { + onClickFolder() { + eventHub.$emit('toggleFolder', this.model); + }, }, - - /** - * If provided, returns the commit url. - * - * @returns {String|Undefined} - */ - commitUrl() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.commit_path) { - return this.model.last_deployment.commit.commit_path; - } - return undefined; - }, - - /** - * If provided, returns the commit short sha. - * - * @returns {String|Undefined} - */ - commitShortSha() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.short_id) { - return this.model.last_deployment.commit.short_id; - } - return undefined; - }, - - /** - * If provided, returns the commit title. - * - * @returns {String|Undefined} - */ - commitTitle() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.title) { - return this.model.last_deployment.commit.title; - } - return undefined; - }, - - /** - * If provided, returns the commit tag. - * - * @returns {Object|Undefined} - */ - commitAuthor() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.author) { - return this.model.last_deployment.commit.author; - } - - return undefined; - }, - - /** - * Verifies if the `retry_path` key is present and returns its value. - * - * @returns {String|Undefined} - */ - retryUrl() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.retry_path) { - return this.model.last_deployment.deployable.retry_path; - } - return undefined; - }, - - /** - * Verifies if the `last?` key is present and returns its value. - * - * @returns {Boolean|Undefined} - */ - isLastDeployment() { - return this.model && this.model.last_deployment && - this.model.last_deployment['last?']; - }, - - /** - * Builds the name of the builds needed to display both the name and the id. - * - * @returns {String} - */ - buildName() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable) { - return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`; - } - return ''; - }, - - /** - * Builds the needed string to show the internal id. - * - * @returns {String} - */ - deploymentInternalId() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.iid) { - return `#${this.model.last_deployment.iid}`; - } - return ''; - }, - - /** - * Verifies if the user object is present under last_deployment object. - * - * @returns {Boolean} - */ - deploymentHasUser() { - return this.model && - !_.isEmpty(this.model.last_deployment) && - !_.isEmpty(this.model.last_deployment.user); - }, - - /** - * Returns the user object nested with the last_deployment object. - * Used to render the template. - * - * @returns {Object} - */ - deploymentUser() { - if (this.model && - !_.isEmpty(this.model.last_deployment) && - !_.isEmpty(this.model.last_deployment.user)) { - return this.model.last_deployment.user; - } - return {}; - }, - - /** - * Verifies if the build name column should be rendered by verifing - * if all the information needed is present - * and if the environment is not a folder. - * - * @returns {Boolean} - */ - shouldRenderBuildName() { - return !this.model.isFolder && - !_.isEmpty(this.model.last_deployment) && - !_.isEmpty(this.model.last_deployment.deployable); - }, - - /** - * Verifies the presence of all the keys needed to render the buil_path. - * - * @return {String} - */ - buildPath() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.build_path) { - return this.model.last_deployment.deployable.build_path; - } - - return ''; - }, - - /** - * Verifies the presence of all the keys needed to render the external_url. - * - * @return {String} - */ - externalURL() { - if (this.model && this.model.external_url) { - return this.model.external_url; - } - - return ''; - }, - - /** - * Verifies if deplyment internal ID should be rendered by verifing - * if all the information needed is present - * and if the environment is not a folder. - * - * @returns {Boolean} - */ - shouldRenderDeploymentID() { - return !this.model.isFolder && - !_.isEmpty(this.model.last_deployment) && - this.model.last_deployment.iid !== undefined; - }, - - environmentPath() { - if (this.model && this.model.environment_path) { - return this.model.environment_path; - } - - return ''; - }, - - monitoringUrl() { - if (this.model && this.model.metrics_path) { - return this.model.metrics_path; - } - - return ''; - }, - - displayEnvironmentActions() { - return this.hasManualActions || - this.externalURL || - this.monitoringUrl || - this.hasStopAction || - this.canRetry; - }, - }, - - methods: { - onClickFolder() { - eventHub.$emit('toggleFolder', this.model); - }, - }, -}; + }; </script> <template> <div @@ -427,18 +428,22 @@ export default { 'folder-row': model.isFolder, }" role="row"> - <div class="table-section section-10" role="gridcell"> + <div + class="table-section section-10" + role="gridcell" + > <div v-if="!model.isFolder" class="table-mobile-header" - role="rowheader"> - {{s__("Environments|Environment")}} + role="rowheader" + > + {{ s__("Environments|Environment") }} </div> <a v-if="!model.isFolder" class="environment-name flex-truncate-parent table-mobile-content" :href="environmentPath"> - <span class="flex-truncate-child">{{model.name}}</span> + <span class="flex-truncate-child">{{ model.name }}</span> </a> <span v-else @@ -450,32 +455,40 @@ export default { <i v-show="model.isOpen" class="fa fa-caret-down" - aria-hidden="true" /> + aria-hidden="true" + > + </i> <i v-show="!model.isOpen" class="fa fa-caret-right" - aria-hidden="true"/> + aria-hidden="true" + > + </i> </span> <span class="folder-icon"> <i class="fa fa-folder" - aria-hidden="true" /> + aria-hidden="true"> + </i> </span> <span> - {{model.folderName}} + {{ model.folderName }} </span> <span class="badge"> - {{model.size}} + {{ model.size }} </span> </span> </div> - <div class="table-section section-10 deployment-column hidden-xs hidden-sm" role="gridcell"> + <div + class="table-section section-10 deployment-column hidden-xs hidden-sm" + role="gridcell" + > <span v-if="shouldRenderDeploymentID"> - {{deploymentInternalId}} + {{ deploymentInternalId }} </span> <span v-if="!model.isFolder && deploymentHasUser"> @@ -490,22 +503,29 @@ export default { </span> </div> - <div class="table-section section-15 hidden-xs hidden-sm" role="gridcell"> + <div + class="table-section section-15 hidden-xs hidden-sm" + role="gridcell" + > <a v-if="shouldRenderBuildName" class="build-link flex-truncate-parent" - :href="buildPath"> - <span class="flex-truncate-child">{{buildName}}</span> + :href="buildPath" + > + <span class="flex-truncate-child">{{ buildName }}</span> </a> </div> <div v-if="!model.isFolder" - class="table-section section-25" role="gridcell"> + class="table-section section-25" + role="gridcell" + > <div role="rowheader" - class="table-mobile-header"> - {{s__("Environments|Commit")}} + class="table-mobile-header" + > + {{ s__("Environments|Commit") }} </div> <div v-if="hasLastDeploymentKey" @@ -521,22 +541,24 @@ export default { <div v-if="!hasLastDeploymentKey" class="commit-title table-mobile-content"> - {{s__("Environments|No deployments yet")}} + {{ s__("Environments|No deployments yet") }} </div> </div> <div v-if="!model.isFolder" - class="table-section section-10" role="gridcell"> + class="table-section section-10" + role="gridcell" + > <div role="rowheader" class="table-mobile-header"> - {{s__("Environments|Updated")}} + {{ s__("Environments|Updated") }} </div> <span v-if="canShowDate" class="environment-created-date-timeago table-mobile-content"> - {{createdDate}} + {{ createdDate }} </span> </div> @@ -552,33 +574,33 @@ export default { <actions-component v-if="hasManualActions && canCreateDeployment" :actions="manualActions" - /> + /> <external-url-component v-if="externalURL && canReadEnvironment" :external-url="externalURL" - /> + /> <monitoring-button-component v-if="monitoringUrl && canReadEnvironment" :monitoring-url="monitoringUrl" - /> + /> <terminal-button-component v-if="model && model.terminal_path" :terminal-path="model.terminal_path" - /> + /> <stop-component v-if="hasStopAction && canCreateDeployment" :stop-url="model.stop_path" - /> + /> <rollback-component v-if="canRetry && canCreateDeployment" :is-last-deployment="isLastDeployment" :retry-url="retryUrl" - /> + /> </div> </div> </div> diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index b45af1a5ebc..081537cf218 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.vue +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -1,27 +1,27 @@ <script> -/** - * Renders the Monitoring (Metrics) link in environments table. - */ -import tooltip from '../../vue_shared/directives/tooltip'; + /** + * Renders the Monitoring (Metrics) link in environments table. + */ + import tooltip from '../../vue_shared/directives/tooltip'; -export default { - props: { - monitoringUrl: { - type: String, - required: true, + export default { + directives: { + tooltip, }, - }, - directives: { - tooltip, - }, + props: { + monitoringUrl: { + type: String, + required: true, + }, + }, - computed: { - title() { - return 'Monitoring'; + computed: { + title() { + return 'Monitoring'; + }, }, - }, -}; + }; </script> <template> <a @@ -31,10 +31,12 @@ export default { rel="noopener noreferrer nofollow" :href="monitoringUrl" :title="title" - :aria-label="title"> + :aria-label="title" + > <i class="fa fa-area-chart" aria-hidden="true" - /> + > + </i> </a> </template> diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue index 92a596bfd33..605a88e997e 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.vue +++ b/app/assets/javascripts/environments/components/environment_rollback.vue @@ -1,57 +1,58 @@ <script> -/** - * Renders Rollback or Re deploy button in environments table depending - * of the provided property `isLastDeployment`. - * - * Makes a post request when the button is clicked. - */ -import eventHub from '../event_hub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - -export default { - props: { - retryUrl: { - type: String, - default: '', + /** + * Renders Rollback or Re deploy button in environments table depending + * of the provided property `isLastDeployment`. + * + * Makes a post request when the button is clicked. + */ + import eventHub from '../event_hub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + + export default { + components: { + loadingIcon, }, - isLastDeployment: { - type: Boolean, - default: true, - }, - }, + props: { + retryUrl: { + type: String, + default: '', + }, - components: { - loadingIcon, - }, + isLastDeployment: { + type: Boolean, + default: true, + }, + }, - data() { - return { - isLoading: false, - }; - }, + data() { + return { + isLoading: false, + }; + }, - methods: { - onClick() { - this.isLoading = true; + methods: { + onClick() { + this.isLoading = true; - eventHub.$emit('postAction', this.retryUrl); + eventHub.$emit('postAction', this.retryUrl); + }, }, - }, -}; + }; </script> <template> <button type="button" class="btn hidden-xs hidden-sm" @click="onClick" - :disabled="isLoading"> + :disabled="isLoading" + > <span v-if="isLastDeployment"> - {{s__("Environments|Re-deploy")}} + {{ s__("Environments|Re-deploy") }} </span> <span v-else> - {{s__("Environments|Rollback")}} + {{ s__("Environments|Rollback") }} </span> <loading-icon v-if="isLoading" /> diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue index 85f11d2071b..1eef17bf1fe 100644 --- a/app/assets/javascripts/environments/components/environment_stop.vue +++ b/app/assets/javascripts/environments/components/environment_stop.vue @@ -1,53 +1,53 @@ <script> -/** - * Renders the stop "button" that allows stop an environment. - * Used in environments table. - */ -import eventHub from '../event_hub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; + /** + * Renders the stop "button" that allows stop an environment. + * Used in environments table. + */ + import eventHub from '../event_hub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; -export default { - props: { - stopUrl: { - type: String, - default: '', + export default { + components: { + loadingIcon, }, - }, - directives: { - tooltip, - }, + directives: { + tooltip, + }, - data() { - return { - isLoading: false, - }; - }, + props: { + stopUrl: { + type: String, + default: '', + }, + }, - components: { - loadingIcon, - }, + data() { + return { + isLoading: false, + }; + }, - computed: { - title() { - return 'Stop'; + computed: { + title() { + return 'Stop'; + }, }, - }, - methods: { - onClick() { - // eslint-disable-next-line no-alert - if (confirm('Are you sure you want to stop this environment?')) { - this.isLoading = true; + methods: { + onClick() { + // eslint-disable-next-line no-alert + if (confirm('Are you sure you want to stop this environment?')) { + this.isLoading = true; - $(this.$el).tooltip('destroy'); + $(this.$el).tooltip('destroy'); - eventHub.$emit('postAction', this.stopUrl); - } + eventHub.$emit('postAction', this.stopUrl); + } + }, }, - }, -}; + }; </script> <template> <button @@ -58,10 +58,13 @@ export default { @click="onClick" :disabled="isLoading" :title="title" - :aria-label="title"> + :aria-label="title" + > <i class="fa fa-stop stop-env-icon" - aria-hidden="true" /> + aria-hidden="true" + > + </i> <loading-icon v-if="isLoading" /> </button> </template> diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue index 2037bf618e3..407d5333c0e 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.vue +++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue @@ -1,36 +1,36 @@ <script> -/** - * Renders a terminal button to open a web terminal. - * Used in environments table. - */ -import terminalIconSvg from 'icons/_icon_terminal.svg'; -import tooltip from '../../vue_shared/directives/tooltip'; + /** + * Renders a terminal button to open a web terminal. + * Used in environments table. + */ + import terminalIconSvg from 'icons/_icon_terminal.svg'; + import tooltip from '../../vue_shared/directives/tooltip'; -export default { - props: { - terminalPath: { - type: String, - required: false, - default: '', + export default { + directives: { + tooltip, }, - }, - directives: { - tooltip, - }, + props: { + terminalPath: { + type: String, + required: false, + default: '', + }, + }, - data() { - return { - terminalIconSvg, - }; - }, + data() { + return { + terminalIconSvg, + }; + }, - computed: { - title() { - return 'Terminal'; + computed: { + title() { + return 'Terminal'; + }, }, - }, -}; + }; </script> <template> <a @@ -40,6 +40,7 @@ export default { :title="title" :aria-label="title" :href="terminalPath" - v-html="terminalIconSvg"> + v-html="terminalIconSvg" + > </a> </template> diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index 2592909734f..c0be72f7401 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -7,6 +7,15 @@ import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; export default { + components: { + emptyState, + }, + + mixins: [ + CIPaginationMixin, + environmentsMixin, + ], + props: { endpoint: { type: String, @@ -37,14 +46,6 @@ required: true, }, }, - components: { - emptyState, - }, - - mixins: [ - CIPaginationMixin, - environmentsMixin, - ], created() { eventHub.$on('toggleFolder', this.toggleFolder); @@ -95,15 +96,17 @@ :tabs="tabs" @onChangeTab="onChangeTab" scope="environments" - /> + /> <div v-if="canCreateEnvironment && !isLoading" - class="nav-controls"> + class="nav-controls" + > <a :href="newEnvironmentPath" - class="btn btn-create"> - {{s__("Environments|New environment")}} + class="btn btn-create" + > + {{ s__("Environments|New environment") }} </a> </div> </div> @@ -116,13 +119,13 @@ :can-read-environment="canReadEnvironment" @onChangePage="onChangePage" > - <empty-state + <empty-state slot="emptyState" v-if="!isLoading && state.environments.length === 0" :new-path="newEnvironmentPath" :help-path="helpPagePath" :can-create-environment="canCreateEnvironment" - /> + /> </container> </div> </template> diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index c04da4b81b7..858acf293a1 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -30,63 +30,96 @@ export default { default: false, }, }, - methods: { folderUrl(model) { return `${window.location.pathname}/folders/${model.folderName}`; }, + shouldRenderFolderContent(env) { + return env.isFolder && + env.isOpen && + env.children && + env.children.length > 0; + }, }, }; </script> <template> - <div class="ci-table" role="grid"> - <div class="gl-responsive-table-row table-row-header" role="row"> - <div class="table-section section-10 environments-name" role="columnheader"> - {{s__("Environments|Environment")}} + <div + class="ci-table" + role="grid" + > + <div + class="gl-responsive-table-row table-row-header" + role="row" + > + <div + class="table-section section-10 environments-name" + role="columnheader" + > + {{ s__("Environments|Environment") }} </div> - <div class="table-section section-10 environments-deploy" role="columnheader"> - {{s__("Environments|Deployment")}} + <div + class="table-section section-10 environments-deploy" + role="columnheader" + > + {{ s__("Environments|Deployment") }} </div> - <div class="table-section section-15 environments-build" role="columnheader"> - {{s__("Environments|Job")}} + <div + class="table-section section-15 environments-build" + role="columnheader" + > + {{ s__("Environments|Job") }} </div> - <div class="table-section section-25 environments-commit" role="columnheader"> - {{s__("Environments|Commit")}} + <div + class="table-section section-25 environments-commit" + role="columnheader" + > + {{ s__("Environments|Commit") }} </div> - <div class="table-section section-10 environments-date" role="columnheader"> - {{s__("Environments|Updated")}} + <div + class="table-section section-10 environments-date" + role="columnheader" + > + {{ s__("Environments|Updated") }} </div> </div> <template - v-for="model in environments" - v-bind:model="model"> + v-for="(model, i) in environments" + :model="model"> <div is="environment-item" :model="model" :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" - /> + :key="i" + /> - <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0"> - <div v-if="model.isLoadingFolderContent"> + <template + v-if="shouldRenderFolderContent(model)" + > + <div + v-if="model.isLoadingFolderContent" + :key="i"> <loading-icon size="2" /> </div> <template v-else> <div is="environment-item" - v-for="children in model.children" + v-for="(children, index) in model.children" :model="children" :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" - /> + :key="index" + /> - <div> + <div :key="i"> <div class="text-center prepend-top-10"> <a :href="folderUrl(model)" - class="btn btn-default"> - {{s__("Environments|Show all")}} + class="btn btn-default" + > + {{ s__("Environments|Show all") }} </a> </div> </div> diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index 27418bad01a..5ef5e347387 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -3,6 +3,10 @@ import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; export default { + mixins: [ + environmentsMixin, + CIPaginationMixin, + ], props: { endpoint: { type: String, @@ -25,10 +29,6 @@ required: true, }, }, - mixins: [ - environmentsMixin, - CIPaginationMixin, - ], methods: { successCallback(resp) { this.saveData(resp); @@ -40,17 +40,18 @@ <div :class="cssContainerClass"> <div class="top-area" - v-if="!isLoading"> + v-if="!isLoading" + > <h4 class="js-folder-name environments-folder-name"> - {{s__("Environments|Environments")}} / <b>{{folderName}}</b> + {{ s__("Environments|Environments") }} / <b>{{ folderName }}</b> </h4> <tabs :tabs="tabs" @onChangeTab="onChangeTab" scope="environments" - /> + /> </div> <container diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js index 27e49d4fb96..c99ed63c4af 100644 --- a/app/assets/javascripts/filtered_search/recent_searches_root.js +++ b/app/assets/javascripts/filtered_search/recent_searches_root.js @@ -32,6 +32,9 @@ class RecentSearchesRoot { const state = this.store.state; this.vm = new Vue({ el: this.wrapperElement, + components: { + 'recent-searches-dropdown-content': RecentSearchesDropdownContent, + }, data() { return state; }, template: ` <recent-searches-dropdown-content @@ -40,9 +43,6 @@ class RecentSearchesRoot { :allowed-keys="allowedKeys" /> `, - components: { - 'recent-searches-dropdown-content': RecentSearchesDropdownContent, - }, }); } diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index 241e026b84c..400306759b2 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -42,6 +42,26 @@ export default { return this.store.getPaginationInfo(); }, }, + created() { + this.searchEmptyMessage = this.hideProjects ? + COMMON_STR.GROUP_SEARCH_EMPTY : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY; + + eventHub.$on('fetchPage', this.fetchPage); + eventHub.$on('toggleChildren', this.toggleChildren); + eventHub.$on('leaveGroup', this.leaveGroup); + eventHub.$on('updatePagination', this.updatePagination); + eventHub.$on('updateGroups', this.updateGroups); + }, + mounted() { + this.fetchAllGroups(); + }, + beforeDestroy() { + eventHub.$off('fetchPage', this.fetchPage); + eventHub.$off('toggleChildren', this.toggleChildren); + eventHub.$off('leaveGroup', this.leaveGroup); + eventHub.$off('updatePagination', this.updatePagination); + eventHub.$off('updateGroups', this.updateGroups); + }, methods: { fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) { return this.service.getGroups(parentId, page, filterGroupsBy, sortBy, archived) @@ -152,26 +172,6 @@ export default { } }, }, - created() { - this.searchEmptyMessage = this.hideProjects ? - COMMON_STR.GROUP_SEARCH_EMPTY : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY; - - eventHub.$on('fetchPage', this.fetchPage); - eventHub.$on('toggleChildren', this.toggleChildren); - eventHub.$on('leaveGroup', this.leaveGroup); - eventHub.$on('updatePagination', this.updatePagination); - eventHub.$on('updateGroups', this.updateGroups); - }, - mounted() { - this.fetchAllGroups(); - }, - beforeDestroy() { - eventHub.$off('fetchPage', this.fetchPage); - eventHub.$off('toggleChildren', this.toggleChildren); - eventHub.$off('leaveGroup', this.leaveGroup); - eventHub.$off('updatePagination', this.updatePagination); - eventHub.$off('updateGroups', this.updateGroups); - }, }; </script> diff --git a/app/assets/javascripts/groups/components/group_folder.vue b/app/assets/javascripts/groups/components/group_folder.vue index e60221fa08d..647c9d0046d 100644 --- a/app/assets/javascripts/groups/components/group_folder.vue +++ b/app/assets/javascripts/groups/components/group_folder.vue @@ -20,7 +20,11 @@ export default { return this.parentGroup.childrenCount > MAX_CHILDREN_COUNT; }, moreChildrenStats() { - return n__('One more item', '%d more items', this.parentGroup.childrenCount - this.parentGroup.children.length); + return n__( + 'One more item', + '%d more items', + this.parentGroup.childrenCount - this.parentGroup.children.length, + ); }, }, }; @@ -43,8 +47,9 @@ export default { <i class="fa fa-external-link" aria-hidden="true" - /> - {{moreChildrenStats}} + > + </i> + {{ moreChildrenStats }} </a> </li> </ul> diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 42e79a9e17a..764b130fdb8 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -75,7 +75,7 @@ export default { :id="groupDomId" :class="rowClass" class="group-row" - > + > <div class="group-row-contents" :class="{ 'project-row-contents': !isGroup }"> @@ -88,7 +88,8 @@ export default { :item="group" /> <div - class="folder-toggle-wrap"> + class="folder-toggle-wrap" + > <item-caret :is-group-open="group.isOpen" /> @@ -113,13 +114,14 @@ export default { <identicon v-else size-class="s24" - :entity-id=group.id + :entity-id="group.id" :entity-name="group.name" /> </a> </div> <div - class="title namespace-title"> + class="title namespace-title" + > <a v-tooltip :href="group.relativePath" @@ -135,7 +137,7 @@ export default { v-if="group.permission" class="user-access-role" > - {{group.permission}} + {{ group.permission }} </span> </div> <div diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index 75a2bf34887..adde8c8cdb3 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -1,47 +1,48 @@ <script> -import tablePagination from '~/vue_shared/components/table_pagination.vue'; -import eventHub from '../event_hub'; -import { getParameterByName } from '../../lib/utils/common_utils'; + import tablePagination from '~/vue_shared/components/table_pagination.vue'; + import eventHub from '../event_hub'; + import { getParameterByName } from '../../lib/utils/common_utils'; -export default { - components: { - tablePagination, - }, - props: { - groups: { - type: Array, - required: true, + export default { + components: { + tablePagination, }, - pageInfo: { - type: Object, - required: true, + props: { + groups: { + type: Array, + required: true, + }, + pageInfo: { + type: Object, + required: true, + }, + searchEmpty: { + type: Boolean, + required: true, + }, + searchEmptyMessage: { + type: String, + required: true, + }, }, - searchEmpty: { - type: Boolean, - required: true, + methods: { + change(page) { + const filterGroupsParam = getParameterByName('filter_groups'); + const sortParam = getParameterByName('sort'); + const archivedParam = getParameterByName('archived'); + eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam); + }, }, - searchEmptyMessage: { - type: String, - required: true, - }, - }, - methods: { - change(page) { - const filterGroupsParam = getParameterByName('filter_groups'); - const sortParam = getParameterByName('sort'); - const archivedParam = getParameterByName('archived'); - eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam); - }, - }, -}; + }; </script> <template> <div class="groups-list-tree-container"> <div v-if="searchEmpty" - class="has-no-search-results"> - {{searchEmptyMessage}} + class="has-no-search-results" + > + {{ searchEmptyMessage }} </div> <group-folder v-if="!searchEmpty" @@ -50,7 +51,7 @@ export default { <table-pagination v-if="!searchEmpty" :change="change" - :pageInfo="pageInfo" + :page-info="pageInfo" /> </div> </template> diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue index 0dd0783ce06..1bde6ae5185 100644 --- a/app/assets/javascripts/groups/components/item_actions.vue +++ b/app/assets/javascripts/groups/components/item_actions.vue @@ -1,56 +1,56 @@ <script> -import { s__ } from '~/locale'; -import tooltip from '~/vue_shared/directives/tooltip'; -import icon from '~/vue_shared/components/icon.vue'; -import modal from '~/vue_shared/components/modal.vue'; -import eventHub from '../event_hub'; -import { COMMON_STR } from '../constants'; + import { s__ } from '~/locale'; + import tooltip from '~/vue_shared/directives/tooltip'; + import icon from '~/vue_shared/components/icon.vue'; + import modal from '~/vue_shared/components/modal.vue'; + import eventHub from '../event_hub'; + import { COMMON_STR } from '../constants'; -export default { - components: { - icon, - modal, - }, - directives: { - tooltip, - }, - props: { - parentGroup: { - type: Object, - required: false, - default: () => ({}), + export default { + components: { + icon, + modal, }, - group: { - type: Object, - required: true, + directives: { + tooltip, }, - }, - data() { - return { - modalStatus: false, - }; - }, - computed: { - leaveBtnTitle() { - return COMMON_STR.LEAVE_BTN_TITLE; + props: { + parentGroup: { + type: Object, + required: false, + default: () => ({}), + }, + group: { + type: Object, + required: true, + }, }, - editBtnTitle() { - return COMMON_STR.EDIT_BTN_TITLE; + data() { + return { + modalStatus: false, + }; }, - leaveConfirmationMessage() { - return s__(`GroupsTree|Are you sure you want to leave the "${this.group.fullName}" group?`); + computed: { + leaveBtnTitle() { + return COMMON_STR.LEAVE_BTN_TITLE; + }, + editBtnTitle() { + return COMMON_STR.EDIT_BTN_TITLE; + }, + leaveConfirmationMessage() { + return s__(`GroupsTree|Are you sure you want to leave the "${this.group.fullName}" group?`); + }, }, - }, - methods: { - onLeaveGroup() { - this.modalStatus = true; + methods: { + onLeaveGroup() { + this.modalStatus = true; + }, + leaveGroup() { + this.modalStatus = false; + eventHub.$emit('leaveGroup', this.group, this.parentGroup); + }, }, - leaveGroup() { - this.modalStatus = false; - eventHub.$emit('leaveGroup', this.group, this.parentGroup); - }, - }, -}; + }; </script> <template> diff --git a/app/assets/javascripts/groups/components/item_caret.vue b/app/assets/javascripts/groups/components/item_caret.vue index 9e90fe2b701..2a5bec5e86c 100644 --- a/app/assets/javascripts/groups/components/item_caret.vue +++ b/app/assets/javascripts/groups/components/item_caret.vue @@ -2,6 +2,9 @@ import icon from '~/vue_shared/components/icon.vue'; export default { + components: { + icon, + }, props: { isGroupOpen: { type: Boolean, @@ -9,9 +12,6 @@ export default { default: false, }, }, - components: { - icon, - }, computed: { iconClass() { return this.isGroupOpen ? 'angle-down' : 'angle-right'; diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue index 2e42fb6c9a6..168b4e4af2c 100644 --- a/app/assets/javascripts/groups/components/item_stats.vue +++ b/app/assets/javascripts/groups/components/item_stats.vue @@ -1,39 +1,44 @@ <script> -import icon from '~/vue_shared/components/icon.vue'; -import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import { ITEM_TYPE, VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE, PROJECT_VISIBILITY_TYPE } from '../constants'; -import itemStatsValue from './item_stats_value.vue'; + import icon from '~/vue_shared/components/icon.vue'; + import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + import { + ITEM_TYPE, + VISIBILITY_TYPE_ICON, + GROUP_VISIBILITY_TYPE, + PROJECT_VISIBILITY_TYPE, + } from '../constants'; + import itemStatsValue from './item_stats_value.vue'; -export default { - components: { - icon, - timeAgoTooltip, - itemStatsValue, - }, - props: { - item: { - type: Object, - required: true, + export default { + components: { + icon, + timeAgoTooltip, + itemStatsValue, }, - }, - computed: { - visibilityIcon() { - return VISIBILITY_TYPE_ICON[this.item.visibility]; + props: { + item: { + type: Object, + required: true, + }, }, - visibilityTooltip() { - if (this.item.type === ITEM_TYPE.GROUP) { - return GROUP_VISIBILITY_TYPE[this.item.visibility]; - } - return PROJECT_VISIBILITY_TYPE[this.item.visibility]; + computed: { + visibilityIcon() { + return VISIBILITY_TYPE_ICON[this.item.visibility]; + }, + visibilityTooltip() { + if (this.item.type === ITEM_TYPE.GROUP) { + return GROUP_VISIBILITY_TYPE[this.item.visibility]; + } + return PROJECT_VISIBILITY_TYPE[this.item.visibility]; + }, + isProject() { + return this.item.type === ITEM_TYPE.PROJECT; + }, + isGroup() { + return this.item.type === ITEM_TYPE.GROUP; + }, }, - isProject() { - return this.item.type === ITEM_TYPE.PROJECT; - }, - isGroup() { - return this.item.type === ITEM_TYPE.GROUP; - }, - }, -}; + }; </script> <template> diff --git a/app/assets/javascripts/groups/components/item_stats_value.vue b/app/assets/javascripts/groups/components/item_stats_value.vue index f441cabf6d2..08d0bf6e344 100644 --- a/app/assets/javascripts/groups/components/item_stats_value.vue +++ b/app/assets/javascripts/groups/components/item_stats_value.vue @@ -1,52 +1,52 @@ <script> -import tooltip from '~/vue_shared/directives/tooltip'; -import icon from '~/vue_shared/components/icon.vue'; + import tooltip from '~/vue_shared/directives/tooltip'; + import icon from '~/vue_shared/components/icon.vue'; -export default { - props: { - title: { - type: String, - required: false, - default: '', + export default { + components: { + icon, }, - cssClass: { - type: String, - required: false, - default: '', + directives: { + tooltip, }, - iconName: { - type: String, - required: true, + props: { + title: { + type: String, + required: false, + default: '', + }, + cssClass: { + type: String, + required: false, + default: '', + }, + iconName: { + type: String, + required: true, + }, + tooltipPlacement: { + type: String, + required: false, + default: 'bottom', + }, + /** + * value could either be number or string + * as `memberCount` is always passed as string + * while `subgroupCount` & `projectCount` + * are always number + */ + value: { + type: [Number, String], + required: false, + default: '', + }, }, - tooltipPlacement: { - type: String, - required: false, - default: 'bottom', + computed: { + isValuePresent() { + return this.value !== ''; + }, }, - /** - * value could either be number or string - * as `memberCount` is always passed as string - * while `subgroupCount` & `projectCount` - * are always number - */ - value: { - type: [Number, String], - required: false, - default: '', - }, - }, - directives: { - tooltip, - }, - components: { - icon, - }, - computed: { - isValuePresent() { - return this.value !== ''; - }, - }, -}; + }; </script> <template> @@ -57,12 +57,12 @@ export default { :class="cssClass" :title="title" > - <icon :name="iconName"/> + <icon :name="iconName" /> <span v-if="isValuePresent" class="stat-value" > - {{value}} + {{ value }} </span> </span> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue index 704dff981df..a8459b011df 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue @@ -32,7 +32,6 @@ this.$emit('toggleCollapsed'); }, }, - }; </script> diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 26a70f6e748..89981ab2c65 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,79 +1,82 @@ <script> -import { mapState, mapGetters } from 'vuex'; -import ideSidebar from './ide_side_bar.vue'; -import ideContextbar from './ide_context_bar.vue'; -import repoTabs from './repo_tabs.vue'; -import repoFileButtons from './repo_file_buttons.vue'; -import ideStatusBar from './ide_status_bar.vue'; -import repoPreview from './repo_preview.vue'; -import repoEditor from './repo_editor.vue'; + import { mapState, mapGetters } from 'vuex'; + import ideSidebar from './ide_side_bar.vue'; + import ideContextbar from './ide_context_bar.vue'; + import repoTabs from './repo_tabs.vue'; + import repoFileButtons from './repo_file_buttons.vue'; + import ideStatusBar from './ide_status_bar.vue'; + import repoPreview from './repo_preview.vue'; + import repoEditor from './repo_editor.vue'; -export default { - props: { - emptyStateSvgPath: { - type: String, - required: true, + export default { + components: { + ideSidebar, + ideContextbar, + repoTabs, + repoFileButtons, + ideStatusBar, + repoEditor, + repoPreview, }, - }, - computed: { - ...mapState([ - 'currentBlobView', - 'selectedFile', - ]), - ...mapGetters([ - 'changedFiles', - 'activeFile', - ]), - }, - components: { - ideSidebar, - ideContextbar, - repoTabs, - repoFileButtons, - ideStatusBar, - repoEditor, - repoPreview, - }, - mounted() { - const returnValue = 'Are you sure you want to lose unsaved changes?'; - window.onbeforeunload = (e) => { - if (!this.changedFiles.length) return undefined; + props: { + emptyStateSvgPath: { + type: String, + required: true, + }, + }, + computed: { + ...mapState([ + 'currentBlobView', + 'selectedFile', + ]), + ...mapGetters([ + 'changedFiles', + 'activeFile', + ]), + }, + mounted() { + const returnValue = 'Are you sure you want to lose unsaved changes?'; + window.onbeforeunload = (e) => { + if (!this.changedFiles.length) return undefined; - Object.assign(e, { - returnValue, - }); - return returnValue; - }; - }, -}; + Object.assign(e, { + returnValue, + }); + return returnValue; + }; + }, + }; </script> <template> - <div + <div class="ide-view" > - <ide-sidebar/> + <ide-sidebar /> <div class="multi-file-edit-pane" > <template - v-if="activeFile"> + v-if="activeFile" + > <repo-tabs/> <component class="multi-file-edit-pane-content" :is="currentBlobView" /> - <repo-file-buttons/> + <repo-file-buttons /> <ide-status-bar - :file="selectedFile"/> + :file="selectedFile" + /> </template> <template - v-else> + v-else + > <div class="ide-empty-state"> <div class="row js-empty-state"> <div class="col-xs-12"> <div class="svg-content svg-250"> - <img :src="emptyStateSvgPath"> + <img :src="emptyStateSvgPath" /> </div> </div> <div class="col-xs-12"> @@ -82,7 +85,8 @@ export default { Welcome to the GitLab IDE </h4> <p> - You can select a file in the left sidebar to begin editing and use the right sidebar to commit your changes. + You can select a file in the left sidebar to begin + editing and use the right sidebar to commit your changes. </p> </div> </div> diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue index 78c01272af6..dd947f66969 100644 --- a/app/assets/javascripts/ide/components/ide_context_bar.vue +++ b/app/assets/javascripts/ide/components/ide_context_bar.vue @@ -1,59 +1,59 @@ <script> -import { mapGetters, mapState, mapActions } from 'vuex'; -import repoCommitSection from './repo_commit_section.vue'; -import icon from '../../vue_shared/components/icon.vue'; -import panelResizer from '../../vue_shared/components/panel_resizer.vue'; + import { mapGetters, mapState, mapActions } from 'vuex'; + import repoCommitSection from './repo_commit_section.vue'; + import icon from '../../vue_shared/components/icon.vue'; + import panelResizer from '../../vue_shared/components/panel_resizer.vue'; -export default { - data() { - return { - width: 290, - }; - }, - components: { - repoCommitSection, - icon, - panelResizer, - }, - computed: { - ...mapState([ - 'rightPanelCollapsed', - ]), - ...mapGetters([ - 'changedFiles', - ]), - currentIcon() { - return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right'; + export default { + components: { + repoCommitSection, + icon, + panelResizer, }, - maxSize() { - return window.innerWidth / 2; + data() { + return { + width: 290, + }; }, - panelStyle() { - if (!this.rightPanelCollapsed) { - return { width: `${this.width}px` }; - } - return {}; + computed: { + ...mapState([ + 'rightPanelCollapsed', + ]), + ...mapGetters([ + 'changedFiles', + ]), + currentIcon() { + return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right'; + }, + maxSize() { + return window.innerWidth / 2; + }, + panelStyle() { + if (!this.rightPanelCollapsed) { + return { width: `${this.width}px` }; + } + return {}; + }, }, - }, - methods: { - ...mapActions([ - 'setPanelCollapsedStatus', - 'setResizingStatus', - ]), - toggleCollapsed() { - this.setPanelCollapsedStatus({ - side: 'right', - collapsed: !this.rightPanelCollapsed, - }); + methods: { + ...mapActions([ + 'setPanelCollapsedStatus', + 'setResizingStatus', + ]), + toggleCollapsed() { + this.setPanelCollapsedStatus({ + side: 'right', + collapsed: !this.rightPanelCollapsed, + }); + }, + resizingStarted() { + this.setResizingStatus(true); + }, + resizingEnded() { + this.setResizingStatus(false); + }, }, - resizingStarted() { - this.setResizingStatus(true); - }, - resizingEnded() { - this.setResizingStatus(false); - }, - }, -}; + }; </script> <template> @@ -64,17 +64,17 @@ export default { }" :style="panelStyle" > - <div - class="multi-file-commit-panel-section"> + <div class="multi-file-commit-panel-section"> <header class="multi-file-commit-panel-header" :class="{ - 'is-collapsed': rightPanelCollapsed, - }" - > + 'is-collapsed': rightPanelCollapsed, + }" + > <div class="multi-file-commit-panel-header-title" - v-if="!rightPanelCollapsed"> + v-if="!rightPanelCollapsed" + > <icon name="list-bulleted" :size="18" @@ -92,8 +92,7 @@ export default { /> </button> </header> - <repo-commit-section - class=""/> + <repo-commit-section /> </div> <panel-resizer :size.sync="width" @@ -103,6 +102,7 @@ export default { :max-size="maxSize" @resize-start="resizingStarted" @resize-end="resizingEnded" - side="left"/> + side="left" + /> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue index bd3a521ff43..af2f7341a91 100644 --- a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue +++ b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue @@ -28,20 +28,20 @@ export default { <div class="branch-header-title"> <icon name="branch" - :size="12"> - </icon> + :size="12" + /> {{ branch.name }} </div> <div class="branch-header-btns"> <new-dropdown :project-id="projectId" :branch="branch.name" - path=""/> + path="" + /> </div> </div> <div> - <repo-tree - :treeId="branch.treeId"/> + <repo-tree :tree-id="branch.treeId" /> </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue index 61daba6d176..ed49a0e72a2 100644 --- a/app/assets/javascripts/ide/components/ide_project_tree.vue +++ b/app/assets/javascripts/ide/components/ide_project_tree.vue @@ -19,9 +19,10 @@ export default { <template> <div class="projects-sidebar"> <div class="context-header"> - <a - :title="project.name" - :href="project.web_url"> + <a + :title="project.name" + :href="project.web_url" + > <div class="avatar-container s40 project-avatar"> <project-avatar-image class="avatar-container project-avatar" @@ -29,7 +30,7 @@ export default { :img-src="project.avatar_url" :img-alt="project.name" :img-size="40" - /> + /> </div> <div class="sidebar-context-title"> {{ project.name }} @@ -38,10 +39,11 @@ export default { </div> <div class="multi-file-commit-panel-inner-scroll"> <branches-tree - v-for="(branch, index) in project.branches" + v-for="branch in project.branches" :key="branch.name" :project-id="project.path_with_namespace" - :branch="branch"/> + :branch="branch" + /> </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue index bd89ebe47d9..4651e345d75 100644 --- a/app/assets/javascripts/ide/components/ide_repo_tree.vue +++ b/app/assets/javascripts/ide/components/ide_repo_tree.vue @@ -44,28 +44,31 @@ export default { </script> <template> -<div> - <div class="ide-file-list"> - <table class="table"> - <tbody - v-if="treeId"> - <repo-previous-directory - v-if="hasPreviousDirectory" - /> - <div - class="multi-file-loading-container" - v-if="showLoading" - v-for="n in 3" - :key="n"> - <skeleton-loading-container/> - </div> - <repo-file - v-for="file in fetchedList" - :key="file.key" - :file="file" - /> - </tbody> - </table> + <div> + <div class="ide-file-list"> + <table class="table"> + <tbody + v-if="treeId" + > + <repo-previous-directory + v-if="hasPreviousDirectory" + /> + <template v-if="showLoading"> + <div + class="multi-file-loading-container" + v-for="n in 3" + :key="n" + > + <skeleton-loading-container /> + </div> + </template> + <repo-file + v-for="file in fetchedList" + :key="file.key" + :file="file" + /> + </tbody> + </table> + </div> </div> -</div> </template> diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index c30018e04b0..a68f8ce0169 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -1,85 +1,88 @@ <script> -import { mapState, mapActions } from 'vuex'; -import projectTree from './ide_project_tree.vue'; -import icon from '../../vue_shared/components/icon.vue'; -import panelResizer from '../../vue_shared/components/panel_resizer.vue'; -import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; + import { mapState, mapActions } from 'vuex'; + import projectTree from './ide_project_tree.vue'; + import icon from '../../vue_shared/components/icon.vue'; + import panelResizer from '../../vue_shared/components/panel_resizer.vue'; + import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; -export default { - data() { - return { - width: 290, - }; - }, - components: { - projectTree, - icon, - panelResizer, - skeletonLoadingContainer, - }, - computed: { - ...mapState([ - 'loading', - 'projects', - 'leftPanelCollapsed', - ]), - currentIcon() { - return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left'; + export default { + components: { + projectTree, + icon, + panelResizer, + skeletonLoadingContainer, }, - maxSize() { - return window.innerWidth / 2; + data() { + return { + width: 290, + }; }, - panelStyle() { - if (!this.leftPanelCollapsed) { - return { width: `${this.width}px` }; - } - return {}; + computed: { + ...mapState([ + 'loading', + 'projects', + 'leftPanelCollapsed', + ]), + currentIcon() { + return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left'; + }, + maxSize() { + return window.innerWidth / 2; + }, + panelStyle() { + if (!this.leftPanelCollapsed) { + return { width: `${this.width}px` }; + } + return {}; + }, + showLoading() { + return this.loading; + }, }, - showLoading() { - return this.loading; + methods: { + ...mapActions([ + 'setPanelCollapsedStatus', + 'setResizingStatus', + ]), + toggleCollapsed() { + this.setPanelCollapsedStatus({ + side: 'left', + collapsed: !this.leftPanelCollapsed, + }); + }, + resizingStarted() { + this.setResizingStatus(true); + }, + resizingEnded() { + this.setResizingStatus(false); + }, }, - }, - methods: { - ...mapActions([ - 'setPanelCollapsedStatus', - 'setResizingStatus', - ]), - toggleCollapsed() { - this.setPanelCollapsedStatus({ - side: 'left', - collapsed: !this.leftPanelCollapsed, - }); - }, - resizingStarted() { - this.setResizingStatus(true); - }, - resizingEnded() { - this.setResizingStatus(false); - }, - }, -}; + }; </script> <template> <div - class="multi-file-commit-panel" - :class="{ - 'is-collapsed': leftPanelCollapsed, - }" - :style="panelStyle" - > + class="multi-file-commit-panel" + :class="{ + 'is-collapsed': leftPanelCollapsed, + }" + :style="panelStyle" + > <div class="multi-file-commit-panel-inner"> - <div - class="multi-file-loading-container" - v-if="showLoading" - v-for="n in 3" - :key="n"> - <skeleton-loading-container/> - </div> + <template v-if="showLoading"> + <div + class="multi-file-loading-container" + v-for="n in 3" + :key="n" + > + <skeleton-loading-container /> + </div> + </template> <project-tree - v-for="(project, index) in projects" + v-for="project in projects" :key="project.id" - :project="project"/> + :project="project" + /> </div> <button type="button" @@ -93,7 +96,9 @@ export default { <span v-if="!leftPanelCollapsed" class="collapse-text" - >Collapse sidebar</span> + > + Collapse sidebar + </span> </button> <panel-resizer :size.sync="width" @@ -103,6 +108,7 @@ export default { :max-size="maxSize" @resize-start="resizingStarted" @resize-end="resizingEnded" - side="right"/> + side="right" + /> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index a24abadd936..e48c446c4a4 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -1,70 +1,65 @@ <script> -import { mapState } from 'vuex'; -import icon from '../../vue_shared/components/icon.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; -import timeAgoMixin from '../../vue_shared/mixins/timeago'; + import { mapState } from 'vuex'; + import icon from '../../vue_shared/components/icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; + import timeAgoMixin from '../../vue_shared/mixins/timeago'; -export default { - props: { - file: { - type: Object, - required: true, + export default { + components: { + icon, }, - }, - components: { - icon, - }, - directives: { - tooltip, - }, - mixins: [ - timeAgoMixin, - ], - computed: { - ...mapState([ - 'selectedFile', - ]), - }, -}; + directives: { + tooltip, + }, + mixins: [ + timeAgoMixin, + ], + props: { + file: { + type: Object, + required: true, + }, + }, + computed: { + ...mapState([ + 'selectedFile', + ]), + }, + }; </script> <template> - <div - class="ide-status-bar"> + <div class="ide-status-bar"> <div> <icon name="branch" - :size="12"> - </icon> + :size="12" + /> {{ selectedFile.branchId }} </div> <div> - <div - v-if="selectedFile.lastCommit && selectedFile.lastCommit.id"> + <div v-if="selectedFile.lastCommit && selectedFile.lastCommit.id"> Last commit: <a v-tooltip :title="selectedFile.lastCommit.message" - :href="selectedFile.lastCommit.url"> - {{ timeFormated(selectedFile.lastCommit.updatedAt) }} by + :href="selectedFile.lastCommit.url" + > + {{ timeFormated(selectedFile.lastCommit.updatedAt) }} by {{ selectedFile.lastCommit.author }} </a> - </div> + </div> </div> - <div - class="text-right"> + <div class="text-right"> {{ selectedFile.name }} </div> - <div - class="text-right"> + <div class="text-right"> {{ selectedFile.eol }} </div> - <div - class="text-right"> + <div class="text-right"> {{ file.editorRow }}:{{ file.editorColumn }} </div> - <div - class="text-right"> + <div class="text-right"> {{ selectedFile.fileLanguage }} </div> </div> diff --git a/app/assets/javascripts/ide/components/new_branch_form.vue b/app/assets/javascripts/ide/components/new_branch_form.vue index 2119d373d31..56e31256132 100644 --- a/app/assets/javascripts/ide/components/new_branch_form.vue +++ b/app/assets/javascripts/ide/components/new_branch_form.vue @@ -21,6 +21,13 @@ return this.loading || this.branchName === ''; }, }, + created() { + // Dropdown is outside of Vue instance & is controlled by Bootstrap + this.$dropdown = $('.git-revision-dropdown'); + + // text element is outside Vue app + this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text'); + }, methods: { ...mapActions([ 'createNewBranch', @@ -55,13 +62,6 @@ })); }, }, - created() { - // Dropdown is outside of Vue instance & is controlled by Bootstrap - this.$dropdown = $('.git-revision-dropdown'); - - // text element is outside Vue app - this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text'); - }, }; </script> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index d475813c4f7..ef653357f5f 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -4,6 +4,11 @@ import icon from '../../../vue_shared/components/icon.vue'; export default { + components: { + icon, + newModal, + upload, + }, props: { branch: { type: String, @@ -18,11 +23,6 @@ default: null, }, }, - components: { - icon, - newModal, - upload, - }, data() { return { openModal: false, diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index 0312f56efbd..36cd825c6dd 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -4,6 +4,9 @@ import modal from '../../../vue_shared/components/modal.vue'; export default { + components: { + modal, + }, props: { branchId: { type: String, @@ -27,28 +30,6 @@ entryName: this.path !== '' ? `${this.path}/` : '', }; }, - components: { - modal, - }, - methods: { - ...mapActions([ - 'createTempEntry', - ]), - createEntryInStore() { - this.createTempEntry({ - projectId: this.currentProjectId, - branchId: this.branchId, - parent: this.parent, - name: this.entryName.replace(new RegExp(`^${this.path}/`), ''), - type: this.type, - }); - - this.hideModal(); - }, - hideModal() { - this.$emit('hide'); - }, - }, computed: { ...mapState([ 'currentProjectId', @@ -78,6 +59,25 @@ mounted() { this.$refs.fieldName.focus(); }, + methods: { + ...mapActions([ + 'createTempEntry', + ]), + createEntryInStore() { + this.createTempEntry({ + projectId: this.currentProjectId, + branchId: this.branchId, + parent: this.parent, + name: this.entryName.replace(new RegExp(`^${this.path}/`), ''), + type: this.type, + }); + + this.hideModal(); + }, + hideModal() { + this.$emit('hide'); + }, + }, }; </script> diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue index 2a2f2a241fc..6244737fa43 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue @@ -18,6 +18,12 @@ 'currentProjectId', ]), }, + mounted() { + this.$refs.fileUpload.addEventListener('change', this.openFile); + }, + beforeDestroy() { + this.$refs.fileUpload.removeEventListener('change', this.openFile); + }, methods: { ...mapActions([ 'createTempEntry', @@ -59,12 +65,6 @@ this.$refs.fileUpload.click(); }, }, - mounted() { - this.$refs.fileUpload.addEventListener('change', this.openFile); - }, - beforeDestroy() { - this.$refs.fileUpload.removeEventListener('change', this.openFile); - }, }; </script> diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 979721dcb5a..5279417a72a 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -49,7 +49,9 @@ export default { const createNewBranch = newBranch || this.startNewMR; const payload = { - branch: createNewBranch ? `${this.currentBranchId}-${new Date().getTime().toString()}` : this.currentBranchId, + branch: createNewBranch ? + `${this.currentBranchId}-${new Date().getTime().toString()}` : + this.currentBranchId, commit_message: this.commitMessage, actions: this.changedFiles.map(f => ({ action: f.tempFile ? 'create' : 'update', @@ -103,69 +105,70 @@ export default { </script> <template> -<div class="multi-file-commit-panel-section"> - <modal - v-if="showNewBranchModal" - :primary-button-label="__('Create new branch')" - kind="primary" - :title="__('Branch has changed')" - :text="__('This branch has changed since you started editing. Would you like to create a new branch?')" - @cancel="showNewBranchModal = false" - @submit="makeCommit(true)" - /> - <commit-files-list - title="Staged" - :file-list="changedFiles" - :collapsed="rightPanelCollapsed" - @toggleCollapsed="toggleCollapsed" - /> - <form - class="form-horizontal multi-file-commit-form" - @submit.prevent="tryCommit" - v-if="!rightPanelCollapsed" - > - <div class="multi-file-commit-fieldset"> - <textarea - class="form-control multi-file-commit-message" - name="commit-message" - v-model="commitMessage" - placeholder="Commit message" - > - </textarea> - </div> - <div class="multi-file-commit-fieldset"> - <label - v-tooltip - title="Create a new merge request with these changes" - data-container="body" - data-placement="top" - > - <input - type="checkbox" - v-model="startNewMR" - /> - Merge Request - </label> - <button - type="submit" - :disabled="commitButtonDisabled" - class="btn btn-default btn-sm append-right-10 prepend-left-10" - > - <i - v-if="submitCommitsLoading" - class="js-commit-loading-icon fa fa-spinner fa-spin" - aria-hidden="true" - aria-label="loading" + <div class="multi-file-commit-panel-section"> + <modal + v-if="showNewBranchModal" + :primary-button-label="__('Create new branch')" + kind="primary" + :title="__('Branch has changed')" + :text="__(`This branch has changed since +you started editing. Would you like to create a new branch?`)" + @cancel="showNewBranchModal = false" + @submit="makeCommit(true)" + /> + <commit-files-list + title="Staged" + :file-list="changedFiles" + :collapsed="rightPanelCollapsed" + @toggleCollapsed="toggleCollapsed" + /> + <form + class="form-horizontal multi-file-commit-form" + @submit.prevent="tryCommit" + v-if="!rightPanelCollapsed" + > + <div class="multi-file-commit-fieldset"> + <textarea + class="form-control multi-file-commit-message" + name="commit-message" + v-model="commitMessage" + placeholder="Commit message" > - </i> - Commit - </button> - <div - class="multi-file-commit-message-count" - > - {{ commitMessageCount }} + </textarea> </div> - </div> - </form> -</div> + <div class="multi-file-commit-fieldset"> + <label + v-tooltip + title="Create a new merge request with these changes" + data-container="body" + data-placement="top" + > + <input + type="checkbox" + v-model="startNewMR" + /> + Merge Request + </label> + <button + type="submit" + :disabled="commitButtonDisabled" + class="btn btn-default btn-sm append-right-10 prepend-left-10" + > + <i + v-if="submitCommitsLoading" + class="js-commit-loading-icon fa fa-spinner fa-spin" + aria-hidden="true" + aria-label="loading" + > + </i> + Commit + </button> + <div + class="multi-file-commit-message-count" + > + {{ commitMessageCount }} + </div> + </div> + </form> + </div> </template> diff --git a/app/assets/javascripts/ide/components/repo_edit_button.vue b/app/assets/javascripts/ide/components/repo_edit_button.vue index 42d5d709209..c43e9163340 100644 --- a/app/assets/javascripts/ide/components/repo_edit_button.vue +++ b/app/assets/javascripts/ide/components/repo_edit_button.vue @@ -40,7 +40,7 @@ export default { aria-hidden="true"> </i> <span> - {{buttonLabel}} + {{ buttonLabel }} </span> </button> <modal diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 343fd0a5300..83b82ae44c9 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -6,6 +6,38 @@ import monacoLoader from '../monaco_loader'; import Editor from '../lib/editor'; export default { + computed: { + ...mapGetters([ + 'activeFile', + 'activeFileExtension', + ]), + ...mapState([ + 'leftPanelCollapsed', + 'rightPanelCollapsed', + 'panelResizing', + ]), + shouldHideEditor() { + return this.activeFile.binary && !this.activeFile.raw; + }, + }, + watch: { + activeFile(oldVal, newVal) { + if (newVal && !newVal.active) { + this.initMonaco(); + } + }, + leftPanelCollapsed() { + this.editor.updateDimensions(); + }, + rightPanelCollapsed() { + this.editor.updateDimensions(); + }, + panelResizing(isResizing) { + if (isResizing === false) { + this.editor.updateDimensions(); + } + }, + }, beforeDestroy() { this.editor.dispose(); }, @@ -78,38 +110,6 @@ export default { }); }, }, - watch: { - activeFile(oldVal, newVal) { - if (newVal && !newVal.active) { - this.initMonaco(); - } - }, - leftPanelCollapsed() { - this.editor.updateDimensions(); - }, - rightPanelCollapsed() { - this.editor.updateDimensions(); - }, - panelResizing(isResizing) { - if (isResizing === false) { - this.editor.updateDimensions(); - } - }, - }, - computed: { - ...mapGetters([ - 'activeFile', - 'activeFileExtension', - ]), - ...mapState([ - 'leftPanelCollapsed', - 'rightPanelCollapsed', - 'panelResizing', - ]), - shouldHideEditor() { - return this.activeFile.binary && !this.activeFile.raw; - }, - }, }; </script> diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index c8b0441d81c..f7f4db89bdf 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -6,14 +6,14 @@ import fileIcon from '../../vue_shared/components/file_icon.vue'; export default { - mixins: [ - timeAgoMixin, - ], components: { skeletonLoadingContainer, newDropdown, fileIcon, }, + mixins: [ + timeAgoMixin, + ], props: { file: { type: Object, @@ -60,6 +60,11 @@ }; }, }, + updated() { + if (this.file.type === 'blob' && this.file.active) { + this.$el.scrollIntoView(); + } + }, methods: { clickFile(row) { // Manual Action if a tree is selected/opened @@ -72,11 +77,6 @@ this.$router.push(`/project${row.url}`); }, }, - updated() { - if (this.file.type === 'blob' && this.file.active) { - this.$el.scrollIntoView(); - } - }, }; </script> @@ -99,8 +99,7 @@ :opened="file.opened" :style="levelIndentation" :size="16" - > - </file-icon> + /> {{ file.name }} </a> <new-dropdown @@ -108,7 +107,8 @@ :project-id="file.projectId" :branch="file.branchId" :path="file.path" - :parent="file"/> + :parent="file" + /> <i class="fa" v-if="changedClass" diff --git a/app/assets/javascripts/ide/components/repo_file_buttons.vue b/app/assets/javascripts/ide/components/repo_file_buttons.vue index 34f0d51819a..aabc0d8eada 100644 --- a/app/assets/javascripts/ide/components/repo_file_buttons.vue +++ b/app/assets/javascripts/ide/components/repo_file_buttons.vue @@ -35,20 +35,24 @@ export default { <div class="btn-group" role="group" - aria-label="File actions"> + aria-label="File actions" + > <a :href="activeFile.blamePath" - class="btn btn-default btn-sm blame"> + class="btn btn-default btn-sm blame" + > Blame </a> <a :href="activeFile.commitsPath" - class="btn btn-default btn-sm history"> + class="btn btn-default btn-sm history" + > History </a> <a :href="activeFile.permalink" - class="btn btn-default btn-sm permalink"> + class="btn btn-default btn-sm permalink" + > Permalink </a> </div> diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue index 7eb840c7608..3aeb6f0b28f 100644 --- a/app/assets/javascripts/ide/components/repo_loading_file.vue +++ b/app/assets/javascripts/ide/components/repo_loading_file.vue @@ -25,15 +25,13 @@ /> </td> <template v-if="!leftPanelCollapsed"> - <td - class="hidden-sm hidden-xs"> + <td class="hidden-sm hidden-xs"> <skeleton-loading-container :small="true" /> </td> - <td - class="hidden-xs"> + <td class="hidden-xs"> <skeleton-loading-container class="animation-container-right" :small="true" diff --git a/app/assets/javascripts/ide/components/repo_preview.vue b/app/assets/javascripts/ide/components/repo_preview.vue index 3d1e0297bd5..e47270a9855 100644 --- a/app/assets/javascripts/ide/components/repo_preview.vue +++ b/app/assets/javascripts/ide/components/repo_preview.vue @@ -1,65 +1,71 @@ <script> -import { mapGetters } from 'vuex'; -import LineHighlighter from '../../line_highlighter'; -import syntaxHighlight from '../../syntax_highlight'; + import { mapGetters } from 'vuex'; + import LineHighlighter from '../../line_highlighter'; + import syntaxHighlight from '../../syntax_highlight'; -export default { - computed: { - ...mapGetters([ - 'activeFile', - ]), - renderErrorTooLarge() { - return this.activeFile.renderError === 'too_large'; + export default { + computed: { + ...mapGetters([ + 'activeFile', + ]), + renderErrorTooLarge() { + return this.activeFile.renderError === 'too_large'; + }, }, - }, - methods: { - highlightFile() { - syntaxHighlight($(this.$el).find('.file-content')); - }, - }, - mounted() { - this.highlightFile(); - this.lineHighlighter = new LineHighlighter({ - fileHolderSelector: '.blob-viewer-container', - scrollFileHolder: true, - }); - }, - updated() { - this.$nextTick(() => { + mounted() { this.highlightFile(); - }); - }, -}; + this.lineHighlighter = new LineHighlighter({ + fileHolderSelector: '.blob-viewer-container', + scrollFileHolder: true, + }); + }, + updated() { + this.$nextTick(() => { + this.highlightFile(); + }); + }, + methods: { + highlightFile() { + syntaxHighlight($(this.$el).find('.file-content')); + }, + }, + }; </script> <template> -<div> - <div - v-if="!activeFile.renderError" - v-html="activeFile.html" - class="multi-file-preview-holder" - > - </div> - <div - v-else-if="activeFile.tempFile" - class="vertical-center render-error"> - <p class="text-center"> - The source could not be displayed for this temporary file. - </p> - </div> - <div - v-else-if="renderErrorTooLarge" - class="vertical-center render-error"> - <p class="text-center"> - The source could not be displayed because it is too large. You can <a :href="activeFile.rawPath" download>download</a> it instead. - </p> - </div> - <div - v-else - class="vertical-center render-error"> - <p class="text-center"> - The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.rawPath" download>download</a> it instead. - </p> + <div> + <div + v-if="!activeFile.renderError" + v-html="activeFile.html" + class="multi-file-preview-holder" + > + </div> + <div + v-else-if="activeFile.tempFile" + class="vertical-center render-error"> + <p class="text-center"> + The source could not be displayed for this temporary file. + </p> + </div> + <div + v-else-if="renderErrorTooLarge" + class="vertical-center render-error"> + <p class="text-center"> + The source could not be displayed because it is too large. + You can <a + :href="activeFile.rawPath" + download>download</a> it instead. + </p> + </div> + <div + v-else + class="vertical-center render-error"> + <p class="text-center"> + The source could not be displayed because a rendering error occurred. + You can <a + :href="activeFile.rawPath" + download>download</a> it instead. + </p> + </div> </div> -</div> </template> diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index e7684884b2c..5ed7bddf6ae 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -1,48 +1,46 @@ <script> -import { mapActions } from 'vuex'; -import fileIcon from '../../vue_shared/components/file_icon.vue'; + import { mapActions } from 'vuex'; + import fileIcon from '../../vue_shared/components/file_icon.vue'; -export default { - props: { - tab: { - type: Object, - required: true, + export default { + components: { + fileIcon, }, - }, - components: { - fileIcon, - }, - computed: { - closeLabel() { - if (this.tab.changed || this.tab.tempFile) { - return `${this.tab.name} changed`; - } - return `Close ${this.tab.name}`; + props: { + tab: { + type: Object, + required: true, + }, }, - changedClass() { - const tabChangedObj = { - 'fa-times close-icon': !this.tab.changed && !this.tab.tempFile, - 'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile, - }; - return tabChangedObj; + computed: { + closeLabel() { + if (this.tab.changed || this.tab.tempFile) { + return `${this.tab.name} changed`; + } + return `Close ${this.tab.name}`; + }, + changedClass() { + const tabChangedObj = { + 'fa-times close-icon': !this.tab.changed && !this.tab.tempFile, + 'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile, + }; + return tabChangedObj; + }, }, - }, - methods: { - ...mapActions([ - 'closeFile', - ]), - clickFile(tab) { - this.$router.push(`/project${tab.url}`); + methods: { + ...mapActions([ + 'closeFile', + ]), + clickFile(tab) { + this.$router.push(`/project${tab.url}`); + }, }, - }, -}; + }; </script> <template> - <li - @click="clickFile(tab)" - > + <li @click="clickFile(tab)"> <button type="button" class="multi-file-tab-close" @@ -69,8 +67,7 @@ export default { <file-icon :file-name="tab.name" :size="16" - > - </file-icon> + /> {{ tab.name }} </div> </li> diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index fc10a43d1bf..f85d66e9b1d 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -1,308 +1,306 @@ <script> -import Visibility from 'visibilityjs'; -import { visitUrl } from '../../lib/utils/url_utility'; -import Poll from '../../lib/utils/poll'; -import eventHub from '../event_hub'; -import Service from '../services/index'; -import Store from '../stores'; -import titleComponent from './title.vue'; -import descriptionComponent from './description.vue'; -import editedComponent from './edited.vue'; -import formComponent from './form.vue'; -import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; + import Visibility from 'visibilityjs'; + import { visitUrl } from '../../lib/utils/url_utility'; + import Poll from '../../lib/utils/poll'; + import eventHub from '../event_hub'; + import Service from '../services/index'; + import Store from '../stores'; + import titleComponent from './title.vue'; + import descriptionComponent from './description.vue'; + import editedComponent from './edited.vue'; + import formComponent from './form.vue'; + import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; -export default { - props: { - endpoint: { - required: true, - type: String, + export default { + components: { + descriptionComponent, + titleComponent, + editedComponent, + formComponent, }, - updateEndpoint: { - required: true, - type: String, - }, - canUpdate: { - required: true, - type: Boolean, - }, - canDestroy: { - required: true, - type: Boolean, - }, - showInlineEditButton: { - type: Boolean, - required: false, - default: true, - }, - showDeleteButton: { - type: Boolean, - required: false, - default: true, - }, - enableAutocomplete: { - type: Boolean, - required: false, - default: true, - }, - issuableRef: { - type: String, - required: true, - }, - initialTitleHtml: { - type: String, - required: true, - }, - initialTitleText: { - type: String, - required: true, - }, - initialDescriptionHtml: { - type: String, - required: false, - default: '', - }, - initialDescriptionText: { - type: String, - required: false, - default: '', - }, - initialTaskStatus: { - type: String, - required: false, - default: '', - }, - updatedAt: { - type: String, - required: false, - default: '', - }, - updatedByName: { - type: String, - required: false, - default: '', - }, - updatedByPath: { - type: String, - required: false, - default: '', - }, - issuableTemplates: { - type: Array, - required: false, - default: () => [], - }, - markdownPreviewPath: { - type: String, - required: true, - }, - markdownDocsPath: { - type: String, - required: true, - }, - projectPath: { - type: String, - required: true, - }, - projectNamespace: { - type: String, - required: true, - }, - issuableType: { - type: String, - required: false, - default: 'issue', - }, - canAttachFile: { - type: Boolean, - required: false, - default: true, + mixins: [ + recaptchaModalImplementor, + ], + props: { + endpoint: { + required: true, + type: String, + }, + updateEndpoint: { + required: true, + type: String, + }, + canUpdate: { + required: true, + type: Boolean, + }, + canDestroy: { + required: true, + type: Boolean, + }, + showInlineEditButton: { + type: Boolean, + required: false, + default: true, + }, + showDeleteButton: { + type: Boolean, + required: false, + default: true, + }, + enableAutocomplete: { + type: Boolean, + required: false, + default: true, + }, + issuableRef: { + type: String, + required: true, + }, + initialTitleHtml: { + type: String, + required: true, + }, + initialTitleText: { + type: String, + required: true, + }, + initialDescriptionHtml: { + type: String, + required: false, + default: '', + }, + initialDescriptionText: { + type: String, + required: false, + default: '', + }, + initialTaskStatus: { + type: String, + required: false, + default: '', + }, + updatedAt: { + type: String, + required: false, + default: '', + }, + updatedByName: { + type: String, + required: false, + default: '', + }, + updatedByPath: { + type: String, + required: false, + default: '', + }, + issuableTemplates: { + type: Array, + required: false, + default: () => [], + }, + markdownPreviewPath: { + type: String, + required: true, + }, + markdownDocsPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + projectNamespace: { + type: String, + required: true, + }, + issuableType: { + type: String, + required: false, + default: 'issue', + }, + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, }, - }, - data() { - const store = new Store({ - titleHtml: this.initialTitleHtml, - titleText: this.initialTitleText, - descriptionHtml: this.initialDescriptionHtml, - descriptionText: this.initialDescriptionText, - updatedAt: this.updatedAt, - updatedByName: this.updatedByName, - updatedByPath: this.updatedByPath, - taskStatus: this.initialTaskStatus, - }); + data() { + const store = new Store({ + titleHtml: this.initialTitleHtml, + titleText: this.initialTitleText, + descriptionHtml: this.initialDescriptionHtml, + descriptionText: this.initialDescriptionText, + updatedAt: this.updatedAt, + updatedByName: this.updatedByName, + updatedByPath: this.updatedByPath, + taskStatus: this.initialTaskStatus, + }); - return { - store, - state: store.state, - showForm: false, - }; - }, - computed: { - formState() { - return this.store.formState; + return { + store, + state: store.state, + showForm: false, + }; }, - hasUpdated() { - return !!this.state.updatedAt; + computed: { + formState() { + return this.store.formState; + }, + hasUpdated() { + return !!this.state.updatedAt; + }, }, - }, - components: { - descriptionComponent, - titleComponent, - editedComponent, - formComponent, - }, - - mixins: [ - recaptchaModalImplementor, - ], + created() { + this.service = new Service(this.endpoint); + this.poll = new Poll({ + resource: this.service, + method: 'getData', + successCallback: res => this.store.updateState(res.data), + errorCallback(err) { + throw new Error(err); + }, + }); - methods: { - openForm() { - if (!this.showForm) { - this.showForm = true; - this.store.setFormState({ - title: this.state.titleText, - description: this.state.descriptionText, - lockedWarningVisible: false, - updateLoading: false, - }); + if (!Visibility.hidden()) { + this.poll.makeRequest(); } - }, - closeForm() { - this.showForm = false; - }, - - updateIssuable() { - return this.service.updateIssuable(this.store.formState) - .then(res => res.data) - .then(data => this.checkForSpam(data)) - .then((data) => { - if (location.pathname !== data.web_url) { - visitUrl(data.web_url); - } - - return this.service.getData(); - }) - .then(res => res.data) - .then((data) => { - this.store.updateState(data); - eventHub.$emit('close.form'); - }) - .catch((error) => { - if (error && error.name === 'SpamError') { - this.openRecaptcha(); - } else { - eventHub.$emit('close.form'); - window.Flash(`Error updating ${this.issuableType}`); - } - }); - }, - closeRecaptchaModal() { - this.store.setFormState({ - updateLoading: false, + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } }); - this.closeRecaptcha(); + eventHub.$on('delete.issuable', this.deleteIssuable); + eventHub.$on('update.issuable', this.updateIssuable); + eventHub.$on('close.form', this.closeForm); + eventHub.$on('open.form', this.openForm); + }, + beforeDestroy() { + eventHub.$off('delete.issuable', this.deleteIssuable); + eventHub.$off('update.issuable', this.updateIssuable); + eventHub.$off('close.form', this.closeForm); + eventHub.$off('open.form', this.openForm); }, + methods: { + openForm() { + if (!this.showForm) { + this.showForm = true; + this.store.setFormState({ + title: this.state.titleText, + description: this.state.descriptionText, + lockedWarningVisible: false, + updateLoading: false, + }); + } + }, + closeForm() { + this.showForm = false; + }, - deleteIssuable() { - this.service.deleteIssuable() - .then(res => res.data) - .then((data) => { - // Stop the poll so we don't get 404's with the issuable not existing - this.poll.stop(); + updateIssuable() { + return this.service.updateIssuable(this.store.formState) + .then(res => res.data) + .then(data => this.checkForSpam(data)) + .then((data) => { + if (location.pathname !== data.web_url) { + visitUrl(data.web_url); + } - visitUrl(data.web_url); - }) - .catch(() => { - eventHub.$emit('close.form'); - window.Flash(`Error deleting ${this.issuableType}`); - }); - }, - }, - created() { - this.service = new Service(this.endpoint); - this.poll = new Poll({ - resource: this.service, - method: 'getData', - successCallback: res => this.store.updateState(res.data), - errorCallback(err) { - throw new Error(err); + return this.service.getData(); + }) + .then(res => res.data) + .then((data) => { + this.store.updateState(data); + eventHub.$emit('close.form'); + }) + .catch((error) => { + if (error && error.name === 'SpamError') { + this.openRecaptcha(); + } else { + eventHub.$emit('close.form'); + window.Flash(`Error updating ${this.issuableType}`); + } + }); }, - }); - if (!Visibility.hidden()) { - this.poll.makeRequest(); - } + closeRecaptchaModal() { + this.store.setFormState({ + updateLoading: false, + }); + + this.closeRecaptcha(); + }, - Visibility.change(() => { - if (!Visibility.hidden()) { - this.poll.restart(); - } else { - this.poll.stop(); - } - }); + deleteIssuable() { + this.service.deleteIssuable() + .then(res => res.data) + .then((data) => { + // Stop the poll so we don't get 404's with the issuable not existing + this.poll.stop(); - eventHub.$on('delete.issuable', this.deleteIssuable); - eventHub.$on('update.issuable', this.updateIssuable); - eventHub.$on('close.form', this.closeForm); - eventHub.$on('open.form', this.openForm); - }, - beforeDestroy() { - eventHub.$off('delete.issuable', this.deleteIssuable); - eventHub.$off('update.issuable', this.updateIssuable); - eventHub.$off('close.form', this.closeForm); - eventHub.$off('open.form', this.openForm); - }, -}; + visitUrl(data.web_url); + }) + .catch(() => { + eventHub.$emit('close.form'); + window.Flash(`Error deleting ${this.issuableType}`); + }); + }, + }, + }; </script> <template> -<div> - <div v-if="canUpdate && showForm"> - <form-component - :form-state="formState" - :can-destroy="canDestroy" - :issuable-templates="issuableTemplates" - :markdown-docs-path="markdownDocsPath" - :markdown-preview-path="markdownPreviewPath" - :project-path="projectPath" - :project-namespace="projectNamespace" - :show-delete-button="showDeleteButton" - :can-attach-file="canAttachFile" - :enable-autocomplete="enableAutocomplete" - /> + <div> + <div v-if="canUpdate && showForm"> + <form-component + :form-state="formState" + :can-destroy="canDestroy" + :issuable-templates="issuableTemplates" + :markdown-docs-path="markdownDocsPath" + :markdown-preview-path="markdownPreviewPath" + :project-path="projectPath" + :project-namespace="projectNamespace" + :show-delete-button="showDeleteButton" + :can-attach-file="canAttachFile" + :enable-autocomplete="enableAutocomplete" + /> - <recaptcha-modal - v-show="showRecaptcha" - :html="recaptchaHTML" - @close="closeRecaptchaModal" - /> - </div> - <div v-else> - <title-component - :issuable-ref="issuableRef" - :can-update="canUpdate" - :title-html="state.titleHtml" - :title-text="state.titleText" - :show-inline-edit-button="showInlineEditButton" - /> - <description-component - v-if="state.descriptionHtml" - :can-update="canUpdate" - :description-html="state.descriptionHtml" - :description-text="state.descriptionText" - :updated-at="state.updatedAt" - :task-status="state.taskStatus" - :issuable-type="issuableType" - :update-url="updateEndpoint" - /> - <edited-component - v-if="hasUpdated" - :updated-at="state.updatedAt" - :updated-by-name="state.updatedByName" - :updated-by-path="state.updatedByPath" - /> + <recaptcha-modal + v-show="showRecaptcha" + :html="recaptchaHTML" + @close="closeRecaptchaModal" + /> + </div> + <div v-else> + <title-component + :issuable-ref="issuableRef" + :can-update="canUpdate" + :title-html="state.titleHtml" + :title-text="state.titleText" + :show-inline-edit-button="showInlineEditButton" + /> + <description-component + v-if="state.descriptionHtml" + :can-update="canUpdate" + :description-html="state.descriptionHtml" + :description-text="state.descriptionText" + :updated-at="state.updatedAt" + :task-status="state.taskStatus" + :issuable-type="issuableType" + :update-url="updateEndpoint" + /> + <edited-component + v-if="hasUpdated" + :updated-at="state.updatedAt" + :updated-by-name="state.updatedByName" + :updated-by-path="state.updatedByPath" + /> + </div> </div> -</div> </template> diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index c3f2bf130bb..9afa9dea126 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -56,7 +56,10 @@ this.updateTaskStatusText(); }, }, - + mounted() { + this.renderGFM(); + this.updateTaskStatusText(); + }, methods: { renderGFM() { $(this.$refs['gfm-content']).renderGFM(); @@ -88,17 +91,17 @@ if (taskRegexMatches) { $tasks.text(this.taskStatus); - $tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`); + $tasksShort.text( + `${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? + 's' : + ''}`, + ); } else { $tasks.text(''); $tasksShort.text(''); } }, }, - mounted() { - this.renderGFM(); - this.updateTaskStatusText(); - }, }; </script> @@ -108,7 +111,8 @@ class="description" :class="{ 'js-task-list-container': canUpdate - }"> + }" + > <div class="wiki" :class="{ diff --git a/app/assets/javascripts/issue_show/components/edited.vue b/app/assets/javascripts/issue_show/components/edited.vue index 992b7064c13..01097b5b35e 100644 --- a/app/assets/javascripts/issue_show/components/edited.vue +++ b/app/assets/javascripts/issue_show/components/edited.vue @@ -1,33 +1,33 @@ <script> -import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; + import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; -export default { - props: { - updatedAt: { - type: String, - required: false, - default: '', + export default { + components: { + timeAgoTooltip, }, - updatedByName: { - type: String, - required: false, - default: '', + props: { + updatedAt: { + type: String, + required: false, + default: '', + }, + updatedByName: { + type: String, + required: false, + default: '', + }, + updatedByPath: { + type: String, + required: false, + default: '', + }, }, - updatedByPath: { - type: String, - required: false, - default: '', + computed: { + hasUpdatedBy() { + return this.updatedByName && this.updatedByPath; + }, }, - }, - components: { - timeAgoTooltip, - }, - computed: { - hasUpdatedBy() { - return this.updatedByName && this.updatedByPath; - }, - }, -}; + }; </script> <template> @@ -48,7 +48,7 @@ export default { class="author_link" :href="updatedByPath" > - <span>{{updatedByName}}</span> + <span>{{ updatedByName }}</span> </a> </span> </small> diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index 4e577546551..d9fa2764d65 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -3,6 +3,9 @@ import markdownField from '../../../vue_shared/components/markdown/field.vue'; export default { + components: { + markdownField, + }, mixins: [updateMixin], props: { formState: { @@ -28,9 +31,6 @@ default: true, }, }, - components: { - markdownField, - }, mounted() { this.$refs.textarea.focus(); }, diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue index 0fa19022336..779705e19ac 100644 --- a/app/assets/javascripts/issue_show/components/form.vue +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -6,6 +6,13 @@ import descriptionTemplate from './fields/description_template.vue'; export default { + components: { + lockedWarning, + titleField, + descriptionField, + descriptionTemplate, + editActions, + }, props: { canDestroy: { type: Boolean, @@ -52,13 +59,6 @@ default: true, }, }, - components: { - lockedWarning, - titleField, - descriptionField, - descriptionTemplate, - editActions, - }, computed: { hasIssuableTemplates() { return this.issuableTemplates.length; @@ -78,16 +78,19 @@ :form-state="formState" :issuable-templates="issuableTemplates" :project-path="projectPath" - :project-namespace="projectNamespace" /> + :project-namespace="projectNamespace" + /> </div> <div :class="{ 'col-sm-8 col-lg-9': hasIssuableTemplates, 'col-xs-12': !hasIssuableTemplates, - }"> + }" + > <title-field :form-state="formState" - :issuable-templates="issuableTemplates" /> + :issuable-templates="issuableTemplates" + /> </div> </div> <description-field @@ -100,6 +103,7 @@ <edit-actions :form-state="formState" :can-destroy="canDestroy" - :show-delete-button="showDeleteButton" /> + :show-delete-button="showDeleteButton" + /> </form> </template> diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue index b7e6eadd440..aec890a2ff6 100644 --- a/app/assets/javascripts/issue_show/components/title.vue +++ b/app/assets/javascripts/issue_show/components/title.vue @@ -5,14 +5,10 @@ import { spriteIcon } from '../../lib/utils/common_utils'; export default { - mixins: [animateMixin], - data() { - return { - preAnimation: false, - pulseAnimation: false, - titleEl: document.querySelector('title'), - }; + directives: { + tooltip, }, + mixins: [animateMixin], props: { issuableRef: { type: String, @@ -37,8 +33,17 @@ default: false, }, }, - directives: { - tooltip, + data() { + return { + preAnimation: false, + pulseAnimation: false, + titleEl: document.querySelector('title'), + }; + }, + computed: { + pencilIcon() { + return spriteIcon('pencil', 'link-highlight'); + }, }, watch: { titleHtml() { @@ -46,11 +51,6 @@ this.animateChange(); }, }, - computed: { - pencilIcon() { - return spriteIcon('pencil', 'link-highlight'); - }, - }, methods: { setPageTitle() { const currentPageTitleScope = this.titleEl.innerText.split('·'); @@ -85,7 +85,7 @@ data-placement="bottom" data-container="body" @click="edit" - > + > </button> </div> </template> diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue index c660828b30e..9e3f659db5f 100644 --- a/app/assets/javascripts/jobs/components/header.vue +++ b/app/assets/javascripts/jobs/components/header.vue @@ -3,7 +3,11 @@ import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { - name: 'jobHeaderSection', + name: 'JobHeaderSection', + components: { + ciHeader, + loadingIcon, + }, props: { job: { type: Object, @@ -14,10 +18,6 @@ required: true, }, }, - components: { - ciHeader, - loadingIcon, - }, data() { return { actions: this.getActions(), @@ -34,6 +34,11 @@ return this.job.started; }, }, + watch: { + job() { + this.actions = this.getActions(); + }, + }, methods: { getActions() { const actions = []; @@ -49,11 +54,6 @@ return actions; }, }, - watch: { - job() { - this.actions = this.getActions(); - }, - }, }; </script> <template> @@ -72,6 +72,6 @@ <loading-icon v-if="isLoading" size="2" - /> + /> </div> </template> diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue index ab2bcd728a8..a6819aaeb12 100644 --- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue +++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue @@ -23,9 +23,10 @@ <p class="build-detail-row"> <span v-if="hasTitle" - class="build-light-text"> - {{title}}: + class="build-light-text" + > + {{ title }}: </span> - {{value}} + {{ value }} </p> </template> diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue index d0145fed396..56814a52525 100644 --- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue +++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue @@ -6,6 +6,13 @@ export default { name: 'SidebarDetailsBlock', + components: { + detailRow, + loadingIcon, + }, + mixins: [ + timeagoMixin, + ], props: { job: { type: Object, @@ -16,13 +23,6 @@ required: true, }, }, - mixins: [ - timeagoMixin, - ], - components: { - detailRow, - loadingIcon, - }, computed: { shouldRenderContent() { return !this.isLoading && Object.keys(this.job).length > 0; @@ -58,11 +58,13 @@ <template v-if="shouldRenderContent"> <div class="block retry-link" - v-if="job.retry_path || job.new_issue_path"> + v-if="job.retry_path || job.new_issue_path" + > <a v-if="job.new_issue_path" class="js-new-issue btn btn-new btn-inverted" - :href="job.new_issue_path"> + :href="job.new_issue_path" + > New issue </a> <a @@ -70,20 +72,21 @@ class="js-retry-job btn btn-inverted-secondary" :href="job.retry_path" data-method="post" - rel="nofollow"> + rel="nofollow" + > Retry </a> </div> <div :class="{block : renderBlock }"> <p class="build-detail-row js-job-mr" - v-if="job.merge_request"> - <span - class="build-light-text"> + v-if="job.merge_request" + > + <span class="build-light-text"> Merge Request: </span> <a :href="job.merge_request.path"> - !{{job.merge_request.iid}} + !{{ job.merge_request.iid }} </a> </p> @@ -92,49 +95,49 @@ v-if="job.duration" title="Duration" :value="duration" - /> + /> <detail-row class="js-job-finished" v-if="job.finished_at" title="Finished" :value="timeFormated(job.finished_at)" - /> + /> <detail-row class="js-job-erased" v-if="job.erased_at" title="Erased" :value="timeFormated(job.erased_at)" - /> + /> <detail-row class="js-job-queued" v-if="job.queued" title="Queued" :value="queued" - /> + /> <detail-row class="js-job-runner" v-if="job.runner" title="Runner" :value="runnerId" - /> + /> <detail-row class="js-job-coverage" v-if="job.coverage" title="Coverage" :value="coverage" - /> + /> <p class="build-detail-row js-job-tags" - v-if="job.tags.length"> - <span - class="build-light-text"> + v-if="job.tags.length" + > + <span class="build-light-text"> Tags: </span> <span - v-for="tag in job.tags" - key="tag" + v-for="(tag, i) in job.tags" + :key="i" class="label label-primary"> - {{tag}} + {{ tag }} </span> </p> @@ -146,7 +149,8 @@ class="js-cancel-job btn btn-sm btn-default" :href="job.cancel_path" data-method="post" - rel="nofollow"> + rel="nofollow" + > Cancel </a> </div> @@ -156,6 +160,6 @@ class="prepend-top-10" v-if="isLoading" size="2" - /> + /> </div> </template> diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js index baaf5641200..db53b04de0e 100644 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -13,14 +13,14 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line no-new new Vue({ el: '#js-build-header-vue', + components: { + jobHeader, + }, data() { return { mediator, }; }, - components: { - jobHeader, - }, mounted() { this.mediator.initBuildClass(); }, @@ -38,14 +38,14 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line new Vue({ el: '#js-details-block-vue', + components: { + detailsBlock, + }, data() { return { mediator, }; }, - components: { - detailsBlock, - }, render(createElement) { return createElement('details-block', { props: { diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 59bfa482bb0..ce6f91439b4 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -46,7 +46,6 @@ import LazyLoader from './lazy_loader'; import './line_highlighter'; import initLogoAnimation from './logo'; import './milestone_select'; -import './preview_markdown'; import './projects_dropdown'; import './render_gfm'; import initBreadcrumbs from './breadcrumb'; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index 94561d6b7c3..792b7523889 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js @@ -25,12 +25,12 @@ $(() => { gl.MergeConflictsResolverApp = new Vue({ el: '#conflicts', - data: mergeConflictsStore.state, components: { 'diff-file-editor': gl.mergeConflicts.diffFileEditor, 'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines, 'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines }, + data: mergeConflictsStore.state, computed: { conflictsCountText() { return mergeConflictsStore.getConflictsCountText(); }, readyToCommit() { return mergeConflictsStore.isReadyToCommit(); }, diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 8da723ced03..025e38ea99a 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -11,6 +11,12 @@ export default { + components: { + Graph, + GraphGroup, + EmptyState, + }, + data() { const metricsData = document.querySelector('#prometheus-graphs').dataset; const store = new MonitoringStore(); @@ -36,12 +42,30 @@ }; }, - components: { - Graph, - GraphGroup, - EmptyState, + created() { + this.service = new MonitoringService({ + metricsEndpoint: this.metricsEndpoint, + deploymentEndpoint: this.deploymentEndpoint, + }); + eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); + eventHub.$on('hoverChanged', this.hoverChanged); + }, + + beforeDestroy() { + eventHub.$off('toggleAspectRatio', this.toggleAspectRatio); + eventHub.$off('hoverChanged', this.hoverChanged); + window.removeEventListener('resize', this.resizeThrottled, false); }, + mounted() { + this.resizeThrottled = _.throttle(this.resize, 600); + if (!this.hasMetrics) { + this.state = 'gettingStarted'; + } else { + this.getGraphsData(); + window.addEventListener('resize', this.resizeThrottled, false); + } + }, methods: { getGraphsData() { this.state = 'loading'; @@ -72,36 +96,14 @@ this.hoverData = data; }, }, - - created() { - this.service = new MonitoringService({ - metricsEndpoint: this.metricsEndpoint, - deploymentEndpoint: this.deploymentEndpoint, - }); - eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); - eventHub.$on('hoverChanged', this.hoverChanged); - }, - - beforeDestroy() { - eventHub.$off('toggleAspectRatio', this.toggleAspectRatio); - eventHub.$off('hoverChanged', this.hoverChanged); - window.removeEventListener('resize', this.resizeThrottled, false); - }, - - mounted() { - this.resizeThrottled = _.throttle(this.resize, 600); - if (!this.hasMetrics) { - this.state = 'gettingStarted'; - } else { - this.getGraphsData(); - window.addEventListener('resize', this.resizeThrottled, false); - } - }, }; </script> <template> - <div v-if="!showEmptyState" class="prometheus-graphs"> + <div + v-if="!showEmptyState" + class="prometheus-graphs" + > <graph-group v-for="(groupData, index) in store.groups" :key="index" diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue index a18164482a2..87d1975d5ad 100644 --- a/app/assets/javascripts/monitoring/components/empty_state.vue +++ b/app/assets/javascripts/monitoring/components/empty_state.vue @@ -33,13 +33,15 @@ gettingStarted: { svgUrl: this.emptyGettingStartedSvgPath, title: 'Get started with performance monitoring', - description: 'Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.', + description: `Stay updated about the performance and health +of your environment by configuring Prometheus to monitor your deployments.`, buttonText: 'Configure Prometheus', }, loading: { svgUrl: this.emptyLoadingSvgPath, title: 'Waiting for performance data', - description: 'Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.', + description: `Creating graphs uses the data from the Prometheus server. +If this takes a long time, ensure that data is available.`, buttonText: 'View documentation', }, unableToConnect: { @@ -74,20 +76,26 @@ <template> <div class="prometheus-state"> <div class="state-svg svg-content"> - <img :src="currentState.svgUrl"/> + <img :src="currentState.svgUrl" /> </div> <h4 class="state-title"> - {{currentState.title}} + {{ currentState.title }} </h4> <p class="state-description"> - {{currentState.description}} - <a v-if="showButtonDescription" :href="settingsPath"> + {{ currentState.description }} + <a + v-if="showButtonDescription" + :href="settingsPath" + > Prometheus server </a> </p> <div class="state-button"> - <a class="btn btn-success" :href="buttonPath"> - {{currentState.buttonText}} + <a + class="btn btn-success" + :href="buttonPath" + > + {{ currentState.buttonText }} </a> </div> </div> diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index a50b80c23d0..ea5c24efaf9 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -17,6 +17,15 @@ const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select }; export default { + components: { + GraphLegend, + GraphFlag, + GraphDeployment, + GraphPath, + }, + + mixins: [MonitoringMixin], + props: { graphData: { type: Object, @@ -45,8 +54,6 @@ }, }, - mixins: [MonitoringMixin], - data() { return { baseGraphHeight: 450, @@ -74,13 +81,6 @@ }; }, - components: { - GraphLegend, - GraphFlag, - GraphDeployment, - GraphPath, - }, - computed: { outerViewBox() { return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`; @@ -105,6 +105,26 @@ }, }, + watch: { + updateAspectRatio() { + if (this.updateAspectRatio) { + this.graphHeight = 450; + this.graphWidth = 600; + this.measurements = measurements.large; + this.draw(); + eventHub.$emit('toggleAspectRatio'); + } + }, + + hoverData() { + this.positionFlag(); + }, + }, + + mounted() { + this.draw(); + }, + methods: { draw() { const breakpointSize = bp.getBreakpointSize(); @@ -197,51 +217,34 @@ }); // This will select all of the ticks once they're rendered }, }, - - watch: { - updateAspectRatio() { - if (this.updateAspectRatio) { - this.graphHeight = 450; - this.graphWidth = 600; - this.measurements = measurements.large; - this.draw(); - eventHub.$emit('toggleAspectRatio'); - } - }, - - hoverData() { - this.positionFlag(); - }, - }, - - mounted() { - this.draw(); - }, }; </script> <template> - <div + <div class="prometheus-graph" @mouseover="showFlagContent = true" - @mouseleave="showFlagContent = false"> + @mouseleave="showFlagContent = false" + > <h5 class="text-center graph-title"> - {{graphData.title}} + {{ graphData.title }} </h5> <div class="prometheus-svg-container" - :style="paddingBottomRootSvg"> + :style="paddingBottomRootSvg" + > <svg :viewBox="outerViewBox" - ref="baseSvg"> + ref="baseSvg" + > <g class="x-axis" - :transform="axisTransform"> - </g> + :transform="axisTransform" + /> <g class="y-axis" - transform="translate(70, 20)"> - </g> + transform="translate(70, 20)" + /> <graph-legend :graph-width="graphWidth" :graph-height="graphHeight" @@ -256,29 +259,30 @@ <svg class="graph-data" :viewBox="innerViewBox" - ref="graphData"> - <graph-path - v-for="(path, index) in timeSeries" - :key="index" - :generated-line-path="path.linePath" - :generated-area-path="path.areaPath" - :line-style="path.lineStyle" - :line-color="path.lineColor" - :area-color="path.areaColor" - /> - <graph-deployment - :deployment-data="reducedDeploymentData" - :graph-height="graphHeight" - :graph-height-offset="graphHeightOffset" - /> - <rect - class="prometheus-graph-overlay" - :width="(graphWidth - 70)" - :height="(graphHeight - 100)" - transform="translate(-5, 20)" - ref="graphOverlay" - @mousemove="handleMouseOverGraph($event)"> - </rect> + ref="graphData" + > + <graph-path + v-for="(path, index) in timeSeries" + :key="index" + :generated-line-path="path.linePath" + :generated-area-path="path.areaPath" + :line-style="path.lineStyle" + :line-color="path.lineColor" + :area-color="path.areaColor" + /> + <graph-deployment + :deployment-data="reducedDeploymentData" + :graph-height="graphHeight" + :graph-height-offset="graphHeightOffset" + /> + <rect + class="prometheus-graph-overlay" + :width="(graphWidth - 70)" + :height="(graphHeight - 100)" + transform="translate(-5, 20)" + ref="graphOverlay" + @mousemove="handleMouseOverGraph($event)" + /> </svg> </svg> <graph-flag diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue index 8d6393d4ce5..98c25307b74 100644 --- a/app/assets/javascripts/monitoring/components/graph/deployment.vue +++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue @@ -39,33 +39,35 @@ y="0" :height="calculatedHeight" width="3" - fill="url(#shadow-gradient)"> - </rect> + fill="url(#shadow-gradient)" + /> <line class="deployment-line" x1="0" y1="0" x2="0" :y2="calculatedHeight" - stroke="#000"> - </line> + stroke="#000" + /> </g> <svg height="0" - width="0"> + width="0" + > <defs> <linearGradient - id="shadow-gradient"> + id="shadow-gradient" + > <stop offset="0%" stop-color="#000" - stop-opacity="0.4"> - </stop> + stop-opacity="0.4" + /> <stop offset="100%" stop-color="#000" - stop-opacity="0"> - </stop> + stop-opacity="0" + /> </linearGradient> </defs> </svg> diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue index 62ebc3f419c..07aa6a3e5de 100644 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ b/app/assets/javascripts/monitoring/components/graph/flag.vue @@ -1,9 +1,12 @@ <script> import { dateFormat, timeFormat } from '../../utils/date_time_formatters'; import { formatRelevantDigits } from '../../../lib/utils/number_utils'; - import Icon from '../../../vue_shared/components/icon.vue'; + import icon from '../../../vue_shared/components/icon.vue'; export default { + components: { + icon, + }, props: { currentXCoordinate: { type: Number, @@ -52,10 +55,6 @@ }, }, - components: { - Icon, - }, - computed: { formatTime() { return this.deploymentFlagData ? @@ -137,33 +136,34 @@ > <div class="arrow"></div> <div class="popover-title"> - <h5 v-if="this.deploymentFlagData"> + <h5 v-if="deploymentFlagData"> Deployed </h5> - {{formatDate}} at - <strong>{{formatTime}}</strong> + {{ formatDate }} at + <strong>{{ formatTime }}</strong> </div> <div - v-if="this.deploymentFlagData" + v-if="deploymentFlagData" class="popover-content deploy-meta-content" > <div> <icon name="commit" - :size="12"> - </icon> + :size="12" + /> <a :href="deploymentFlagData.commitUrl"> - {{deploymentFlagData.sha.slice(0, 8)}} + {{ deploymentFlagData.sha.slice(0, 8) }} </a> </div> <div - v-if="deploymentFlagData.tag"> + v-if="deploymentFlagData.tag" + > <icon name="label" - :size="12"> - </icon> + :size="12" + /> <a :href="deploymentFlagData.tagUrl"> - {{deploymentFlagData.ref}} + {{ deploymentFlagData.ref }} </a> </div> </div> @@ -174,7 +174,10 @@ :key="index" > <td> - <svg width="15" height="6"> + <svg + width="15" + height="6" + > <line :stroke="series.lineColor" :stroke-dasharray="strokeDashArray(series.lineStyle)" @@ -182,13 +185,13 @@ x1="0" x2="15" y1="2" - y2="2"> - </line> + y2="2" + /> </svg> </td> - <td>{{seriesMetricLabel(index, series)}}</td> + <td>{{ seriesMetricLabel(index, series) }}</td> <td> - <strong>{{seriesMetricValue(series)}}</strong> + <strong>{{ seriesMetricValue(series) }}</strong> </td> </tr> </table> diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue index 440b1b12631..c6e8d726ffc 100644 --- a/app/assets/javascripts/monitoring/components/graph/legend.vue +++ b/app/assets/javascripts/monitoring/components/graph/legend.vue @@ -73,6 +73,21 @@ }, }, + mounted() { + this.$nextTick(() => { + const bbox = this.$refs.ylabel.getBBox(); + this.metricUsageXPosition = 0; + this.seriesXPosition = 0; + if (this.$refs.legendTitleSvg != null) { + this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width; + } + if (this.$refs.seriesTitleSvg != null) { + this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width; + } + this.yLabelWidth = bbox.width + 10; // Added some padding + this.yLabelHeight = bbox.height + 5; + }); + }, methods: { translateLegendGroup(index) { return `translate(0, ${12 * (index)})`; @@ -100,26 +115,10 @@ return null; }, }, - mounted() { - this.$nextTick(() => { - const bbox = this.$refs.ylabel.getBBox(); - this.metricUsageXPosition = 0; - this.seriesXPosition = 0; - if (this.$refs.legendTitleSvg != null) { - this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width; - } - if (this.$refs.seriesTitleSvg != null) { - this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width; - } - this.yLabelWidth = bbox.width + 10; // Added some padding - this.yLabelHeight = bbox.height + 5; - }); - }, }; </script> <template> - <g - class="axis-label-container"> + <g class="axis-label-container"> <line class="label-x-axis-line" stroke="#000000" @@ -127,8 +126,8 @@ x1="10" :y1="yPosition" :x2="graphWidth + 20" - :y2="yPosition"> - </line> + :y2="yPosition" + /> <line class="label-y-axis-line" stroke="#000000" @@ -136,39 +135,43 @@ x1="10" y1="0" :x2="10" - :y2="yPosition"> - </line> + :y2="yPosition" + /> <rect class="rect-axis-text" :transform="rectTransform" :width="yLabelWidth" - :height="yLabelHeight"> - </rect> + :height="yLabelHeight" + /> <text class="label-axis-text y-label-text" text-anchor="middle" :transform="textTransform" - ref="ylabel"> - {{yAxisLabel}} + ref="ylabel" + > + {{ yAxisLabel }} </text> <rect class="rect-axis-text" :x="xPosition + 60" :y="graphHeight - 80" width="35" - height="50"> - </rect> + height="50" + /> <text class="label-axis-text x-label-text" :x="xPosition + 60" :y="yPosition" - dy=".35em"> + dy=".35em" + > Time </text> - <g class="legend-group" + <g + class="legend-group" v-for="(series, index) in timeSeries" :key="index" - :transform="translateLegendGroup(index)"> + :transform="translateLegendGroup(index)" + > <line :stroke="series.lineColor" :stroke-width="measurements.legends.height" @@ -176,23 +179,25 @@ :x1="measurements.legends.offsetX" :x2="measurements.legends.offsetX + measurements.legends.width" :y1="graphHeight - measurements.legends.offsetY" - :y2="graphHeight - measurements.legends.offsetY"> - </line> + :y2="graphHeight - measurements.legends.offsetY" + /> <text v-if="timeSeries.length > 1" class="legend-metric-title" ref="legendTitleSvg" x="38" - :y="graphHeight - 30"> - {{createSeriesString(index, series)}} + :y="graphHeight - 30" + > + {{ createSeriesString(index, series) }} </text> <text v-else class="legend-metric-title" ref="legendTitleSvg" x="38" - :y="graphHeight - 30"> - {{legendTitle}} {{formatMetricUsage(series)}} + :y="graphHeight - 30" + > + {{ legendTitle }} {{ formatMetricUsage(series) }} </text> </g> </g> diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue index 5e6d409033a..c9721c4cb01 100644 --- a/app/assets/javascripts/monitoring/components/graph/path.vue +++ b/app/assets/javascripts/monitoring/components/graph/path.vue @@ -12,6 +12,7 @@ lineStyle: { type: String, required: false, + default: '', }, lineColor: { type: String, @@ -37,8 +38,8 @@ class="metric-area" :d="generatedAreaPath" :fill="areaColor" - transform="translate(-5, 20)"> - </path> + transform="translate(-5, 20)" + /> <path class="metric-line" :d="generatedLinePath" @@ -46,7 +47,7 @@ fill="none" stroke-width="1" :stroke-dasharray="strokeDashArray" - transform="translate(-5, 20)"> - </path> + transform="translate(-5, 20)" + /> </g> </template> diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue index 958f537d31b..079351a69af 100644 --- a/app/assets/javascripts/monitoring/components/graph_group.vue +++ b/app/assets/javascripts/monitoring/components/graph_group.vue @@ -1,21 +1,21 @@ <script> -export default { - props: { - name: { - type: String, - required: true, + export default { + props: { + name: { + type: String, + required: true, + }, }, - }, -}; + }; </script> <template> <div class="panel panel-default prometheus-panel"> <div class="panel-heading"> - <h4>{{name}}</h4> + <h4>{{ name }}</h4> </div> <div class="panel-body prometheus-graph-group"> - <slot /> + <slot></slot> </div> </div> </template> diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue index 82c51a1068c..d0ec70f1fcf 100644 --- a/app/assets/javascripts/notebook/cells/markdown.vue +++ b/app/assets/javascripts/notebook/cells/markdown.vue @@ -91,18 +91,21 @@ <template> <div class="cell text-cell"> <prompt /> - <div class="markdown" v-html="markdown"></div> + <div + class="markdown" + v-html="markdown"> + </div> </div> </template> <style> -.markdown .katex { - display: block; - text-align: center; -} + .markdown .katex { + display: block; + text-align: center; + } -.markdown .inline-katex .katex { - display: inline; - text-align: initial; -} + .markdown .inline-katex .katex { + display: inline; + text-align: initial; + } </style> diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue index 2110a9de7ed..ebba5954de9 100644 --- a/app/assets/javascripts/notebook/cells/output/html.vue +++ b/app/assets/javascripts/notebook/cells/output/html.vue @@ -1,17 +1,17 @@ <script> -import Prompt from '../prompt.vue'; + import Prompt from '../prompt.vue'; -export default { - props: { - rawCode: { - type: String, - required: true, + export default { + components: { + prompt: Prompt, }, - }, - components: { - prompt: Prompt, - }, -}; + props: { + rawCode: { + type: String, + required: true, + }, + }, + }; </script> <template> diff --git a/app/assets/javascripts/notebook/cells/output/image.vue b/app/assets/javascripts/notebook/cells/output/image.vue index fbb39ea6e2d..67d6c5ad12b 100644 --- a/app/assets/javascripts/notebook/cells/output/image.vue +++ b/app/assets/javascripts/notebook/cells/output/image.vue @@ -1,27 +1,26 @@ <script> -import Prompt from '../prompt.vue'; + import Prompt from '../prompt.vue'; -export default { - props: { - outputType: { - type: String, - required: true, + export default { + components: { + prompt: Prompt, }, - rawCode: { - type: String, - required: true, + props: { + outputType: { + type: String, + required: true, + }, + rawCode: { + type: String, + required: true, + }, }, - }, - components: { - prompt: Prompt, - }, -}; + }; </script> <template> <div class="output"> <prompt /> - <img - :src="'data:' + outputType + ';base64,' + rawCode" /> + <img :src="'data:' + outputType + ';base64,' + rawCode" /> </div> </template> diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue index 05af0bf1e8e..91b2269a83a 100644 --- a/app/assets/javascripts/notebook/cells/output/index.vue +++ b/app/assets/javascripts/notebook/cells/output/index.vue @@ -1,83 +1,87 @@ <script> -import CodeCell from '../code/index.vue'; -import Html from './html.vue'; -import Image from './image.vue'; + import CodeCell from '../code/index.vue'; + import Html from './html.vue'; + import Image from './image.vue'; -export default { - props: { - codeCssClass: { - type: String, - required: false, - default: '', + export default { + components: { + 'code-cell': CodeCell, + 'html-output': Html, + 'image-output': Image, }, - count: { - type: Number, - required: false, - default: 0, + props: { + codeCssClass: { + type: String, + required: false, + default: '', + }, + count: { + type: Number, + required: false, + default: 0, + }, + output: { + type: Object, + requred: true, + default: () => ({}), + }, }, - output: { - type: Object, - requred: true, - }, - }, - components: { - 'code-cell': CodeCell, - 'html-output': Html, - 'image-output': Image, - }, - data() { - return { - outputType: '', - }; - }, - computed: { - componentName() { - if (this.output.text) { - return 'code-cell'; - } else if (this.output.data['image/png']) { - this.outputType = 'image/png'; - - return 'image-output'; - } else if (this.output.data['text/html']) { - this.outputType = 'text/html'; + computed: { + componentName() { + if (this.output.text) { + return 'code-cell'; + } else if (this.output.data['image/png']) { + return 'image-output'; + } else if (this.output.data['text/html']) { + return 'html-output'; + } else if (this.output.data['image/svg+xml']) { + return 'html-output'; + } - return 'html-output'; - } else if (this.output.data['image/svg+xml']) { - this.outputType = 'image/svg+xml'; - - return 'html-output'; - } + return 'code-cell'; + }, + rawCode() { + if (this.output.text) { + return this.output.text.join(''); + } - this.outputType = 'text/plain'; - return 'code-cell'; - }, - rawCode() { - if (this.output.text) { - return this.output.text.join(''); - } + return this.dataForType(this.outputType); + }, + outputType() { + if (this.output.text) { + return ''; + } else if (this.output.data['image/png']) { + return 'image/png'; + } else if (this.output.data['text/html']) { + return 'text/html'; + } else if (this.output.data['image/svg+xml']) { + return 'image/svg+xml'; + } - return this.dataForType(this.outputType); + return 'text/plain'; + }, }, - }, - methods: { - dataForType(type) { - let data = this.output.data[type]; + methods: { + dataForType(type) { + let data = this.output.data[type]; - if (typeof data === 'object') { - data = data.join(''); - } + if (typeof data === 'object') { + data = data.join(''); + } - return data; + return data; + }, }, - }, -}; + }; </script> <template> - <component :is="componentName" + <component + :is="componentName" type="output" - :outputType="outputType" + :output-type="outputType" :count="count" :raw-code="rawCode" - :code-css-class="codeCssClass" /> + :code-css-class="codeCssClass" + /> </template> diff --git a/app/assets/javascripts/notebook/cells/prompt.vue b/app/assets/javascripts/notebook/cells/prompt.vue index 039fb99293d..fe1fc37e1dc 100644 --- a/app/assets/javascripts/notebook/cells/prompt.vue +++ b/app/assets/javascripts/notebook/cells/prompt.vue @@ -4,10 +4,17 @@ type: { type: String, required: false, + default: '', }, count: { type: Number, required: false, + default: 0, + }, + }, + computed: { + hasKeys() { + return this.type !== '' && this.count; }, }, }; @@ -15,16 +22,16 @@ <template> <div class="prompt"> - <span v-if="type && count"> + <span v-if="hasKeys"> {{ type }} [{{ count }}]: </span> </div> </template> <style scoped> -.prompt { - padding: 0 10px; - min-width: 7em; - font-family: monospace; -} + .prompt { + padding: 0 10px; + min-width: 7em; + font-family: monospace; + } </style> diff --git a/app/assets/javascripts/notebook/index.vue b/app/assets/javascripts/notebook/index.vue index e88806431af..e2e3b08c77f 100644 --- a/app/assets/javascripts/notebook/index.vue +++ b/app/assets/javascripts/notebook/index.vue @@ -20,11 +20,6 @@ default: '', }, }, - methods: { - cellType(type) { - return `${type}-cell`; - }, - }, computed: { cells() { if (this.notebook.worksheets) { @@ -45,6 +40,11 @@ return Object.keys(this.notebook).length; }, }, + methods: { + cellType(type) { + return `${type}-cell`; + }, + }, }; </script> diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index e594377bc40..1f18c196137 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -15,7 +15,17 @@ import issuableStateMixin from '../mixins/issuable_state'; export default { - name: 'commentForm', + name: 'CommentForm', + components: { + issueWarning, + noteSignedOutWidget, + discussionLockedWidget, + markdownField, + userAvatarLink, + }, + mixins: [ + issuableStateMixin, + ], data() { return { note: '', @@ -27,21 +37,6 @@ isSubmitButtonDisabled: true, }; }, - components: { - issueWarning, - noteSignedOutWidget, - discussionLockedWidget, - markdownField, - userAvatarLink, - }, - watch: { - note(newNote) { - this.setIsSubmitButtonDisabled(newNote, this.isSubmitting); - }, - isSubmitting(newValue) { - this.setIsSubmitButtonDisabled(this.note, newValue); - }, - }, computed: { ...mapGetters([ 'getCurrentUserLastNote', @@ -65,7 +60,9 @@ if (this.note.length) { const actionText = this.isIssueOpen ? 'close' : 'reopen'; - return this.noteType === constants.COMMENT ? `Comment & ${actionText} issue` : `Start discussion & ${actionText} issue`; + return this.noteType === constants.COMMENT ? + `Comment & ${actionText} issue` : + `Start discussion & ${actionText} issue`; } return this.isIssueOpen ? 'Close issue' : 'Reopen issue'; @@ -97,6 +94,23 @@ return this.getNoteableData.create_note_path; }, }, + watch: { + note(newNote) { + this.setIsSubmitButtonDisabled(newNote, this.isSubmitting); + }, + isSubmitting(newValue) { + this.setIsSubmitButtonDisabled(this.note, newValue); + }, + }, + mounted() { + // jQuery is needed here because it is a custom event being dispatched with jQuery. + $(document).on('issuable:change', (e, isClosed) => { + this.issueState = isClosed ? constants.CLOSED : constants.REOPENED; + }); + + this.initAutoSave(); + this.initTaskList(); + }, methods: { ...mapActions([ 'saveNote', @@ -159,7 +173,9 @@ .catch(() => { this.isSubmitting = false; this.discard(false); - const msg = 'Your comment could not be submitted! Please check your network connection and try again.'; + const msg = + `Your comment could not be submitted! +Please check your network connection and try again.`; Flash(msg, 'alert', this.$el); this.note = noteData.data.note.note; // Restore textarea content. this.removePlaceholderNotes(); @@ -207,7 +223,11 @@ }, initAutoSave() { if (this.isLoggedIn) { - this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getNoteableData.id], 'issue'); + this.autosave = new Autosave( + $(this.$refs.textarea), + ['Note', 'Issue', this.getNoteableData.id], + 'issue', + ); } }, initTaskList() { @@ -223,18 +243,6 @@ }); }, }, - mixins: [ - issuableStateMixin, - ], - mounted() { - // jQuery is needed here because it is a custom event being dispatched with jQuery. - $(document).on('issuable:change', (e, isClosed) => { - this.issueState = isClosed ? constants.CLOSED : constants.REOPENED; - }); - - this.initAutoSave(); - this.initTaskList(); - }, }; </script> @@ -258,7 +266,7 @@ :img-src="author.avatar_url" :img-alt="author.name" :img-size="40" - /> + /> </div> <div class="timeline-content timeline-content-form"> <form @@ -283,7 +291,8 @@ <textarea id="note-body" name="note[note]" - class="note-textarea js-vue-comment-form js-gfm-input js-autosize markdown-area js-vue-textarea" + class="note-textarea js-vue-comment-form +js-gfm-input js-autosize markdown-area js-vue-textarea" data-supports-quick-actions="true" aria-label="Description" v-model="note" @@ -296,13 +305,15 @@ </textarea> </markdown-field> <div class="note-form-actions"> - <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> + <div + class="pull-left btn-group +append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> <button @click.prevent="handleSave()" :disabled="isSubmitButtonDisabled" class="btn btn-create comment-btn js-comment-button js-comment-submit-button" type="submit"> - {{commentButtonTitle}} + {{ commentButtonTitle }} </button> <button :disabled="isSubmitButtonDisabled" @@ -344,7 +355,7 @@ <i aria-hidden="true" class="fa fa-check icon"> - </i> + </i> <div class="description"> <strong>Start discussion</strong> <p> @@ -362,7 +373,7 @@ :class="actionButtonClassNames" :disabled="isSubmitting" class="btn btn-comment btn-comment-and-close js-action-button"> - {{issueActionButtonTitle}} + {{ issueActionButtonTitle }} </button> <button type="button" diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue index e6f7ee56ff3..fc0722042cc 100644 --- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue +++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue @@ -3,12 +3,12 @@ import Issuable from '~/vue_shared/mixins/issuable'; export default { - mixins: [ - Issuable, - ], components: { Icon, }, + mixins: [ + Issuable, + ], }; </script> @@ -18,9 +18,11 @@ <icon name="lock" :size="16" - class="icon"> - </icon> - <span>This {{ issuableDisplayName }} is locked. Only <b>project members</b> can comment.</span> - </span> + class="icon" + /> + <span> + This {{ issuableDisplayName }} is locked. Only <b>project members</b> can comment. + </span> + </span> </div> </template> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 7fb45ed4d4b..46ffb60aa60 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -9,7 +9,13 @@ import tooltip from '~/vue_shared/directives/tooltip'; export default { - name: 'noteActions', + name: 'NoteActions', + directives: { + tooltip, + }, + components: { + loadingIcon, + }, props: { authorId: { type: Number, @@ -41,12 +47,6 @@ required: true, }, }, - directives: { - tooltip, - }, - components: { - loadingIcon, - }, computed: { ...mapGetters([ 'getUserDataByProp', @@ -64,6 +64,13 @@ return this.getUserDataByProp('id'); }, }, + created() { + this.emojiSmiling = emojiSmiling; + this.emojiSmile = emojiSmile; + this.emojiSmiley = emojiSmiley; + this.editSvg = editSvg; + this.ellipsisSvg = ellipsisSvg; + }, methods: { onEdit() { this.$emit('handleEdit'); @@ -72,13 +79,6 @@ this.$emit('handleDelete'); }, }, - created() { - this.emojiSmiling = emojiSmiling; - this.emojiSmile = emojiSmile; - this.emojiSmiley = emojiSmiley; - this.editSvg = editSvg; - this.ellipsisSvg = ellipsisSvg; - }, }; </script> @@ -86,7 +86,9 @@ <div class="note-actions"> <span v-if="accessLevel" - class="note-role user-access-role">{{accessLevel}}</span> + class="note-role user-access-role"> + {{ accessLevel }} + </span> <div v-if="canAddAwardEmoji" class="note-actions-item"> @@ -98,20 +100,21 @@ data-placement="bottom" data-container="body" href="#" - title="Add reaction"> - <loading-icon :inline="true" /> - <span - v-html="emojiSmiling" - class="link-highlight award-control-icon-neutral"> - </span> - <span - v-html="emojiSmiley" - class="link-highlight award-control-icon-positive"> - </span> - <span - v-html="emojiSmile" - class="link-highlight award-control-icon-super-positive"> - </span> + title="Add reaction" + > + <loading-icon :inline="true" /> + <span + v-html="emojiSmiling" + class="link-highlight award-control-icon-neutral"> + </span> + <span + v-html="emojiSmiley" + class="link-highlight award-control-icon-positive"> + </span> + <span + v-html="emojiSmile" + class="link-highlight award-control-icon-super-positive"> + </span> </a> </div> <div @@ -125,9 +128,10 @@ class="note-action-button js-note-edit btn btn-transparent" data-container="body" data-placement="bottom"> - <span - v-html="editSvg" - class="link-highlight"></span> + <span + v-html="editSvg" + class="link-highlight"> + </span> </button> </div> <div @@ -141,9 +145,10 @@ data-toggle="dropdown" data-container="body" data-placement="bottom"> - <span - class="icon" - v-html="ellipsisSvg"></span> + <span + class="icon" + v-html="ellipsisSvg"> + </span> </button> <ul class="dropdown-menu more-actions-dropdown dropdown-open-left"> <li v-if="canReportAsAbuse"> diff --git a/app/assets/javascripts/notes/components/note_attachment.vue b/app/assets/javascripts/notes/components/note_attachment.vue index cd9571a4002..618b807b9cc 100644 --- a/app/assets/javascripts/notes/components/note_attachment.vue +++ b/app/assets/javascripts/notes/components/note_attachment.vue @@ -1,6 +1,6 @@ <script> export default { - name: 'noteAttachment', + name: 'NoteAttachment', props: { attachment: { type: Object, @@ -19,7 +19,8 @@ rel="noopener noreferrer"> <img :src="attachment.url" - class="note-image-attach" /> + class="note-image-attach" + /> </a> <div class="attachment"> <a @@ -29,8 +30,9 @@ rel="noopener noreferrer"> <i class="fa fa-paperclip" - aria-hidden="true"></i> - {{attachment.filename}} + aria-hidden="true"> + </i> + {{ attachment.filename }} </a> </div> </div> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index c3a340139e7..caa9701e03f 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -8,6 +8,9 @@ import tooltip from '../../vue_shared/directives/tooltip'; export default { + directives: { + tooltip, + }, props: { awards: { type: Array, @@ -26,9 +29,6 @@ required: true, }, }, - directives: { - tooltip, - }, computed: { ...mapGetters([ 'getUserData', @@ -73,6 +73,11 @@ return this.getUserData.id; }, }, + created() { + this.emojiSmiling = emojiSmiling; + this.emojiSmile = emojiSmile; + this.emojiSmiley = emojiSmiley; + }, methods: { ...mapActions([ 'toggleAwardRequest', @@ -168,11 +173,6 @@ .catch(() => Flash('Something went wrong on our end.')); }, }, - created() { - this.emojiSmiling = emojiSmiling; - this.emojiSmile = emojiSmile; - this.emojiSmiley = emojiSmiley; - }, }; </script> @@ -191,7 +191,7 @@ type="button"> <span v-html="getAwardHTML(awardName)"></span> <span class="award-control-text js-counter"> - {{awardList.length}} + {{ awardList.length }} </span> </button> <div diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index ac4e1ffe53a..2d7cd30115d 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -7,6 +7,15 @@ import autosave from '../mixins/autosave'; export default { + components: { + noteEditedText, + noteAwardsList, + noteAttachment, + noteForm, + }, + mixins: [ + autosave, + ], props: { note: { type: Object, @@ -22,40 +31,11 @@ default: false, }, }, - mixins: [ - autosave, - ], - components: { - noteEditedText, - noteAwardsList, - noteAttachment, - noteForm, - }, computed: { noteBody() { return this.note.note; }, }, - methods: { - renderGFM() { - $(this.$refs['note-body']).renderGFM(); - }, - initTaskList() { - if (this.canEdit) { - this.taskList = new TaskList({ - dataType: 'note', - fieldName: 'note', - selector: '.notes', - }); - } - }, - handleFormUpdate(note, parentElement, callback) { - this.$emit('handleFormUpdate', note, parentElement, callback); - }, - formCancelHandler(shouldConfirm, isDirty) { - this.$emit('cancelFormEdition', shouldConfirm, isDirty); - }, - }, mounted() { this.renderGFM(); this.initTaskList(); @@ -76,6 +56,26 @@ } } }, + methods: { + renderGFM() { + $(this.$refs['note-body']).renderGFM(); + }, + initTaskList() { + if (this.canEdit) { + this.taskList = new TaskList({ + dataType: 'note', + fieldName: 'note', + selector: '.notes', + }); + } + }, + handleFormUpdate(note, parentElement, callback) { + this.$emit('handleFormUpdate', note, parentElement, callback); + }, + formCancelHandler(shouldConfirm, isDirty) { + this.$emit('cancelFormEdition', shouldConfirm, isDirty); + }, + }, }; </script> @@ -95,7 +95,7 @@ :is-editing="isEditing" :note-body="noteBody" :note-id="note.id" - /> + /> <textarea v-if="canEdit" v-model="note.note" @@ -106,17 +106,17 @@ :edited-at="note.last_edited_at" :edited-by="note.last_edited_by" action-text="Edited" - /> + /> <note-awards-list v-if="note.award_emoji.length" :note-id="note.id" :note-author-id="note.author.id" :awards="note.award_emoji" :toggle-award-path="note.toggle_award_path" - /> + /> <note-attachment v-if="note.attachment" :attachment="note.attachment" - /> + /> </div> </template> diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue index 49e09f0ecc5..ae2e52554d2 100644 --- a/app/assets/javascripts/notes/components/note_edited_text.vue +++ b/app/assets/javascripts/notes/components/note_edited_text.vue @@ -2,7 +2,10 @@ import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; export default { - name: 'editedNoteText', + name: 'EditedNoteText', + components: { + timeAgoTooltip, + }, props: { actionText: { type: String, @@ -15,6 +18,7 @@ editedBy: { type: Object, required: false, + default: () => ({}), }, className: { type: String, @@ -22,25 +26,22 @@ default: 'edited-text', }, }, - components: { - timeAgoTooltip, - }, }; </script> <template> <div :class="className"> - {{actionText}} + {{ actionText }} <time-ago-tooltip :time="editedAt" tooltip-placement="bottom" - /> + /> <template v-if="editedBy"> by <a :href="editedBy.path" class="js-vue-author author_link"> - {{editedBy.name}} + {{ editedBy.name }} </a> </template> </div> diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 4d527cb6643..aeda3497715 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -6,7 +6,14 @@ import issuableStateMixin from '../mixins/issuable_state'; export default { - name: 'issueNoteForm', + name: 'IssueNoteForm', + components: { + issueWarning, + markdownField, + }, + mixins: [ + issuableStateMixin, + ], props: { noteBody: { type: String, @@ -16,6 +23,7 @@ noteId: { type: Number, required: false, + default: 0, }, saveButtonTitle: { type: String, @@ -39,10 +47,6 @@ isSubmitting: false, }; }, - components: { - issueWarning, - markdownField, - }, computed: { ...mapGetters([ 'getDiscussionLastNote', @@ -70,6 +74,18 @@ return !this.note.length || this.isSubmitting; }, }, + watch: { + noteBody() { + if (this.note === this.noteBody) { + this.note = this.noteBody; + } else { + this.conflictWhileEditing = true; + } + }, + }, + mounted() { + this.$refs.textarea.focus(); + }, methods: { handleUpdate() { this.isSubmitting = true; @@ -94,26 +110,13 @@ this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.note); }, }, - mixins: [ - issuableStateMixin, - ], - mounted() { - this.$refs.textarea.focus(); - }, - watch: { - noteBody() { - if (this.note === this.noteBody) { - this.note = this.noteBody; - } else { - this.conflictWhileEditing = true; - } - }, - }, }; </script> <template> - <div ref="editNoteForm" class="note-edit-form current-note-edit-form"> + <div + ref="editNoteForm" + class="note-edit-form current-note-edit-form"> <div v-if="conflictWhileEditing" class="js-conflict-edit-warning alert alert-danger"> @@ -121,12 +124,13 @@ <a :href="noteHash" target="_blank" - rel="noopener noreferrer">updated comment</a> - to ensure information is not lost. + rel="noopener noreferrer"> + updated comment + </a> + to ensure information is not lost. </div> <div class="flash-container timeline-content"></div> - <form - class="edit-note common-note-form js-quick-submit gfm-form"> + <form class="edit-note common-note-form js-quick-submit gfm-form"> <issue-warning v-if="hasWarning(getNoteableData)" @@ -142,7 +146,8 @@ <textarea id="note_note" name="note[note]" - class="note-textarea js-gfm-input js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" + class="note-textarea js-gfm-input +js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" :data-supports-quick-actions="!isEditing" aria-label="Description" v-model="note" @@ -160,7 +165,7 @@ @click="handleUpdate()" :disabled="isDisabled" class="js-vue-issue-save btn btn-save"> - {{saveButtonTitle}} + {{ saveButtonTitle }} </button> <button @click="cancelHandler()" diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index 63aa3d777d0..b28dda4904d 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -3,6 +3,9 @@ import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; export default { + components: { + timeAgoTooltip, + }, props: { author: { type: Object, @@ -37,9 +40,6 @@ isExpanded: true, }; }, - components: { - timeAgoTooltip, - }, computed: { toggleChevronClass() { return this.isExpanded ? 'fa-chevron-up' : 'fa-chevron-down'; @@ -67,16 +67,16 @@ <div class="note-header-info"> <a :href="author.path"> <span class="note-header-author-name"> - {{author.name}} + {{ author.name }} </span> <span class="note-headline-light"> - @{{author.username}} + @{{ author.username }} </span> </a> <span class="note-headline-light"> <span class="note-headline-meta"> <template v-if="actionText"> - {{actionText}} + {{ actionText }} </template> <span v-if="actionTextHtml" @@ -90,12 +90,13 @@ <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" - /> + /> </a> <i class="fa fa-spinner fa-spin editing-spinner" aria-label="Comment is being updated" - aria-hidden="true"> + aria-hidden="true" + > </i> </span> </span> @@ -106,12 +107,12 @@ @click="handleToggle" class="note-action-button discussion-toggle-button js-vue-toggle-button" type="button"> - <i - :class="toggleChevronClass" - class="fa" - aria-hidden="true"> - </i> - Toggle discussion + <i + :class="toggleChevronClass" + class="fa" + aria-hidden="true"> + </i> + Toggle discussion </button> </div> </div> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 11e8f805635..98a06c5fc71 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -13,17 +13,6 @@ import autosave from '../mixins/autosave'; export default { - props: { - note: { - type: Object, - required: true, - }, - }, - data() { - return { - isReplying: false, - }; - }, components: { noteableNote, userAvatarLink, @@ -37,6 +26,17 @@ mixins: [ autosave, ], + props: { + note: { + type: Object, + required: true, + }, + }, + data() { + return { + isReplying: false, + }; + }, computed: { ...mapGetters([ 'getNoteableData', @@ -72,6 +72,20 @@ return null; }, }, + mounted() { + if (this.isReplying) { + this.initAutoSave(); + } + }, + updated() { + if (this.isReplying) { + if (!this.autosave) { + this.initAutoSave(); + } else { + this.setAutoSave(); + } + } + }, methods: { ...mapActions([ 'saveNote', @@ -130,7 +144,8 @@ this.removePlaceholderNotes(); this.isReplying = true; this.$nextTick(() => { - const msg = 'Your comment could not be submitted! Please check your network connection and try again.'; + const msg = `Your comment could not be submitted! +Please check your network connection and try again.`; Flash(msg, 'alert', this.$el); this.$refs.noteForm.note = noteText; callback(err); @@ -138,20 +153,6 @@ }); }, }, - mounted() { - if (this.isReplying) { - this.initAutoSave(); - } - }, - updated() { - if (this.isReplying) { - if (!this.autosave) { - this.initAutoSave(); - } else { - this.setAutoSave(); - } - } - }, }; </script> @@ -164,7 +165,7 @@ :img-src="author.avatar_url" :img-alt="author.name" :img-size="40" - /> + /> </div> <div class="timeline-content"> <div class="discussion"> @@ -184,42 +185,43 @@ :edited-by="lastUpdatedBy" action-text="Last updated" class-name="discussion-headline-light js-discussion-headline" - /> - </div> + /> </div> - <div - v-if="note.expanded" - class="discussion-body"> - <div class="panel panel-default"> - <div class="discussion-notes"> - <ul class="notes"> - <component - v-for="note in note.notes" - :is="componentName(note)" - :note="componentData(note)" - :key="note.id" - /> - </ul> - <div - :class="{ 'is-replying': isReplying }" - class="discussion-reply-holder"> - <button - v-if="canReply && !isReplying" - @click="showReplyForm" - type="button" - class="js-vue-discussion-reply btn btn-text-field" - title="Add a reply">Reply...</button> - <note-form - v-if="isReplying" - save-button-title="Comment" - :discussion="note" - :is-editing="false" - @handleFormUpdate="saveReply" - @cancelFormEdition="cancelReplyForm" - ref="noteForm" - /> - <note-signed-out-widget v-if="!canReply" /> - </div> + </div> + <div + v-if="note.expanded" + class="discussion-body"> + <div class="panel panel-default"> + <div class="discussion-notes"> + <ul class="notes"> + <component + v-for="note in note.notes" + :is="componentName(note)" + :note="componentData(note)" + :key="note.id" + /> + </ul> + <div + :class="{ 'is-replying': isReplying }" + class="discussion-reply-holder"> + <button + v-if="canReply && !isReplying" + @click="showReplyForm" + type="button" + class="js-vue-discussion-reply btn btn-text-field" + title="Add a reply"> + Reply... + </button> + <note-form + v-if="isReplying" + save-button-title="Comment" + :discussion="note" + :is-editing="false" + @handleFormUpdate="saveReply" + @cancelFormEdition="cancelReplyForm" + ref="noteForm" + /> + <note-signed-out-widget v-if="!canReply" /> </div> </div> </div> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 9186d6ff64a..30e7ccc8229 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -9,6 +9,12 @@ import eventHub from '../event_hub'; export default { + components: { + userAvatarLink, + noteHeader, + noteActions, + noteBody, + }, props: { note: { type: Object, @@ -22,12 +28,6 @@ isRequesting: false, }; }, - components: { - userAvatarLink, - noteHeader, - noteActions, - noteBody, - }, computed: { ...mapGetters([ 'targetNoteHash', @@ -51,6 +51,16 @@ return `note_${this.note.id}`; }, }, + + created() { + eventHub.$on('enterEditMode', ({ noteId }) => { + if (noteId === this.note.id) { + this.isEditing = true; + this.scrollToNoteIfNeeded($(this.$el)); + } + }); + }, + methods: { ...mapActions([ 'deleteNote', @@ -126,14 +136,6 @@ this.$refs.noteBody.$refs.noteForm.note = noteText; }, }, - created() { - eventHub.$on('enterEditMode', ({ noteId }) => { - if (noteId === this.note.id) { - this.isEditing = true; - this.scrollToNoteIfNeeded($(this.$el)); - } - }); - }, }; </script> @@ -150,7 +152,7 @@ :img-src="author.avatar_url" :img-alt="author.name" :img-size="40" - /> + /> </div> <div class="timeline-content"> <div class="note-header"> @@ -159,7 +161,7 @@ :created-at="note.created_at" :note-id="note.id" action-text="commented" - /> + /> <note-actions :author-id="author.id" :note-id="note.id" @@ -170,7 +172,7 @@ :report-abuse-path="note.report_abuse_path" @handleEdit="editHandler" @handleDelete="deleteHandler" - /> + /> </div> <note-body :note="note" @@ -179,7 +181,7 @@ @handleFormUpdate="formUpdateHandler" @cancelFormEdition="formCancelHandler" ref="noteBody" - /> + /> </div> </div> </li> diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index c4cae4b3b6f..92db4830704 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -13,7 +13,16 @@ import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { - name: 'notesApp', + name: 'NotesApp', + components: { + noteableNote, + noteableDiscussion, + systemNote, + commentForm, + loadingIcon, + placeholderNote, + placeholderSystemNote, + }, props: { noteableData: { type: Object, @@ -26,7 +35,7 @@ userData: { type: Object, required: false, - default: {}, + default: () => ({}), }, }, store, @@ -35,21 +44,30 @@ isLoading: true, }; }, - components: { - noteableNote, - noteableDiscussion, - systemNote, - commentForm, - loadingIcon, - placeholderNote, - placeholderSystemNote, - }, computed: { ...mapGetters([ 'notes', 'getNotesDataByProp', ]), }, + created() { + this.setNotesData(this.notesData); + this.setNoteableData(this.noteableData); + this.setUserData(this.userData); + }, + mounted() { + this.fetchNotes(); + + const parentElement = this.$el.parentElement; + + if (parentElement && + parentElement.classList.contains('js-vue-notes-event')) { + parentElement.addEventListener('toggleAward', (event) => { + const { awardName, noteId } = event.detail; + this.actionToggleAward({ awardName, noteId }); + }); + } + }, methods: { ...mapActions({ actionFetchNotes: 'fetchNotes', @@ -105,24 +123,6 @@ } }, }, - created() { - this.setNotesData(this.notesData); - this.setNoteableData(this.noteableData); - this.setUserData(this.userData); - }, - mounted() { - this.fetchNotes(); - - const parentElement = this.$el.parentElement; - - if (parentElement && - parentElement.classList.contains('js-vue-notes-event')) { - parentElement.addEventListener('toggleAward', (event) => { - const { awardName, noteId } = event.detail; - this.actionToggleAward({ awardName, noteId }); - }); - } - }, }; </script> @@ -144,7 +144,7 @@ :is="getComponentName(note)" :note="getComponentData(note)" :key="note.id" - /> + /> </ul> <comment-form /> diff --git a/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js b/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js new file mode 100644 index 00000000000..6e66ef69fe1 --- /dev/null +++ b/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js @@ -0,0 +1,3 @@ +import UserCallout from '../../../../user_callout'; + +export default () => new UserCallout(); diff --git a/app/assets/javascripts/ci_lint_editor.js b/app/assets/javascripts/pages/ci/lints/ci_lint_editor.js index b9469e5b7cb..b9469e5b7cb 100644 --- a/app/assets/javascripts/ci_lint_editor.js +++ b/app/assets/javascripts/pages/ci/lints/ci_lint_editor.js diff --git a/app/assets/javascripts/pages/ci/lints/index.js b/app/assets/javascripts/pages/ci/lints/index.js new file mode 100644 index 00000000000..5cc66546109 --- /dev/null +++ b/app/assets/javascripts/pages/ci/lints/index.js @@ -0,0 +1,3 @@ +import CILintEditor from './ci_lint_editor'; + +export default () => new CILintEditor(); diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js new file mode 100644 index 00000000000..b7353669e65 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/issues/index.js @@ -0,0 +1,7 @@ +import projectSelect from '~/project_select'; +import initLegacyFilters from '~/init_legacy_filters'; + +export default () => { + projectSelect(); + initLegacyFilters(); +}; diff --git a/app/assets/javascripts/pages/dashboard/milestones/index/index.js b/app/assets/javascripts/pages/dashboard/milestones/index/index.js new file mode 100644 index 00000000000..0f2f1bd4a25 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/milestones/index/index.js @@ -0,0 +1,3 @@ +import projectSelect from '~/project_select'; + +export default projectSelect; diff --git a/app/assets/javascripts/pages/dashboard/milestones/show/index.js b/app/assets/javascripts/pages/dashboard/milestones/show/index.js new file mode 100644 index 00000000000..2e7a08a369c --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/milestones/show/index.js @@ -0,0 +1,7 @@ +import Milestone from '~/milestone'; +import Sidebar from '~/right_sidebar'; + +export default () => { + new Milestone(); // eslint-disable-line no-new + new Sidebar(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/dashboard/projects/index.js b/app/assets/javascripts/pages/dashboard/projects/index.js new file mode 100644 index 00000000000..c88cbf1a6ba --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/projects/index.js @@ -0,0 +1,3 @@ +import ProjectsList from '~/projects_list'; + +export default () => new ProjectsList(); diff --git a/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js new file mode 100644 index 00000000000..5defea104d4 --- /dev/null +++ b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js @@ -0,0 +1,3 @@ +import UsersSelect from '../../../../users_select'; + +export default () => new UsersSelect(); diff --git a/app/assets/javascripts/pages/snippets/show/index.js b/app/assets/javascripts/pages/snippets/show/index.js new file mode 100644 index 00000000000..04c9562bfbb --- /dev/null +++ b/app/assets/javascripts/pages/snippets/show/index.js @@ -0,0 +1,12 @@ +/* eslint-disable no-new */ +import LineHighlighter from '../../../line_highlighter'; +import BlobViewer from '../../../blob/viewer'; +import ZenMode from '../../../zen_mode'; +import initNotes from '../../../init_notes'; + +export default () => { + new LineHighlighter(); + new BlobViewer(); + initNotes(); + new ZenMode(); +}; diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue index c8a2f778ee8..00f32d9de78 100644 --- a/app/assets/javascripts/pdf/index.vue +++ b/app/assets/javascripts/pdf/index.vue @@ -5,6 +5,7 @@ import page from './page/index.vue'; export default { + components: { page }, props: { pdf: { type: [String, Uint8Array], @@ -17,8 +18,6 @@ pages: [], }; }, - components: { page }, - watch: { pdf: 'load' }, computed: { document() { return typeof this.pdf === 'string' ? this.pdf : { data: this.pdf }; @@ -27,6 +26,11 @@ return this.pdf && this.pdf.length > 0; }, }, + watch: { pdf: 'load' }, + mounted() { + pdfjsLib.PDFJS.workerSrc = workerSrc; + if (this.hasPDF) this.load(); + }, methods: { load() { this.pages = []; @@ -47,20 +51,20 @@ return Promise.all(pagePromises); }, }, - mounted() { - pdfjsLib.PDFJS.workerSrc = workerSrc; - if (this.hasPDF) this.load(); - }, }; </script> <template> - <div class="pdf-viewer" v-if="hasPDF"> - <page v-for="(page, index) in pages" + <div + class="pdf-viewer" + v-if="hasPDF"> + <page + v-for="(page, index) in pages" :key="index" :v-if="!loading" :page="page" - :number="index + 1" /> + :number="index + 1" + /> </div> </template> diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue index be38f7cc129..fcba819beba 100644 --- a/app/assets/javascripts/pdf/page/index.vue +++ b/app/assets/javascripts/pdf/page/index.vue @@ -45,24 +45,26 @@ <canvas class="pdf-page" ref="canvas" - :data-page="number" /> + :data-page="number" + > + </canvas> </template> <style> -.pdf-page { - margin: 8px auto 0 auto; - border-top: 1px #ddd solid; - border-bottom: 1px #ddd solid; - width: 100%; -} + .pdf-page { + margin: 8px auto 0 auto; + border-top: 1px #ddd solid; + border-bottom: 1px #ddd solid; + width: 100%; + } -.pdf-page:first-child { - margin-top: 0px; - border-top: 0px; -} + .pdf-page:first-child { + margin-top: 0px; + border-top: 0px; + } -.pdf-page:last-child { - margin-bottom: 0px; - border-bottom: 0px; -} + .pdf-page:last-child { + margin-bottom: 0px; + border-bottom: 0px; + } </style> diff --git a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue index b5d85299cf8..2d18fa2044b 100644 --- a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue @@ -32,6 +32,20 @@ return !!(this.customInputEnabled || !this.intervalIsPreset); }, }, + watch: { + cronInterval() { + // updates field validation state when model changes, as + // glFieldError only updates on input. + this.$nextTick(() => { + gl.pipelineScheduleFieldErrors.updateFormValidityState(); + }); + }, + }, + created() { + if (this.intervalIsPreset) { + this.enableCustomInput = false; + } + }, methods: { toggleCustomInput(shouldEnable) { this.customInputEnabled = shouldEnable; @@ -43,20 +57,6 @@ } }, }, - created() { - if (this.intervalIsPreset) { - this.enableCustomInput = false; - } - }, - watch: { - cronInterval() { - // updates field validation state when model changes, as - // glFieldError only updates on input. - this.$nextTick(() => { - gl.pipelineScheduleFieldErrors.updateFormValidityState(); - }); - }, - }, }; </script> @@ -78,7 +78,12 @@ </label> <span class="cron-syntax-link-wrap"> - (<a :href="cronSyntaxUrl" target="_blank">{{ __('Cron syntax') }}</a>) + (<a + :href="cronSyntaxUrl" + target="_blank" + > + {{ __('Cron syntax') }} + </a>) </span> </div> @@ -93,7 +98,10 @@ @click="toggleCustomInput(false)" /> - <label class="label-light" for="every-day"> + <label + class="label-light" + for="every-day" + > {{ __('Every day (at 4:00am)') }} </label> </div> @@ -109,7 +117,10 @@ @click="toggleCustomInput(false)" /> - <label class="label-light" for="every-week"> + <label + class="label-light" + for="every-week" + > {{ __('Every week (Sundays at 4:00am)') }} </label> </div> @@ -125,7 +136,10 @@ @click="toggleCustomInput(false)" /> - <label class="label-light" for="every-month"> + <label + class="label-light" + for="every-month" + > {{ __('Every month (on the 1st at 4:00am)') }} </label> </div> diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue index 6e0bc2d697a..aa04a0ac47a 100644 --- a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue +++ b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue @@ -16,15 +16,15 @@ calloutDismissed: Cookies.get(cookieKey) === 'true', }; }, + created() { + this.illustrationSvg = illustrationSvg; + }, methods: { dismissCallout() { this.calloutDismissed = true; Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 }); }, }, - created() { - this.illustrationSvg = illustrationSvg; - }, }; </script> <template> @@ -41,17 +41,25 @@ class="fa fa-times"> </i> </button> - <div class="svg-container" v-html="illustrationSvg"></div> + <div + class="svg-container" + v-html="illustrationSvg"> + </div> <div class="user-callout-copy"> <h4>{{ __('Scheduling Pipelines') }}</h4> <p> - {{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }} + {{ __(`The pipelines schedule runs pipelines in the future, +repeatedly, for specific branches or tags. +Those scheduled pipelines will inherit limited project access based on their associated user.`) }} </p> <p> {{ __('Learn more in the') }} <a :href="docsUrl" target="_blank" - rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period --> + rel="nofollow" + > + {{ s__('Learn more in the|pipeline schedules documentation') }}</a>. + <!-- oneline to prevent extra space before period --> </p> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue index 16cc0761fc1..4ad3f66ee8c 100644 --- a/app/assets/javascripts/pipelines/components/async_button.vue +++ b/app/assets/javascripts/pipelines/components/async_button.vue @@ -1,67 +1,68 @@ <script> -/* eslint-disable no-new, no-alert */ + /* eslint-disable no-alert */ -import eventHub from '../event_hub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; + import eventHub from '../event_hub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; -export default { - props: { - endpoint: { - type: String, - required: true, + export default { + directives: { + tooltip, }, - title: { - type: String, - required: true, + components: { + loadingIcon, }, - icon: { - type: String, - required: true, + props: { + endpoint: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + icon: { + type: String, + required: true, + }, + cssClass: { + type: String, + required: true, + }, + confirmActionMessage: { + type: String, + required: false, + default: '', + }, }, - cssClass: { - type: String, - required: true, + data() { + return { + isLoading: false, + }; }, - confirmActionMessage: { - type: String, - required: false, + computed: { + iconClass() { + return `fa fa-${this.icon}`; + }, + buttonClass() { + return `btn ${this.cssClass}`; + }, }, - }, - directives: { - tooltip, - }, - components: { - loadingIcon, - }, - data() { - return { - isLoading: false, - }; - }, - computed: { - iconClass() { - return `fa fa-${this.icon}`; - }, - buttonClass() { - return `btn ${this.cssClass}`; - }, - }, - methods: { - onClick() { - if (this.confirmActionMessage && confirm(this.confirmActionMessage)) { - this.makeRequest(); - } else if (!this.confirmActionMessage) { - this.makeRequest(); - } - }, - makeRequest() { - this.isLoading = true; + methods: { + onClick() { + if (this.confirmActionMessage !== '' && confirm(this.confirmActionMessage)) { + this.makeRequest(); + } else if (this.confirmActionMessage === '') { + this.makeRequest(); + } + }, + makeRequest() { + this.isLoading = true; - eventHub.$emit('postAction', this.endpoint); + eventHub.$emit('postAction', this.endpoint); + }, }, - }, -}; + }; </script> <template> diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue index 78322f30685..dfaa2574091 100644 --- a/app/assets/javascripts/pipelines/components/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/empty_state.vue @@ -26,13 +26,15 @@ {{ s__("Pipelines|Build with confidence") }} </h4> <p> - {{ s__("Pipelines|Continous Integration can help catch bugs by running your tests automatically, while Continuous Deployment can help you deliver code to your product environment.") }} + {{ s__(`Pipelines|Continous Integration can help +catch bugs by running your tests automatically, +while Continuous Deployment can help you deliver code to your product environment.`) }} </p> <div class="text-center"> <a :href="helpPagePath" class="btn btn-info" - > + > {{ s__("Pipelines|Get started with Pipelines") }} </a> </div> diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index 19d8e1f49cf..d7effb27bff 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -7,6 +7,14 @@ * TODO: Remove UJS from here and use an async request instead. */ export default { + components: { + icon, + }, + + directives: { + tooltip, + }, + props: { tooltipText: { type: String, @@ -29,14 +37,6 @@ }, }, - components: { - icon, - }, - - directives: { - tooltip, - }, - computed: { cssClass() { const actionIconDash = dasherize(this.actionIcon); @@ -53,7 +53,8 @@ :href="link" class="ci-action-icon-container ci-action-icon-wrapper" :class="cssClass" - data-container="body"> - <icon :name="actionIcon"/> + data-container="body" + > + <icon :name="actionIcon" /> </a> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue index 1c0944d45fc..7c4fd65e36f 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue @@ -7,6 +7,13 @@ * TODO: Remove UJS from here and use an async request instead. */ export default { + components: { + icon, + }, + + directives: { + tooltip, + }, props: { tooltipText: { type: String, @@ -28,14 +35,6 @@ required: true, }, }, - - components: { - icon, - }, - - directives: { - tooltip, - }, }; </script> <template> @@ -47,7 +46,8 @@ rel="nofollow" class="ci-action-icon-wrapper js-ci-status-icon" data-container="body" - aria-label="Job's action"> - <icon :name="actionIcon"/> + aria-label="Job's action" + > + <icon :name="actionIcon" /> </a> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index 7006d05e7b2..b86e95f0b4a 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -27,13 +27,6 @@ * } */ export default { - props: { - job: { - type: Object, - required: true, - }, - }, - directives: { tooltip, }, @@ -43,12 +36,23 @@ jobNameComponent, }, + props: { + job: { + type: Object, + required: true, + }, + }, + computed: { tooltipText() { return `${this.job.name} - ${this.job.status.label}`; }, }, + mounted() { + this.stopDropdownClickPropagation(); + }, + methods: { /** * When the user right clicks or cmd/ctrl + click in the job name @@ -59,16 +63,13 @@ * target the click event of this component. */ stopDropdownClickPropagation() { - $(this.$el.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item')) + $(this.$el + .querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item')) .on('click', (e) => { e.stopPropagation(); }); }, }, - - mounted() { - this.stopDropdownClickPropagation(); - }, }; </script> <template> @@ -83,22 +84,25 @@ <job-name-component :name="job.name" - :status="job.status" /> + :status="job.status" + /> <span class="dropdown-counter-badge"> - {{job.size}} + {{ job.size }} </span> </button> <ul class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown"> <li class="scrollable-menu"> <ul> - <li v-for="item in job.jobs"> + <li + v-for="(item, i) in job.jobs" + :key="i"> <job-component :job="item" :is-dropdown="true" css-class-job-name="mini-pipeline-graph-dropdown-item" - /> + /> </li> </ul> </li> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 66bc1d1979c..a1f58580318 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,9 +1,13 @@ <script> import loadingIcon from '~/vue_shared/components/loading_icon.vue'; - import '~/flash'; import stageColumnComponent from './stage_column_component.vue'; export default { + components: { + stageColumnComponent, + loadingIcon, + }, + props: { isLoading: { type: Boolean, @@ -15,11 +19,6 @@ }, }, - components: { - stageColumnComponent, - loadingIcon, - }, - computed: { graph() { return this.pipeline.details && this.pipeline.details.stages; @@ -58,7 +57,7 @@ <loading-icon v-if="isLoading" size="3" - /> + /> </div> <ul @@ -70,7 +69,8 @@ :jobs="stage.groups" :key="stage.name" :stage-connector-class="stageConnectorClass(index, stage)" - :is-first-column="isFirstColumn(index)"/> + :is-first-column="isFirstColumn(index)" + /> </ul> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index b01c799643c..9b136573135 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -29,6 +29,15 @@ */ export default { + components: { + actionComponent, + dropdownActionComponent, + jobNameComponent, + }, + + directives: { + tooltip, + }, props: { job: { type: Object, @@ -48,16 +57,6 @@ }, }, - components: { - actionComponent, - dropdownActionComponent, - jobNameComponent, - }, - - directives: { - tooltip, - }, - computed: { status() { return this.job && this.job.status ? this.job.status : {}; @@ -102,12 +101,12 @@ :class="cssClassJobName" data-container="body" class="js-pipeline-graph-job-link" - > + > <job-name-component :name="job.name" :status="job.status" - /> + /> </a> <div @@ -117,12 +116,12 @@ :title="tooltipText" :class="cssClassJobName" data-container="body" - > + > <job-name-component :name="job.name" :status="job.status" - /> + /> </div> <action-component @@ -131,7 +130,7 @@ :link="status.action.path" :action-icon="status.action.icon" :action-method="status.action.method" - /> + /> <dropdown-action-component v-if="hasAction && isDropdown" @@ -139,6 +138,6 @@ :link="status.action.path" :action-icon="status.action.icon" :action-method="status.action.method" - /> + /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue index f46d21bd6d7..14f4964a406 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue @@ -8,6 +8,9 @@ * - Dropdown badge components */ export default { + components: { + ciIcon, + }, props: { name: { type: String, @@ -19,19 +22,14 @@ required: true, }, }, - - components: { - ciIcon, - }, }; </script> <template> <span class="ci-job-name-component"> - <ci-icon - :status="status" /> + <ci-icon :status="status" /> <span class="ci-status-text"> - {{name}} + {{ name }} </span> </span> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 9b1bbb0906f..e027f08ff5c 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -1,58 +1,58 @@ <script> -import jobComponent from './job_component.vue'; -import dropdownJobComponent from './dropdown_job_component.vue'; + import jobComponent from './job_component.vue'; + import dropdownJobComponent from './dropdown_job_component.vue'; -export default { - props: { - title: { - type: String, - required: true, + export default { + components: { + jobComponent, + dropdownJobComponent, }, - jobs: { - type: Array, - required: true, - }, - - isFirstColumn: { - type: Boolean, - required: false, - default: false, - }, + props: { + title: { + type: String, + required: true, + }, - stageConnectorClass: { - type: String, - required: false, - default: '', - }, - }, + jobs: { + type: Array, + required: true, + }, - components: { - jobComponent, - dropdownJobComponent, - }, + isFirstColumn: { + type: Boolean, + required: false, + default: false, + }, - methods: { - firstJob(list) { - return list[0]; + stageConnectorClass: { + type: String, + required: false, + default: '', + }, }, - jobId(job) { - return `ci-badge-${job.name}`; - }, + methods: { + firstJob(list) { + return list[0]; + }, + + jobId(job) { + return `ci-badge-${job.name}`; + }, - buildConnnectorClass(index) { - return index === 0 && !this.isFirstColumn ? 'left-connector' : ''; + buildConnnectorClass(index) { + return index === 0 && !this.isFirstColumn ? 'left-connector' : ''; + }, }, - }, -}; + }; </script> <template> <li class="stage-column" :class="stageConnectorClass"> <div class="stage-name"> - {{title}} + {{ title }} </div> <div class="builds-container"> <ul> @@ -61,7 +61,8 @@ export default { :key="job.id" class="build" :class="buildConnnectorClass(index)" - :id="jobId(job)"> + :id="jobId(job)" + > <div class="curve"></div> @@ -69,12 +70,12 @@ export default { v-if="job.size === 1" :job="job" css-class-job-name="build-content" - /> + /> <dropdown-job-component v-if="job.size > 1" :job="job" - /> + /> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index 2a1ecac3707..942acc8c412 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -1,82 +1,81 @@ <script> -import ciHeader from '../../vue_shared/components/header_ci_component.vue'; -import eventHub from '../event_hub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import ciHeader from '../../vue_shared/components/header_ci_component.vue'; + import eventHub from '../event_hub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -export default { - name: 'PipelineHeaderSection', - props: { - pipeline: { - type: Object, - required: true, + export default { + name: 'PipelineHeaderSection', + components: { + ciHeader, + loadingIcon, }, - isLoading: { - type: Boolean, - required: true, + props: { + pipeline: { + type: Object, + required: true, + }, + isLoading: { + type: Boolean, + required: true, + }, }, - }, - components: { - ciHeader, - loadingIcon, - }, - - data() { - return { - actions: this.getActions(), - }; - }, - - computed: { - status() { - return this.pipeline.details && this.pipeline.details.status; + data() { + return { + actions: this.getActions(), + }; }, - shouldRenderContent() { - return !this.isLoading && Object.keys(this.pipeline).length; + + computed: { + status() { + return this.pipeline.details && this.pipeline.details.status; + }, + shouldRenderContent() { + return !this.isLoading && Object.keys(this.pipeline).length; + }, }, - }, - methods: { - postAction(action) { - const index = this.actions.indexOf(action); + watch: { + pipeline() { + this.actions = this.getActions(); + }, + }, - this.$set(this.actions[index], 'isLoading', true); + methods: { + postAction(action) { + const index = this.actions.indexOf(action); - eventHub.$emit('headerPostAction', action); - }, + this.$set(this.actions[index], 'isLoading', true); - getActions() { - const actions = []; + eventHub.$emit('headerPostAction', action); + }, - if (this.pipeline.retry_path) { - actions.push({ - label: 'Retry', - path: this.pipeline.retry_path, - cssClass: 'js-retry-button btn btn-inverted-secondary', - type: 'button', - isLoading: false, - }); - } + getActions() { + const actions = []; - if (this.pipeline.cancel_path) { - actions.push({ - label: 'Cancel running', - path: this.pipeline.cancel_path, - cssClass: 'js-btn-cancel-pipeline btn btn-danger', - type: 'button', - isLoading: false, - }); - } + if (this.pipeline.retry_path) { + actions.push({ + label: 'Retry', + path: this.pipeline.retry_path, + cssClass: 'js-retry-button btn btn-inverted-secondary', + type: 'button', + isLoading: false, + }); + } - return actions; - }, - }, + if (this.pipeline.cancel_path) { + actions.push({ + label: 'Cancel running', + path: this.pipeline.cancel_path, + cssClass: 'js-btn-cancel-pipeline btn btn-danger', + type: 'button', + isLoading: false, + }); + } - watch: { - pipeline() { - this.actions = this.getActions(); + return actions; + }, }, - }, -}; + }; </script> <template> <div class="pipeline-header-container"> @@ -89,9 +88,10 @@ export default { :user="pipeline.user" :actions="actions" @actionClicked="postAction" - /> + /> <loading-icon v-if="isLoading" - size="2"/> + size="2" + /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 9da0aac50a1..ceb4d9ca604 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -4,6 +4,13 @@ import popover from '../../vue_shared/directives/popover'; export default { + components: { + userAvatarLink, + }, + directives: { + tooltip, + popover, + }, props: { pipeline: { type: Object, @@ -14,13 +21,6 @@ required: true, }, }, - components: { - userAvatarLink, - }, - directives: { - tooltip, - popover, - }, computed: { user() { return this.pipeline.user; @@ -30,8 +30,16 @@ html: true, trigger: 'focus', placement: 'top', - title: '<div class="autodevops-title">This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b></div>', - content: `<a class="autodevops-link" href="${this.autoDevopsHelpPath}" target="_blank" rel="noopener noreferrer nofollow">Learn more about Auto DevOps</a>`, + title: `<div class="autodevops-title"> + This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b> + </div>`, + content: `<a + class="autodevops-link" + href="${this.autoDevopsHelpPath}" + target="_blank" + rel="noopener noreferrer nofollow"> + Learn more about Auto DevOps + </a>`, }; }, }, @@ -42,7 +50,7 @@ <a :href="pipeline.path" class="js-pipeline-url-link"> - <span class="pipeline-id">#{{pipeline.id}}</span> + <span class="pipeline-id">#{{ pipeline.id }}</span> </a> <span>by</span> <user-avatar-link diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index 8fa416168e7..90930d5ff44 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -13,6 +13,15 @@ import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; export default { + components: { + tablePagination, + navigationTabs, + navigationControls, + }, + mixins: [ + pipelinesMixin, + CIPaginationMixin, + ], props: { store: { type: Object, @@ -28,15 +37,6 @@ default: 'root', }, }, - components: { - tablePagination, - navigationTabs, - navigationControls, - }, - mixins: [ - pipelinesMixin, - CIPaginationMixin, - ], data() { const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; @@ -197,7 +197,8 @@ <div class="pipelines-container"> <div class="top-area scrolling-tabs-container inner-page-scroll-tabs" - v-if="!shouldRenderEmptyState"> + v-if="!shouldRenderEmptyState" + > <div class="fade-left"> <i class="fa fa-angle-left" @@ -215,16 +216,16 @@ :tabs="tabs" @onChangeTab="onChangeTab" scope="pipelines" - /> + /> <navigation-controls :new-pipeline-path="newPipelinePath" :has-ci-enabled="hasCiEnabled" :help-page-path="helpPagePath" - :resetCachePath="resetCachePath" + :reset-cache-path="resetCachePath" :ci-lint-path="ciLintPath" :can-create-pipeline="canCreatePipelineParsed " - /> + /> </div> <div class="content-list pipelines"> @@ -234,22 +235,23 @@ size="3" v-if="isLoading" class="prepend-top-20" - /> + /> <empty-state v-if="shouldRenderEmptyState" :help-page-path="helpPagePath" :empty-state-svg-path="emptyStateSvgPath" - /> + /> <error-state v-if="shouldRenderErrorState" :error-state-svg-path="errorStateSvgPath" - /> + /> <div class="blank-state-row" - v-if="shouldRenderNoPipelinesMessage"> + v-if="shouldRenderNoPipelinesMessage" + > <div class="blank-state-center"> <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2> </div> @@ -257,21 +259,22 @@ <div class="table-holder" - v-if="shouldRenderTable"> + v-if="shouldRenderTable" + > <pipelines-table-component :pipelines="state.pipelines" :update-graph-dropdown="updateGraphDropdown" :auto-devops-help-path="autoDevopsPath" :view-type="viewType" - /> + /> </div> <table-pagination v-if="shouldRenderPagination" :change="onChangePage" :page-info="state.pageInfo" - /> + /> </div> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index f3c0aca17ba..efda36c12d6 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -5,18 +5,18 @@ import tooltip from '../../vue_shared/directives/tooltip'; export default { - props: { - actions: { - type: Array, - required: true, - }, - }, directives: { tooltip, }, components: { loadingIcon, }, + props: { + actions: { + type: Array, + required: true, + }, + }, data() { return { playIconSvg, @@ -50,7 +50,8 @@ data-toggle="dropdown" data-placement="top" aria-label="Manual job" - :disabled="isLoading"> + :disabled="isLoading" + > <span v-html="playIconSvg"></span> <i class="fa fa-caret-down" @@ -60,14 +61,18 @@ </button> <ul class="dropdown-menu dropdown-menu-align-right"> - <li v-for="action in actions"> + <li + v-for="(action, i) in actions" + :key="i" + > <button type="button" class="js-pipeline-action-link no-btn btn" @click="onClickAction(action.path)" :class="{ disabled: isActionDisabled(action) }" - :disabled="isActionDisabled(action)"> - {{action.name}} + :disabled="isActionDisabled(action)" + > + {{ action.name }} </button> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue index 831aa92ac4f..1b9e0f917a4 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue @@ -3,46 +3,50 @@ import icon from '../../vue_shared/components/icon.vue'; export default { - props: { - artifacts: { - type: Array, - required: true, - }, - }, directives: { tooltip, }, components: { icon, }, + props: { + artifacts: { + type: Array, + required: true, + }, + }, }; </script> <template> <div class="btn-group" - role="group"> + role="group" + > <button v-tooltip class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download" title="Artifacts" data-placement="top" data-toggle="dropdown" - aria-label="Artifacts"> - <icon - name="download"> - </icon> + aria-label="Artifacts" + > + <icon name="download" /> <i class="fa fa-caret-down" - aria-hidden="true"> + aria-hidden="true" + > </i> </button> <ul class="dropdown-menu dropdown-menu-align-right"> - <li v-for="artifact in artifacts"> + <li + v-for="(artifact, i) in artifacts" + :key="i"> <a rel="nofollow" download - :href="artifact.path"> - Download {{artifact.name}} artifacts + :href="artifact.path" + > + Download {{ artifact.name }} artifacts </a> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 16a705cbaff..c6638cdcf1e 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -7,6 +7,9 @@ * Given an array of objects, renders a table. */ export default { + components: { + pipelinesTableRowComponent, + }, props: { pipelines: { type: Array, @@ -26,34 +29,36 @@ required: true, }, }, - components: { - pipelinesTableRowComponent, - }, }; </script> <template> <div class="ci-table"> <div class="gl-responsive-table-row table-row-header" - role="row"> + role="row" + > <div class="table-section section-10 js-pipeline-status pipeline-status" - role="rowheader"> + role="rowheader" + > Status </div> <div class="table-section section-15 js-pipeline-info pipeline-info" - role="rowheader"> + role="rowheader" + > Pipeline </div> <div class="table-section section-25 js-pipeline-commit pipeline-commit" - role="rowheader"> + role="rowheader" + > Commit </div> <div class="table-section section-15 js-pipeline-stages pipeline-stages" - role="rowheader"> + role="rowheader" + > Stages </div> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 33fbce993b2..670b777199c 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -1,227 +1,228 @@ <script> -/* eslint-disable no-param-reassign */ -import asyncButtonComponent from './async_button.vue'; -import pipelinesActionsComponent from './pipelines_actions.vue'; -import pipelinesArtifactsComponent from './pipelines_artifacts.vue'; -import ciBadge from '../../vue_shared/components/ci_badge_link.vue'; -import pipelineStage from './stage.vue'; -import pipelineUrl from './pipeline_url.vue'; -import pipelinesTimeago from './time_ago.vue'; -import commitComponent from '../../vue_shared/components/commit.vue'; + /* eslint-disable no-param-reassign */ + import asyncButtonComponent from './async_button.vue'; + import pipelinesActionsComponent from './pipelines_actions.vue'; + import pipelinesArtifactsComponent from './pipelines_artifacts.vue'; + import ciBadge from '../../vue_shared/components/ci_badge_link.vue'; + import pipelineStage from './stage.vue'; + import pipelineUrl from './pipeline_url.vue'; + import pipelinesTimeago from './time_ago.vue'; + import commitComponent from '../../vue_shared/components/commit.vue'; -/** - * Pipeline table row. - * - * Given the received object renders a table row in the pipelines' table. - */ -export default { - props: { - pipeline: { - type: Object, - required: true, + /** + * Pipeline table row. + * + * Given the received object renders a table row in the pipelines' table. + */ + export default { + components: { + asyncButtonComponent, + pipelinesActionsComponent, + pipelinesArtifactsComponent, + commitComponent, + pipelineStage, + pipelineUrl, + ciBadge, + pipelinesTimeago, }, - updateGraphDropdown: { - type: Boolean, - required: false, - default: false, + props: { + pipeline: { + type: Object, + required: true, + }, + updateGraphDropdown: { + type: Boolean, + required: false, + default: false, + }, + autoDevopsHelpPath: { + type: String, + required: true, + }, + viewType: { + type: String, + required: true, + }, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, - viewType: { - type: String, - required: true, - }, - }, - components: { - asyncButtonComponent, - pipelinesActionsComponent, - pipelinesArtifactsComponent, - commitComponent, - pipelineStage, - pipelineUrl, - ciBadge, - pipelinesTimeago, - }, - computed: { - /** - * If provided, returns the commit tag. - * Needed to render the commit component column. - * - * This field needs a lot of verification, because of different possible cases: - * - * 1. person who is an author of a commit might be a GitLab user - * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar - * 3. If GitLab user does not have avatar he/she might have a Gravatar - * 4. If committer is not a GitLab User he/she can have a Gravatar - * 5. We do not have consistent API object in this case - * 6. We should improve API and the code - * - * @returns {Object|Undefined} - */ - commitAuthor() { - let commitAuthorInformation; + computed: { + /** + * If provided, returns the commit tag. + * Needed to render the commit component column. + * + * This field needs a lot of verification, because of different possible cases: + * + * 1. person who is an author of a commit might be a GitLab user + * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar + * 3. If GitLab user does not have avatar he/she might have a Gravatar + * 4. If committer is not a GitLab User he/she can have a Gravatar + * 5. We do not have consistent API object in this case + * 6. We should improve API and the code + * + * @returns {Object|Undefined} + */ + commitAuthor() { + let commitAuthorInformation; - if (!this.pipeline || !this.pipeline.commit) { - return null; - } + if (!this.pipeline || !this.pipeline.commit) { + return null; + } - // 1. person who is an author of a commit might be a GitLab user - if (this.pipeline.commit.author) { - // 2. if person who is an author of a commit is a GitLab user - // he/she can have a GitLab avatar - if (this.pipeline.commit.author.avatar_url) { - commitAuthorInformation = this.pipeline.commit.author; + // 1. person who is an author of a commit might be a GitLab user + if (this.pipeline.commit.author) { + // 2. if person who is an author of a commit is a GitLab user + // he/she can have a GitLab avatar + if (this.pipeline.commit.author.avatar_url) { + commitAuthorInformation = this.pipeline.commit.author; - // 3. If GitLab user does not have avatar he/she might have a Gravatar - } else if (this.pipeline.commit.author_gravatar_url) { - commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, { + // 3. If GitLab user does not have avatar he/she might have a Gravatar + } else if (this.pipeline.commit.author_gravatar_url) { + commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, { + avatar_url: this.pipeline.commit.author_gravatar_url, + }); + } + // 4. If committer is not a GitLab User he/she can have a Gravatar + } else { + commitAuthorInformation = { avatar_url: this.pipeline.commit.author_gravatar_url, - }); + path: `mailto:${this.pipeline.commit.author_email}`, + username: this.pipeline.commit.author_name, + }; } - // 4. If committer is not a GitLab User he/she can have a Gravatar - } else { - commitAuthorInformation = { - avatar_url: this.pipeline.commit.author_gravatar_url, - path: `mailto:${this.pipeline.commit.author_email}`, - username: this.pipeline.commit.author_name, - }; - } - return commitAuthorInformation; - }, + return commitAuthorInformation; + }, - /** - * If provided, returns the commit tag. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ - commitTag() { - if (this.pipeline.ref && - this.pipeline.ref.tag) { - return this.pipeline.ref.tag; - } - return undefined; - }, + /** + * If provided, returns the commit tag. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitTag() { + if (this.pipeline.ref && + this.pipeline.ref.tag) { + return this.pipeline.ref.tag; + } + return undefined; + }, - /** - * If provided, returns the commit ref. - * Needed to render the commit component column. - * - * Matches `path` prop sent in the API to `ref_url` prop needed - * in the commit component. - * - * @returns {Object|Undefined} - */ - commitRef() { - if (this.pipeline.ref) { - return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { - if (prop === 'path') { - accumulator.ref_url = this.pipeline.ref[prop]; - } else { - accumulator[prop] = this.pipeline.ref[prop]; - } - return accumulator; - }, {}); - } + /** + * If provided, returns the commit ref. + * Needed to render the commit component column. + * + * Matches `path` prop sent in the API to `ref_url` prop needed + * in the commit component. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.pipeline.ref) { + return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { + if (prop === 'path') { + accumulator.ref_url = this.pipeline.ref[prop]; + } else { + accumulator[prop] = this.pipeline.ref[prop]; + } + return accumulator; + }, {}); + } - return undefined; - }, + return undefined; + }, - /** - * If provided, returns the commit url. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ - commitUrl() { - if (this.pipeline.commit && - this.pipeline.commit.commit_path) { - return this.pipeline.commit.commit_path; - } - return undefined; - }, + /** + * If provided, returns the commit url. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitUrl() { + if (this.pipeline.commit && + this.pipeline.commit.commit_path) { + return this.pipeline.commit.commit_path; + } + return undefined; + }, - /** - * If provided, returns the commit short sha. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ - commitShortSha() { - if (this.pipeline.commit && - this.pipeline.commit.short_id) { - return this.pipeline.commit.short_id; - } - return undefined; - }, + /** + * If provided, returns the commit short sha. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if (this.pipeline.commit && + this.pipeline.commit.short_id) { + return this.pipeline.commit.short_id; + } + return undefined; + }, - /** - * If provided, returns the commit title. - * Needed to render the commit component column. - * - * @returns {String|Undefined} - */ - commitTitle() { - if (this.pipeline.commit && - this.pipeline.commit.title) { - return this.pipeline.commit.title; - } - return undefined; - }, + /** + * If provided, returns the commit title. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitTitle() { + if (this.pipeline.commit && + this.pipeline.commit.title) { + return this.pipeline.commit.title; + } + return undefined; + }, - /** - * Timeago components expects a number - * - * @return {type} description - */ - pipelineDuration() { - if (this.pipeline.details && this.pipeline.details.duration) { - return this.pipeline.details.duration; - } + /** + * Timeago components expects a number + * + * @return {type} description + */ + pipelineDuration() { + if (this.pipeline.details && this.pipeline.details.duration) { + return this.pipeline.details.duration; + } - return 0; - }, + return 0; + }, - /** - * Timeago component expects a String. - * - * @return {String} - */ - pipelineFinishedAt() { - if (this.pipeline.details && this.pipeline.details.finished_at) { - return this.pipeline.details.finished_at; - } + /** + * Timeago component expects a String. + * + * @return {String} + */ + pipelineFinishedAt() { + if (this.pipeline.details && this.pipeline.details.finished_at) { + return this.pipeline.details.finished_at; + } - return ''; - }, + return ''; + }, - pipelineStatus() { - if (this.pipeline.details && this.pipeline.details.status) { - return this.pipeline.details.status; - } - return {}; - }, + pipelineStatus() { + if (this.pipeline.details && this.pipeline.details.status) { + return this.pipeline.details.status; + } + return {}; + }, - displayPipelineActions() { - return this.pipeline.flags.retryable || - this.pipeline.flags.cancelable || - this.pipeline.details.manual_actions.length || - this.pipeline.details.artifacts.length; - }, + displayPipelineActions() { + return this.pipeline.flags.retryable || + this.pipeline.flags.cancelable || + this.pipeline.details.manual_actions.length || + this.pipeline.details.artifacts.length; + }, - isChildView() { - return this.viewType === 'child'; + isChildView() { + return this.viewType === 'child'; + }, }, - }, -}; + }; </script> <template> <div class="commit gl-responsive-table-row"> <div class="table-section section-10 commit-link"> - <div class="table-mobile-header" + <div + class="table-mobile-header" role="rowheader"> Status </div> @@ -229,14 +230,14 @@ export default { <ci-badge :status="pipelineStatus" :show-text="!isChildView" - /> + /> </div> </div> <pipeline-url :pipeline="pipeline" :auto-devops-help-path="autoDevopsHelpPath" - /> + /> <div class="table-section section-25"> <div @@ -253,7 +254,7 @@ export default { :title="commitTitle" :author="commitAuthor" :show-branch="!isChildView" - /> + /> </div> </div> @@ -264,21 +265,24 @@ export default { Stages </div> <div class="table-mobile-content"> - <div class="stage-container dropdown js-mini-pipeline-graph" - v-if="pipeline.details.stages.length > 0" - v-for="stage in pipeline.details.stages"> - <pipeline-stage - :stage="stage" - :update-dropdown="updateGraphDropdown" + <template v-if="pipeline.details.stages.length > 0"> + <div + class="stage-container dropdown js-mini-pipeline-graph" + v-for="(stage, index) in pipeline.details.stages" + :key="index"> + <pipeline-stage + :stage="stage" + :update-dropdown="updateGraphDropdown" /> - </div> + </div> + </template> </div> </div> <pipelines-timeago :duration="pipelineDuration" :finished-time="pipelineFinishedAt" - /> + /> <div v-if="displayPipelineActions" @@ -287,13 +291,13 @@ export default { <pipelines-actions-component v-if="pipeline.details.manual_actions.length" :actions="pipeline.details.manual_actions" - /> + /> <pipelines-artifacts-component v-if="pipeline.details.artifacts.length" class="hidden-xs hidden-sm" :artifacts="pipeline.details.artifacts" - /> + /> <async-button-component v-if="pipeline.flags.retryable" @@ -301,7 +305,7 @@ export default { css-class="js-pipelines-retry-button btn-default btn-retry" title="Retry" icon="repeat" - /> + /> <async-button-component v-if="pipeline.flags.cancelable" @@ -310,7 +314,7 @@ export default { title="Cancel" icon="remove" confirm-action-message="Are you sure you want to cancel this pipeline?" - /> + /> </div> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index ac9d9c901ca..58806aa114a 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -1,133 +1,135 @@ <script> -/** - * Renders each stage of the pipeline mini graph. - * - * Given the provided endpoint will make a request to - * fetch the dropdown data when the stage is clicked. - * - * Request is made inside this component to make it reusable between: - * 1. Pipelines main table - * 2. Pipelines table in commit and Merge request views - * 3. Merge request widget - * 4. Commit widget - */ - -import Flash from '../../flash'; -import icon from '../../vue_shared/components/icon.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; - -export default { - props: { - stage: { - type: Object, - required: true, + /** + * Renders each stage of the pipeline mini graph. + * + * Given the provided endpoint will make a request to + * fetch the dropdown data when the stage is clicked. + * + * Request is made inside this component to make it reusable between: + * 1. Pipelines main table + * 2. Pipelines table in commit and Merge request views + * 3. Merge request widget + * 4. Commit widget + */ + + import Flash from '../../flash'; + import icon from '../../vue_shared/components/icon.vue'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; + + export default { + components: { + loadingIcon, + icon, }, - updateDropdown: { - type: Boolean, - required: false, - default: false, + directives: { + tooltip, }, - }, - - directives: { - tooltip, - }, - - data() { - return { - isLoading: false, - dropdownContent: '', - }; - }, - - components: { - loadingIcon, - icon, - }, - - updated() { - if (this.dropdownContent.length > 0) { - this.stopDropdownClickPropagation(); - } - }, - - watch: { - updateDropdown() { - if (this.updateDropdown && - this.isDropdownOpen() && - !this.isLoading) { - this.fetchJobs(); - } - }, - }, - methods: { - onClickStage() { - if (!this.isDropdownOpen()) { - this.isLoading = true; - this.fetchJobs(); - } + props: { + stage: { + type: Object, + required: true, + }, + + updateDropdown: { + type: Boolean, + required: false, + default: false, + }, }, - fetchJobs() { - this.$http.get(this.stage.dropdown_path) - .then(response => response.json()) - .then((data) => { - this.dropdownContent = data.html; - this.isLoading = false; - }) - .catch(() => { - this.closeDropdown(); - this.isLoading = false; - - const flash = new Flash('Something went wrong on our end.'); - return flash; - }); + data() { + return { + isLoading: false, + dropdownContent: '', + }; }, - /** - * When the user right clicks or cmd/ctrl + click in the job name - * the dropdown should not be closed and the link should open in another tab, - * so we stop propagation of the click event inside the dropdown. - * - * Since this component is rendered multiple times per page we need to guarantee we only - * target the click event of this component. - */ - stopDropdownClickPropagation() { - $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')) - .on('click', (e) => { - e.stopPropagation(); - }); - }, + computed: { + dropdownClass() { + return this.dropdownContent.length > 0 ? + 'js-builds-dropdown-container' : + 'js-builds-dropdown-loading'; + }, - closeDropdown() { - if (this.isDropdownOpen()) { - $(this.$refs.dropdown).dropdown('toggle'); - } - }, + triggerButtonClass() { + return `ci-status-icon-${this.stage.status.group}`; + }, - isDropdownOpen() { - return this.$el.classList.contains('open'); + borderlessIcon() { + return `${this.stage.status.icon}_borderless`; + }, }, - }, - computed: { - dropdownClass() { - return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading'; + watch: { + updateDropdown() { + if (this.updateDropdown && + this.isDropdownOpen() && + !this.isLoading) { + this.fetchJobs(); + } + }, }, - triggerButtonClass() { - return `ci-status-icon-${this.stage.status.group}`; + updated() { + if (this.dropdownContent.length > 0) { + this.stopDropdownClickPropagation(); + } }, - borderlessIcon() { - return `${this.stage.status.icon}_borderless`; + methods: { + onClickStage() { + if (!this.isDropdownOpen()) { + this.isLoading = true; + this.fetchJobs(); + } + }, + + fetchJobs() { + this.$http.get(this.stage.dropdown_path) + .then(response => response.json()) + .then((data) => { + this.dropdownContent = data.html; + this.isLoading = false; + }) + .catch(() => { + this.closeDropdown(); + this.isLoading = false; + + const flash = new Flash('Something went wrong on our end.'); + return flash; + }); + }, + + /** + * When the user right clicks or cmd/ctrl + click in the job name + * the dropdown should not be closed and the link should open in another tab, + * so we stop propagation of the click event inside the dropdown. + * + * Since this component is rendered multiple times per page we need to guarantee we only + * target the click event of this component. + */ + stopDropdownClickPropagation() { + $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')) + .on('click', (e) => { + e.stopPropagation(); + }); + }, + + closeDropdown() { + if (this.isDropdownOpen()) { + $(this.$refs.dropdown).dropdown('toggle'); + } + }, + + isDropdownOpen() { + return this.$el.classList.contains('open'); + }, }, - }, -}; + }; </script> <template> @@ -143,36 +145,41 @@ export default { type="button" id="stageDropdown" aria-haspopup="true" - aria-expanded="false"> + aria-expanded="false" + > <span aria-hidden="true" - :aria-label="stage.title"> - <icon - :name="borderlessIcon"/> + :aria-label="stage.title" + > + <icon :name="borderlessIcon" /> </span> <i class="fa fa-caret-down" - aria-hidden="true"> + aria-hidden="true" + > </i> </button> <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container" - aria-labelledby="stageDropdown"> + aria-labelledby="stageDropdown" + > <li :class="dropdownClass" - class="js-builds-dropdown-list scrollable-menu"> + class="js-builds-dropdown-list scrollable-menu" + > <loading-icon v-if="isLoading"/> <ul v-else - v-html="dropdownContent"> + v-html="dropdownContent" + > </ul> </li> </ul> </div> -</script> +</template> diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue index 037684b4e72..cd54d26c9d3 100644 --- a/app/assets/javascripts/pipelines/components/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/time_ago.vue @@ -5,6 +5,12 @@ import timeagoMixin from '../../vue_shared/mixins/timeago'; export default { + directives: { + tooltip, + }, + mixins: [ + timeagoMixin, + ], props: { finishedTime: { type: String, @@ -15,12 +21,6 @@ required: true, }, }, - mixins: [ - timeagoMixin, - ], - directives: { - tooltip, - }, data() { return { iconTimerSvg, @@ -60,26 +60,29 @@ <div class="table-section section-15 pipelines-time-ago"> <div class="table-mobile-header" - role="rowheader"> + role="rowheader" + > Duration </div> <div class="table-mobile-content"> <p class="duration" - v-if="hasDuration"> - <span - v-html="iconTimerSvg"> + v-if="hasDuration" + > + <span v-html="iconTimerSvg"> </span> - {{durationFormated}} + {{ durationFormated }} </p> <p class="finished-at hidden-xs hidden-sm" - v-if="hasFinishedTime"> + v-if="hasFinishedTime" + > <i class="fa fa-calendar" - aria-hidden="true"> + aria-hidden="true" + > </i> <time @@ -87,9 +90,9 @@ data-placement="top" data-container="body" :title="tooltipTitle(finishedTime)"> - {{timeFormated(finishedTime)}} + {{ timeFormated(finishedTime) }} </time> </p> </div> </div> -</script> +</template> diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 206023d4ddb..d88d280cb3f 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -15,14 +15,14 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line new Vue({ el: '#js-pipeline-graph-vue', + components: { + pipelineGraph, + }, data() { return { mediator, }; }, - components: { - pipelineGraph, - }, render(createElement) { return createElement('pipeline-graph', { props: { @@ -36,14 +36,14 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line new Vue({ el: '#js-pipeline-header-vue', + components: { + pipelineHeader, + }, data() { return { mediator, }; }, - components: { - pipelineHeader, - }, created() { eventHub.$on('headerPostAction', this.postAction); }, diff --git a/app/assets/javascripts/pipelines/pipelines_bundle.js b/app/assets/javascripts/pipelines/pipelines_bundle.js index 3e4b6eeb5bf..ab5596e70f0 100644 --- a/app/assets/javascripts/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/pipelines/pipelines_bundle.js @@ -7,6 +7,9 @@ Vue.use(Translate); document.addEventListener('DOMContentLoaded', () => new Vue({ el: '#pipelines-list-vue', + components: { + pipelinesComponent, + }, data() { const store = new PipelinesStore(); @@ -14,9 +17,6 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ store, }; }, - components: { - pipelinesComponent, - }, render(createElement) { return createElement('pipelines-component', { props: { diff --git a/app/assets/javascripts/pipelines/pipelines_charts.js b/app/assets/javascripts/pipelines/pipelines_charts.js index 001faf4be33..821aa7e229f 100644 --- a/app/assets/javascripts/pipelines/pipelines_charts.js +++ b/app/assets/javascripts/pipelines/pipelines_charts.js @@ -6,16 +6,16 @@ document.addEventListener('DOMContentLoaded', () => { const data = { labels: chartScope.labels, datasets: [{ - fillColor: '#7f8fa4', - strokeColor: '#7f8fa4', - pointColor: '#7f8fa4', + fillColor: '#707070', + strokeColor: '#707070', + pointColor: '#707070', pointStrokeColor: '#EEE', data: chartScope.totalValues, }, { - fillColor: '#44aa22', - strokeColor: '#44aa22', - pointColor: '#44aa22', + fillColor: '#1aaa55', + strokeColor: '#1aaa55', + pointColor: '#1aaa55', pointStrokeColor: '#fff', data: chartScope.successValues, }, diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index ffaafb3ee9e..86c7b56198d 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -6,195 +6,193 @@ // (including the explanation of quick actions), and showing a warning when // more than `x` users are referenced. // -(function () { - var lastTextareaPreviewed; - var lastTextareaHeight = null; - var markdownPreview; - var previewButtonSelector; - var writeButtonSelector; - - window.MarkdownPreview = (function () { - function MarkdownPreview() {} - - // Minimum number of users referenced before triggering a warning - MarkdownPreview.prototype.referenceThreshold = 10; - MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.'; - - MarkdownPreview.prototype.ajaxCache = {}; - - MarkdownPreview.prototype.showPreview = function ($form) { - var mdText; - var preview = $form.find('.js-md-preview'); - var url = preview.data('url'); - if (preview.hasClass('md-preview-loading')) { - return; - } - mdText = $form.find('textarea.markdown-area').val(); - - if (mdText.trim().length === 0) { - preview.text(this.emptyMessage); - this.hideReferencedUsers($form); - } else { - preview.addClass('md-preview-loading').text('Loading...'); - this.fetchMarkdownPreview(mdText, url, (function (response) { - var body; - if (response.body.length > 0) { - body = response.body; - } else { - body = this.emptyMessage; - } - - preview.removeClass('md-preview-loading').html(body); - preview.renderGFM(); - this.renderReferencedUsers(response.references.users, $form); - - if (response.references.commands) { - this.renderReferencedCommands(response.references.commands, $form); - } - }).bind(this)); - } - }; - MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) { - if (!url) { - return; - } - if (text === this.ajaxCache.text) { - success(this.ajaxCache.response); - return; - } - $.ajax({ - type: 'POST', - url: url, - data: { - text: text - }, - dataType: 'json', - success: (function (response) { - this.ajaxCache = { - text: text, - response: response - }; - success(response); - }).bind(this) - }); - }; - - MarkdownPreview.prototype.hideReferencedUsers = function ($form) { - $form.find('.referenced-users').hide(); - }; - - MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) { - var referencedUsers; - referencedUsers = $form.find('.referenced-users'); - if (referencedUsers.length) { - if (users.length >= this.referenceThreshold) { - referencedUsers.show(); - referencedUsers.find('.js-referenced-users-count').text(users.length); - } else { - referencedUsers.hide(); - } - } - }; - - MarkdownPreview.prototype.hideReferencedCommands = function ($form) { - $form.find('.referenced-commands').hide(); - }; - - MarkdownPreview.prototype.renderReferencedCommands = function (commands, $form) { - var referencedCommands; - referencedCommands = $form.find('.referenced-commands'); - if (commands.length > 0) { - referencedCommands.html(commands); - referencedCommands.show(); +var lastTextareaPreviewed; +var lastTextareaHeight = null; +var markdownPreview; +var previewButtonSelector; +var writeButtonSelector; + +function MarkdownPreview() {} + +// Minimum number of users referenced before triggering a warning +MarkdownPreview.prototype.referenceThreshold = 10; +MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.'; + +MarkdownPreview.prototype.ajaxCache = {}; + +MarkdownPreview.prototype.showPreview = function ($form) { + var mdText; + var preview = $form.find('.js-md-preview'); + var url = preview.data('url'); + if (preview.hasClass('md-preview-loading')) { + return; + } + mdText = $form.find('textarea.markdown-area').val(); + + if (mdText.trim().length === 0) { + preview.text(this.emptyMessage); + this.hideReferencedUsers($form); + } else { + preview.addClass('md-preview-loading').text('Loading...'); + this.fetchMarkdownPreview(mdText, url, (function (response) { + var body; + if (response.body.length > 0) { + body = response.body; } else { - referencedCommands.html(''); - referencedCommands.hide(); + body = this.emptyMessage; } - }; - - return MarkdownPreview; - }()); - - markdownPreview = new window.MarkdownPreview(); - previewButtonSelector = '.js-md-preview-button'; - writeButtonSelector = '.js-md-write-button'; - lastTextareaPreviewed = null; - const markdownToolbar = $('.md-header-toolbar'); - - $.fn.setupMarkdownPreview = function () { - var $form = $(this); - $form.find('textarea.markdown-area').on('input', function () { - markdownPreview.hideReferencedUsers($form); - }); - }; - - $(document).on('markdown-preview:show', function (e, $form) { - if (!$form) { - return; - } - - lastTextareaPreviewed = $form.find('textarea.markdown-area'); - lastTextareaHeight = lastTextareaPreviewed.height(); - - // toggle tabs - $form.find(writeButtonSelector).parent().removeClass('active'); - $form.find(previewButtonSelector).parent().addClass('active'); - // toggle content - $form.find('.md-write-holder').hide(); - $form.find('.md-preview-holder').show(); - markdownToolbar.removeClass('active'); - markdownPreview.showPreview($form); - }); - - $(document).on('markdown-preview:hide', function (e, $form) { - if (!$form) { - return; - } - lastTextareaPreviewed = null; - - if (lastTextareaHeight) { - $form.find('textarea.markdown-area').height(lastTextareaHeight); - } - - // toggle tabs - $form.find(writeButtonSelector).parent().addClass('active'); - $form.find(previewButtonSelector).parent().removeClass('active'); - - // toggle content - $form.find('.md-write-holder').show(); - $form.find('textarea.markdown-area').focus(); - $form.find('.md-preview-holder').hide(); - markdownToolbar.addClass('active'); + preview.removeClass('md-preview-loading').html(body); + preview.renderGFM(); + this.renderReferencedUsers(response.references.users, $form); - markdownPreview.hideReferencedCommands($form); + if (response.references.commands) { + this.renderReferencedCommands(response.references.commands, $form); + } + }).bind(this)); + } +}; + +MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) { + if (!url) { + return; + } + if (text === this.ajaxCache.text) { + success(this.ajaxCache.response); + return; + } + $.ajax({ + type: 'POST', + url: url, + data: { + text: text + }, + dataType: 'json', + success: (function (response) { + this.ajaxCache = { + text: text, + response: response + }; + success(response); + }).bind(this) }); - - $(document).on('markdown-preview:toggle', function (e, keyboardEvent) { - var $target; - $target = $(keyboardEvent.target); - if ($target.is('textarea.markdown-area')) { - $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]); - keyboardEvent.preventDefault(); - } else if (lastTextareaPreviewed) { - $target = lastTextareaPreviewed; - $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]); - keyboardEvent.preventDefault(); +}; + +MarkdownPreview.prototype.hideReferencedUsers = function ($form) { + $form.find('.referenced-users').hide(); +}; + +MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) { + var referencedUsers; + referencedUsers = $form.find('.referenced-users'); + if (referencedUsers.length) { + if (users.length >= this.referenceThreshold) { + referencedUsers.show(); + referencedUsers.find('.js-referenced-users-count').text(users.length); + } else { + referencedUsers.hide(); } + } +}; + +MarkdownPreview.prototype.hideReferencedCommands = function ($form) { + $form.find('.referenced-commands').hide(); +}; + +MarkdownPreview.prototype.renderReferencedCommands = function (commands, $form) { + var referencedCommands; + referencedCommands = $form.find('.referenced-commands'); + if (commands.length > 0) { + referencedCommands.html(commands); + referencedCommands.show(); + } else { + referencedCommands.html(''); + referencedCommands.hide(); + } +}; + +markdownPreview = new MarkdownPreview(); + +previewButtonSelector = '.js-md-preview-button'; +writeButtonSelector = '.js-md-write-button'; +lastTextareaPreviewed = null; +const markdownToolbar = $('.md-header-toolbar'); + +$.fn.setupMarkdownPreview = function () { + var $form = $(this); + $form.find('textarea.markdown-area').on('input', function () { + markdownPreview.hideReferencedUsers($form); }); +}; + +$(document).on('markdown-preview:show', function (e, $form) { + if (!$form) { + return; + } + + lastTextareaPreviewed = $form.find('textarea.markdown-area'); + lastTextareaHeight = lastTextareaPreviewed.height(); + + // toggle tabs + $form.find(writeButtonSelector).parent().removeClass('active'); + $form.find(previewButtonSelector).parent().addClass('active'); + + // toggle content + $form.find('.md-write-holder').hide(); + $form.find('.md-preview-holder').show(); + markdownToolbar.removeClass('active'); + markdownPreview.showPreview($form); +}); + +$(document).on('markdown-preview:hide', function (e, $form) { + if (!$form) { + return; + } + lastTextareaPreviewed = null; - $(document).on('click', previewButtonSelector, function (e) { - var $form; - e.preventDefault(); - $form = $(this).closest('form'); - $(document).triggerHandler('markdown-preview:show', [$form]); - }); - - $(document).on('click', writeButtonSelector, function (e) { - var $form; - e.preventDefault(); - $form = $(this).closest('form'); - $(document).triggerHandler('markdown-preview:hide', [$form]); - }); -}()); + if (lastTextareaHeight) { + $form.find('textarea.markdown-area').height(lastTextareaHeight); + } + + // toggle tabs + $form.find(writeButtonSelector).parent().addClass('active'); + $form.find(previewButtonSelector).parent().removeClass('active'); + + // toggle content + $form.find('.md-write-holder').show(); + $form.find('textarea.markdown-area').focus(); + $form.find('.md-preview-holder').hide(); + markdownToolbar.addClass('active'); + + markdownPreview.hideReferencedCommands($form); +}); + +$(document).on('markdown-preview:toggle', function (e, keyboardEvent) { + var $target; + $target = $(keyboardEvent.target); + if ($target.is('textarea.markdown-area')) { + $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]); + keyboardEvent.preventDefault(); + } else if (lastTextareaPreviewed) { + $target = lastTextareaPreviewed; + $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]); + keyboardEvent.preventDefault(); + } +}); + +$(document).on('click', previewButtonSelector, function (e) { + var $form; + e.preventDefault(); + $form = $(this).closest('form'); + $(document).triggerHandler('markdown-preview:show', [$form]); +}); + +$(document).on('click', writeButtonSelector, function (e) { + var $form; + e.preventDefault(); + $form = $(this).closest('form'); + $(document).triggerHandler('markdown-preview:hide', [$form]); +}); + +export default MarkdownPreview; diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index 36ad618aa46..1ffe482d782 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -4,6 +4,9 @@ import csrf from '~/lib/utils/csrf'; export default { + components: { + modal, + }, props: { actionUrl: { type: String, @@ -24,9 +27,6 @@ enteredUsername: '', }; }, - components: { - modal, - }, computed: { csrfToken() { return csrf.token; @@ -85,7 +85,9 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), @submit="onSubmit" :submit-disabled="!canSubmit()"> - <template slot="body" slot-scope="props"> + <template + slot="body" + slot-scope="props"> <p v-html="props.text"></p> <form @@ -96,13 +98,19 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), <input type="hidden" name="_method" - value="delete" /> + value="delete" + /> <input type="hidden" name="authenticity_token" - :value="csrfToken" /> + :value="csrfToken" + /> - <p id="input-label" v-html="inputLabel"></p> + <p + id="input-label" + v-html="inputLabel" + > + </p> <input v-if="confirmWithPassword" @@ -110,14 +118,16 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), class="form-control" type="password" v-model="enteredPassword" - aria-labelledby="input-label" /> + aria-labelledby="input-label" + /> <input v-else name="username" class="form-control" type="text" v-model="enteredUsername" - aria-labelledby="input-label" /> + aria-labelledby="input-label" + /> </form> </template> diff --git a/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue b/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue index 8fce4c63872..3ebfe82597a 100644 --- a/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue +++ b/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue @@ -1,77 +1,80 @@ <script> -import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue'; + import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue'; -export default { - props: { - name: { - type: String, - required: false, - default: '', + export default { + components: { + projectFeatureToggle, }, - options: { - type: Array, - required: false, - default: () => [], - }, - value: { - type: Number, - required: false, - default: 0, - }, - disabledInput: { - type: Boolean, - required: false, - default: false, - }, - }, - - components: { - projectFeatureToggle, - }, - computed: { - featureEnabled() { - return this.value !== 0; + model: { + prop: 'value', + event: 'change', }, - displayOptions() { - if (this.featureEnabled) { - return this.options; - } - return [ - [0, 'Enable feature to choose access level'], - ]; + props: { + name: { + type: String, + required: false, + default: '', + }, + options: { + type: Array, + required: false, + default: () => [], + }, + value: { + type: Number, + required: false, + default: 0, + }, + disabledInput: { + type: Boolean, + required: false, + default: false, + }, }, - displaySelectInput() { - return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2; - }, - }, + computed: { + featureEnabled() { + return this.value !== 0; + }, - model: { - prop: 'value', - event: 'change', - }, + displayOptions() { + if (this.featureEnabled) { + return this.options; + } + return [ + [0, 'Enable feature to choose access level'], + ]; + }, - methods: { - toggleFeature(featureEnabled) { - if (featureEnabled === false || this.options.length < 1) { - this.$emit('change', 0); - } else { - const [firstOptionValue] = this.options[this.options.length - 1]; - this.$emit('change', firstOptionValue); - } + displaySelectInput() { + return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2; + }, }, - selectOption(e) { - this.$emit('change', Number(e.target.value)); + methods: { + toggleFeature(featureEnabled) { + if (featureEnabled === false || this.options.length < 1) { + this.$emit('change', 0); + } else { + const [firstOptionValue] = this.options[this.options.length - 1]; + this.$emit('change', firstOptionValue); + } + }, + + selectOption(e) { + this.$emit('change', Number(e.target.value)); + }, }, - }, -}; + }; </script> <template> - <div class="project-feature-controls" :data-for="name"> + <div + class="project-feature-controls" + :data-for="name" + > <input v-if="name" type="hidden" @@ -81,7 +84,7 @@ export default { <project-feature-toggle :value="featureEnabled" @change="toggleFeature" - :disabledInput="disabledInput" + :disabled-input="disabledInput" /> <div class="select-wrapper"> <select @@ -95,10 +98,14 @@ export default { :value="optionValue" :selected="optionValue === value" > - {{optionName}} + {{ optionName }} </option> </select> - <i aria-hidden="true" class="fa fa-chevron-down"></i> + <i + aria-hidden="true" + class="fa fa-chevron-down" + > + </i> </div> </div> </template> diff --git a/app/assets/javascripts/projects/permissions/components/project_setting_row.vue b/app/assets/javascripts/projects/permissions/components/project_setting_row.vue index 6140d74fea8..25a88f846eb 100644 --- a/app/assets/javascripts/projects/permissions/components/project_setting_row.vue +++ b/app/assets/javascripts/projects/permissions/components/project_setting_row.vue @@ -1,36 +1,51 @@ <script> -export default { - props: { - label: { - type: String, - required: false, - default: null, + export default { + props: { + label: { + type: String, + required: false, + default: null, + }, + helpPath: { + type: String, + required: false, + default: null, + }, + helpText: { + type: String, + required: false, + default: null, + }, }, - helpPath: { - type: String, - required: false, - default: null, - }, - helpText: { - type: String, - required: false, - default: null, - }, - }, -}; + }; </script> <template> <div class="project-feature-row"> - <label v-if="label" class="label-light"> - {{label}} - <a v-if="helpPath" :href="helpPath" target="_blank"> - <i aria-hidden="true" data-hidden="true" class="fa fa-question-circle"></i> + <label + v-if="label" + class="label-light" + > + {{ label }} + <a + v-if="helpPath" + :href="helpPath" + target="_blank" + > + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-question-circle" + > + </i> </a> </label> - <span v-if="helpText" class="help-block"> - {{helpText}} + <span + v-if="helpText" + class="help-block" + > + {{ helpText }} </span> - <slot /> + <slot></slot> </div> </template> diff --git a/app/assets/javascripts/projects/permissions/components/settings_panel.vue b/app/assets/javascripts/projects/permissions/components/settings_panel.vue index 639429baf26..c96ce12d9fb 100644 --- a/app/assets/javascripts/projects/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/projects/permissions/components/settings_panel.vue @@ -1,172 +1,174 @@ <script> -import projectFeatureSetting from './project_feature_setting.vue'; -import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue'; -import projectSettingRow from './project_setting_row.vue'; -import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; -import { toggleHiddenClassBySelector } from '../external'; + import projectFeatureSetting from './project_feature_setting.vue'; + import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue'; + import projectSettingRow from './project_setting_row.vue'; + import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; + import { toggleHiddenClassBySelector } from '../external'; -export default { - props: { - currentSettings: { - type: Object, - required: true, + export default { + components: { + projectFeatureSetting, + projectFeatureToggle, + projectSettingRow, }, - canChangeVisibilityLevel: { - type: Boolean, - required: false, - default: false, - }, - allowedVisibilityOptions: { - type: Array, - required: false, - default: () => [0, 10, 20], - }, - lfsAvailable: { - type: Boolean, - required: false, - default: false, - }, - registryAvailable: { - type: Boolean, - required: false, - default: false, - }, - visibilityHelpPath: { - type: String, - required: false, - }, - lfsHelpPath: { - type: String, - required: false, - }, - registryHelpPath: { - type: String, - required: false, - }, - }, - data() { - const defaults = { - visibilityOptions, - visibilityLevel: visibilityOptions.PUBLIC, - issuesAccessLevel: 20, - repositoryAccessLevel: 20, - mergeRequestsAccessLevel: 20, - buildsAccessLevel: 20, - wikiAccessLevel: 20, - snippetsAccessLevel: 20, - containerRegistryEnabled: true, - lfsEnabled: true, - requestAccessEnabled: true, - highlightChangesClass: false, - }; - - return { ...defaults, ...this.currentSettings }; - }, - - components: { - projectFeatureSetting, - projectFeatureToggle, - projectSettingRow, - }, - - computed: { - featureAccessLevelOptions() { - const options = [ - [10, 'Only Project Members'], - ]; - if (this.visibilityLevel !== visibilityOptions.PRIVATE) { - options.push([20, 'Everyone With Access']); - } - return options; + props: { + currentSettings: { + type: Object, + required: true, + }, + canChangeVisibilityLevel: { + type: Boolean, + required: false, + default: false, + }, + allowedVisibilityOptions: { + type: Array, + required: false, + default: () => [0, 10, 20], + }, + lfsAvailable: { + type: Boolean, + required: false, + default: false, + }, + registryAvailable: { + type: Boolean, + required: false, + default: false, + }, + visibilityHelpPath: { + type: String, + required: false, + default: '', + }, + lfsHelpPath: { + type: String, + required: false, + default: '', + }, + registryHelpPath: { + type: String, + required: false, + default: '', + }, }, - repoFeatureAccessLevelOptions() { - return this.featureAccessLevelOptions.filter( - ([value]) => value <= this.repositoryAccessLevel, - ); - }, + data() { + const defaults = { + visibilityOptions, + visibilityLevel: visibilityOptions.PUBLIC, + issuesAccessLevel: 20, + repositoryAccessLevel: 20, + mergeRequestsAccessLevel: 20, + buildsAccessLevel: 20, + wikiAccessLevel: 20, + snippetsAccessLevel: 20, + containerRegistryEnabled: true, + lfsEnabled: true, + requestAccessEnabled: true, + highlightChangesClass: false, + }; - repositoryEnabled() { - return this.repositoryAccessLevel > 0; + return { ...defaults, ...this.currentSettings }; }, - visibilityLevelDescription() { - return visibilityLevelDescriptions[this.visibilityLevel]; - }, - }, + computed: { + featureAccessLevelOptions() { + const options = [ + [10, 'Only Project Members'], + ]; + if (this.visibilityLevel !== visibilityOptions.PRIVATE) { + options.push([20, 'Everyone With Access']); + } + return options; + }, - methods: { - highlightChanges() { - this.highlightChangesClass = true; - this.$nextTick(() => { - this.highlightChangesClass = false; - }); - }, + repoFeatureAccessLevelOptions() { + return this.featureAccessLevelOptions.filter( + ([value]) => value <= this.repositoryAccessLevel, + ); + }, - visibilityAllowed(option) { - return this.allowedVisibilityOptions.includes(option); - }, - }, + repositoryEnabled() { + return this.repositoryAccessLevel > 0; + }, - watch: { - visibilityLevel(value, oldValue) { - if (value === visibilityOptions.PRIVATE) { - // when private, features are restricted to "only team members" - this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel); - this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel); - this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel); - this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel); - this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel); - this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel); - this.highlightChanges(); - } else if (oldValue === visibilityOptions.PRIVATE) { - // if changing away from private, make enabled features more permissive - if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20; - if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20; - if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20; - if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20; - if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20; - if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20; - this.highlightChanges(); - } + visibilityLevelDescription() { + return visibilityLevelDescriptions[this.visibilityLevel]; + }, }, - repositoryAccessLevel(value, oldValue) { - if (value < oldValue) { - // sub-features cannot have more premissive access level - this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value); - this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value); + watch: { + visibilityLevel(value, oldValue) { + if (value === visibilityOptions.PRIVATE) { + // when private, features are restricted to "only team members" + this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel); + this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel); + this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel); + this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel); + this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel); + this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel); + this.highlightChanges(); + } else if (oldValue === visibilityOptions.PRIVATE) { + // if changing away from private, make enabled features more permissive + if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20; + if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20; + if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20; + if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20; + if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20; + if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20; + this.highlightChanges(); + } + }, + + repositoryAccessLevel(value, oldValue) { + if (value < oldValue) { + // sub-features cannot have more premissive access level + this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value); + this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value); - if (value === 0) { - this.containerRegistryEnabled = false; - this.lfsEnabled = false; + if (value === 0) { + this.containerRegistryEnabled = false; + this.lfsEnabled = false; + } + } else if (oldValue === 0) { + this.mergeRequestsAccessLevel = value; + this.buildsAccessLevel = value; + this.containerRegistryEnabled = true; + this.lfsEnabled = true; } - } else if (oldValue === 0) { - this.mergeRequestsAccessLevel = value; - this.buildsAccessLevel = value; - this.containerRegistryEnabled = true; - this.lfsEnabled = true; - } - }, + }, - issuesAccessLevel(value, oldValue) { - if (value === 0) toggleHiddenClassBySelector('.issues-feature', true); - else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false); - }, + issuesAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.issues-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false); + }, - mergeRequestsAccessLevel(value, oldValue) { - if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true); - else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false); - }, + mergeRequestsAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false); + }, - buildsAccessLevel(value, oldValue) { - if (value === 0) toggleHiddenClassBySelector('.builds-feature', true); - else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false); + buildsAccessLevel(value, oldValue) { + if (value === 0) toggleHiddenClassBySelector('.builds-feature', true); + else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false); + }, }, - }, -}; + methods: { + highlightChanges() { + this.highlightChangesClass = true; + this.$nextTick(() => { + this.highlightChangesClass = false; + }); + }, + + visibilityAllowed(option) { + return this.allowedVisibilityOptions.includes(option); + }, + }, + }; </script> <template> @@ -203,22 +205,36 @@ export default { Public </option> </select> - <i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i> + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-chevron-down" + > + </i> </div> </div> <span class="help-block">{{ visibilityLevelDescription }}</span> - <label v-if="visibilityLevel !== visibilityOptions.PUBLIC" class="request-access"> + <label + v-if="visibilityLevel !== visibilityOptions.PUBLIC" + class="request-access" + > <input type="hidden" name="project[request_access_enabled]" :value="requestAccessEnabled" /> - <input type="checkbox" v-model="requestAccessEnabled" /> + <input + type="checkbox" + v-model="requestAccessEnabled" + /> Allow users to request access </label> </project-setting-row> </div> - <div class="project-feature-settings" :class="{ 'highlight-changes': highlightChangesClass }"> + <div + class="project-feature-settings" + :class="{ 'highlight-changes': highlightChangesClass }" + > <project-setting-row label="Issues" help-text="Lightweight issue tracking system for this project" @@ -248,7 +264,7 @@ export default { name="project[project_feature_attributes][merge_requests_access_level]" :options="repoFeatureAccessLevelOptions" v-model="mergeRequestsAccessLevel" - :disabledInput="!repositoryEnabled" + :disabled-input="!repositoryEnabled" /> </project-setting-row> <project-setting-row @@ -259,7 +275,7 @@ export default { name="project[project_feature_attributes][builds_access_level]" :options="repoFeatureAccessLevelOptions" v-model="buildsAccessLevel" - :disabledInput="!repositoryEnabled" + :disabled-input="!repositoryEnabled" /> </project-setting-row> <project-setting-row @@ -271,7 +287,7 @@ export default { <project-feature-toggle name="project[container_registry_enabled]" v-model="containerRegistryEnabled" - :disabledInput="!repositoryEnabled" + :disabled-input="!repositoryEnabled" /> </project-setting-row> <project-setting-row @@ -283,7 +299,7 @@ export default { <project-feature-toggle name="project[lfs_enabled]" v-model="lfsEnabled" - :disabledInput="!repositoryEnabled" + :disabled-input="!repositoryEnabled" /> </project-setting-row> </div> diff --git a/app/assets/javascripts/projects_dropdown/components/app.vue b/app/assets/javascripts/projects_dropdown/components/app.vue index 7606605be32..34a60dd574b 100644 --- a/app/assets/javascripts/projects_dropdown/components/app.vue +++ b/app/assets/javascripts/projects_dropdown/components/app.vue @@ -47,6 +47,22 @@ export default { return this.store.getSearchedProjects(); }, }, + created() { + if (this.currentProject.id) { + this.logCurrentProjectAccess(); + } + + eventHub.$on('dropdownOpen', this.fetchFrequentProjects); + eventHub.$on('searchProjects', this.fetchSearchedProjects); + eventHub.$on('searchCleared', this.handleSearchClear); + eventHub.$on('searchFailed', this.handleSearchFailure); + }, + beforeDestroy() { + eventHub.$off('dropdownOpen', this.fetchFrequentProjects); + eventHub.$off('searchProjects', this.fetchSearchedProjects); + eventHub.$off('searchCleared', this.handleSearchClear); + eventHub.$off('searchFailed', this.handleSearchFailure); + }, methods: { toggleFrequentProjectsList(state) { this.isLoadingProjects = !state; @@ -108,22 +124,6 @@ export default { this.toggleSearchProjectsList(true); }, }, - created() { - if (this.currentProject.id) { - this.logCurrentProjectAccess(); - } - - eventHub.$on('dropdownOpen', this.fetchFrequentProjects); - eventHub.$on('searchProjects', this.fetchSearchedProjects); - eventHub.$on('searchCleared', this.handleSearchClear); - eventHub.$on('searchFailed', this.handleSearchFailure); - }, - beforeDestroy() { - eventHub.$off('dropdownOpen', this.fetchFrequentProjects); - eventHub.$off('searchProjects', this.fetchSearchedProjects); - eventHub.$off('searchCleared', this.handleSearchClear); - eventHub.$off('searchFailed', this.handleSearchFailure); - }, }; </script> diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue index 093554cd0bc..246dbeaaded 100644 --- a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue @@ -1,32 +1,32 @@ <script> -import { s__ } from '../../locale'; -import projectsListItem from './projects_list_item.vue'; + import { s__ } from '../../locale'; + import projectsListItem from './projects_list_item.vue'; -export default { - components: { - projectsListItem, - }, - props: { - projects: { - type: Array, - required: true, + export default { + components: { + projectsListItem, }, - localStorageFailed: { - type: Boolean, - required: true, + props: { + projects: { + type: Array, + required: true, + }, + localStorageFailed: { + type: Boolean, + required: true, + }, }, - }, - computed: { - isListEmpty() { - return this.projects.length === 0; + computed: { + isListEmpty() { + return this.projects.length === 0; + }, + listEmptyMessage() { + return this.localStorageFailed ? + s__('ProjectsDropdown|This feature requires browser localStorage support') : + s__('ProjectsDropdown|Projects you visit often will appear here'); + }, }, - listEmptyMessage() { - return this.localStorageFailed ? - s__('ProjectsDropdown|This feature requires browser localStorage support') : - s__('ProjectsDropdown|Projects you visit often will appear here'); - }, - }, -}; + }; </script> <template> @@ -40,7 +40,7 @@ export default { class="section-empty" v-if="isListEmpty" > - {{listEmptyMessage}} + {{ listEmptyMessage }} </li> <projects-list-item v-else diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue index d482a7025de..759cdd1ded9 100644 --- a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue @@ -1,76 +1,77 @@ <script> -import identicon from '../../vue_shared/components/identicon.vue'; + /* eslint-disable vue/require-default-prop, vue/require-prop-types */ + import identicon from '../../vue_shared/components/identicon.vue'; -export default { - components: { - identicon, - }, - props: { - matcher: { - type: String, - required: false, + export default { + components: { + identicon, }, - projectId: { - type: Number, - required: true, - }, - projectName: { - type: String, - required: true, - }, - namespace: { - type: String, - required: true, - }, - webUrl: { - type: String, - required: true, - }, - avatarUrl: { - required: true, - validator(value) { - return value === null || typeof value === 'string'; + props: { + matcher: { + type: String, + required: false, + }, + projectId: { + type: Number, + required: true, + }, + projectName: { + type: String, + required: true, + }, + namespace: { + type: String, + required: true, + }, + webUrl: { + type: String, + required: true, + }, + avatarUrl: { + required: true, + validator(value) { + return value === null || typeof value === 'string'; + }, }, }, - }, - computed: { - hasAvatar() { - return this.avatarUrl !== null; - }, - highlightedProjectName() { - if (this.matcher) { - const matcherRegEx = new RegExp(this.matcher, 'gi'); - const matches = this.projectName.match(matcherRegEx); + computed: { + hasAvatar() { + return this.avatarUrl !== null; + }, + highlightedProjectName() { + if (this.matcher) { + const matcherRegEx = new RegExp(this.matcher, 'gi'); + const matches = this.projectName.match(matcherRegEx); - if (matches && matches.length > 0) { - return this.projectName.replace(matches[0], `<b>${matches[0]}</b>`); + if (matches && matches.length > 0) { + return this.projectName.replace(matches[0], `<b>${matches[0]}</b>`); + } } - } - return this.projectName; - }, - /** - * Smartly truncates project namespace by doing two things; - * 1. Only include Group names in path by removing project name - * 2. Only include first and last group names in the path - * when namespace has more than 2 groups present - * - * First part (removal of project name from namespace) can be - * done from backend but doing so involves migration of - * existing project namespaces which is not wise thing to do. - */ - truncatedNamespace() { - const namespaceArr = this.namespace.split(' / '); - namespaceArr.splice(-1, 1); - let namespace = namespaceArr.join(' / '); + return this.projectName; + }, + /** + * Smartly truncates project namespace by doing two things; + * 1. Only include Group names in path by removing project name + * 2. Only include first and last group names in the path + * when namespace has more than 2 groups present + * + * First part (removal of project name from namespace) can be + * done from backend but doing so involves migration of + * existing project namespaces which is not wise thing to do. + */ + truncatedNamespace() { + const namespaceArr = this.namespace.split(' / '); + namespaceArr.splice(-1, 1); + let namespace = namespaceArr.join(' / '); - if (namespaceArr.length > 2) { - namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`; - } + if (namespaceArr.length > 2) { + namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`; + } - return namespace; + return namespace; + }, }, - }, -}; + }; </script> <template> @@ -92,7 +93,7 @@ export default { <identicon v-else size-class="s32" - :entity-id=projectId + :entity-id="projectId" :entity-name="projectName" /> </div> @@ -108,7 +109,7 @@ export default { <div class="project-namespace" :title="namespace" - >{{truncatedNamespace}}</div> + >{{ truncatedNamespace }}</div> </div> </a> </li> diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue index 53bc76d0f2d..0c46ed184be 100644 --- a/app/assets/javascripts/projects_dropdown/components/search.vue +++ b/app/assets/javascripts/projects_dropdown/components/search.vue @@ -1,47 +1,47 @@ <script> -import _ from 'underscore'; -import eventHub from '../event_hub'; + import _ from 'underscore'; + import eventHub from '../event_hub'; -export default { - data() { - return { - searchQuery: '', - }; - }, - watch: { - searchQuery() { - this.handleInput(); + export default { + data() { + return { + searchQuery: '', + }; }, - }, - methods: { - setFocus() { - this.$refs.search.focus(); + watch: { + searchQuery() { + this.handleInput(); + }, }, - emitSearchEvents() { - if (this.searchQuery) { - eventHub.$emit('searchProjects', this.searchQuery); - } else { - eventHub.$emit('searchCleared'); - } + mounted() { + eventHub.$on('dropdownOpen', this.setFocus); }, - /** - * Callback function within _.debounce is intentionally - * kept as ES5 `function() {}` instead of ES6 `() => {}` - * as it otherwise messes up function context - * and component reference is no longer accessible via `this` - */ - // eslint-disable-next-line func-names - handleInput: _.debounce(function () { - this.emitSearchEvents(); - }, 500), - }, - mounted() { - eventHub.$on('dropdownOpen', this.setFocus); - }, - beforeDestroy() { - eventHub.$off('dropdownOpen', this.setFocus); - }, -}; + beforeDestroy() { + eventHub.$off('dropdownOpen', this.setFocus); + }, + methods: { + setFocus() { + this.$refs.search.focus(); + }, + emitSearchEvents() { + if (this.searchQuery) { + eventHub.$emit('searchProjects', this.searchQuery); + } else { + eventHub.$emit('searchCleared'); + } + }, + /** + * Callback function within _.debounce is intentionally + * kept as ES5 `function() {}` instead of ES6 `() => {}` + * as it otherwise messes up function context + * and component reference is no longer accessible via `this` + */ + // eslint-disable-next-line func-names + handleInput: _.debounce(function () { + this.emitSearchEvents(); + }, 500), + }, + }; </script> <template> @@ -59,6 +59,7 @@ export default { v-if="!searchQuery" class="search-icon fa fa-fw fa-search" aria-hidden="true" - /> + > + </i> </div> </template> diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index 2d8ca443ea7..ea0f7199a70 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -1,14 +1,17 @@ <script> - /* globals Flash */ import { mapGetters, mapActions } from 'vuex'; - import '../../flash'; + import Flash from '../../flash'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import store from '../stores'; import collapsibleContainer from './collapsible_container.vue'; import { errorMessages, errorMessagesTypes } from '../constants'; export default { - name: 'registryListApp', + name: 'RegistryListApp', + components: { + collapsibleContainer, + loadingIcon, + }, props: { endpoint: { type: String, @@ -16,22 +19,12 @@ }, }, store, - components: { - collapsibleContainer, - loadingIcon, - }, computed: { ...mapGetters([ 'isLoading', 'repos', ]), }, - methods: { - ...mapActions([ - 'setMainEndpoint', - 'fetchRepos', - ]), - }, created() { this.setMainEndpoint(this.endpoint); }, @@ -39,6 +32,12 @@ this.fetchRepos() .catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS])); }, + methods: { + ...mapActions([ + 'setMainEndpoint', + 'fetchRepos', + ]), + }, }; </script> <template> @@ -46,17 +45,18 @@ <loading-icon v-if="isLoading" size="3" - /> + /> <collapsible-container v-else-if="!isLoading && repos.length" v-for="(item, index) in repos" :key="index" :repo="item" - /> + /> <p v-else-if="!isLoading && !repos.length"> - {{__("No container images stored for this project. Add one by following the instructions above.")}} + {{ __(`No container images stored for this project. +Add one by following the instructions above.`) }} </p> </div> </template> diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index ac1c3ec253c..b4906ba4ee5 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -1,7 +1,6 @@ <script> - /* globals Flash */ import { mapActions } from 'vuex'; - import '../../flash'; + import Flash from '../../flash'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; @@ -9,13 +8,7 @@ import { errorMessages, errorMessagesTypes } from '../constants'; export default { - name: 'collapsibeContainerRegisty', - props: { - repo: { - type: Object, - required: true, - }, - }, + name: 'CollapsibeContainerRegisty', components: { clipboardButton, loadingIcon, @@ -24,6 +17,12 @@ directives: { tooltip, }, + props: { + repo: { + type: Object, + required: true, + }, + }, data() { return { isOpen: false, @@ -65,28 +64,29 @@ <template> <div class="container-image"> - <div - class="container-image-head"> + <div class="container-image-head"> <button type="button" @click="toggleRepo" - class="js-toggle-repo btn-link"> + class="js-toggle-repo btn-link" + > <i class="fa" :class="{ 'fa-chevron-right': !isOpen, 'fa-chevron-up': isOpen, }" - aria-hidden="true"> + aria-hidden="true" + > </i> - {{repo.name}} + {{ repo.name }} </button> <clipboard-button v-if="repo.location" :text="clipboardText" :title="repo.location" - /> + /> <div class="controls hidden-xs pull-right"> <button @@ -96,35 +96,38 @@ :title="s__('ContainerRegistry|Remove repository')" :aria-label="s__('ContainerRegistry|Remove repository')" v-tooltip - @click="handleDeleteRepository"> + @click="handleDeleteRepository" + > <i class="fa fa-trash" - aria-hidden="true"> + aria-hidden="true" + > </i> </button> </div> - </div> <loading-icon v-if="repo.isLoading" class="append-bottom-20" size="2" - /> + /> <div v-else-if="!repo.isLoading && isOpen" - class="container-image-tags"> + class="container-image-tags" + > <table-registry v-if="repo.list.length" :repo="repo" - /> + /> <div v-else - class="nothing-here-block"> - {{s__("ContainerRegistry|No tags in Container Registry for this container image.")}} + class="nothing-here-block" + > + {{ s__("ContainerRegistry|No tags in Container Registry for this container image.") }} </div> </div> </div> diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index 14d43e135fe..bef850eddc0 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -1,8 +1,7 @@ <script> - /* globals Flash */ import { mapActions } from 'vuex'; import { n__ } from '../../locale'; - import '../../flash'; + import Flash from '../../flash'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import tablePagination from '../../vue_shared/components/table_pagination.vue'; import tooltip from '../../vue_shared/directives/tooltip'; @@ -11,21 +10,21 @@ import { numberToHumanSize } from '../../lib/utils/number_utils'; export default { - props: { - repo: { - type: Object, - required: true, - }, - }, components: { clipboardButton, tablePagination, }, + directives: { + tooltip, + }, mixins: [ timeagoMixin, ], - directives: { - tooltip, + props: { + repo: { + type: Object, + required: true, + }, }, computed: { shouldRenderPagination() { @@ -68,75 +67,78 @@ }; </script> <template> -<div> - <table class="table tags"> - <thead> - <tr> - <th>{{s__('ContainerRegistry|Tag')}}</th> - <th>{{s__('ContainerRegistry|Tag ID')}}</th> - <th>{{s__("ContainerRegistry|Size")}}</th> - <th>{{s__("ContainerRegistry|Created")}}</th> - <th></th> - </tr> - </thead> - <tbody> - <tr - v-for="(item, i) in repo.list" - :key="i"> - <td> + <div> + <table class="table tags"> + <thead> + <tr> + <th>{{ s__('ContainerRegistry|Tag') }}</th> + <th>{{ s__('ContainerRegistry|Tag ID') }}</th> + <th>{{ s__("ContainerRegistry|Size") }}</th> + <th>{{ s__("ContainerRegistry|Created") }}</th> + <th></th> + </tr> + </thead> + <tbody> + <tr + v-for="(item, i) in repo.list" + :key="i"> + <td> - {{item.tag}} + {{ item.tag }} - <clipboard-button - v-if="item.location" - :title="item.location" - :text="clipboardText(item.location)" + <clipboard-button + v-if="item.location" + :title="item.location" + :text="clipboardText(item.location)" /> - </td> - <td> - <span - v-tooltip - :title="item.revision" - data-placement="bottom"> - {{item.shortRevision}} + </td> + <td> + <span + v-tooltip + :title="item.revision" + data-placement="bottom" + > + {{ item.shortRevision }} </span> - </td> - <td> - {{formatSize(item.size)}} - <template v-if="item.size && item.layers"> - · - </template> - {{layers(item)}} - </td> + </td> + <td> + {{ formatSize(item.size) }} + <template v-if="item.size && item.layers"> + · + </template> + {{ layers(item) }} + </td> - <td> - {{timeFormated(item.createdAt)}} - </td> + <td> + {{ timeFormated(item.createdAt) }} + </td> - <td class="content"> - <button - v-if="item.canDelete" - type="button" - class="js-delete-registry btn btn-danger hidden-xs pull-right" - :title="s__('ContainerRegistry|Remove tag')" - :aria-label="s__('ContainerRegistry|Remove tag')" - data-container="body" - v-tooltip - @click="handleDeleteRegistry(item)"> - <i - class="fa fa-trash" - aria-hidden="true"> - </i> - </button> - </td> - </tr> - </tbody> - </table> + <td class="content"> + <button + v-if="item.canDelete" + type="button" + class="js-delete-registry btn btn-danger hidden-xs pull-right" + :title="s__('ContainerRegistry|Remove tag')" + :aria-label="s__('ContainerRegistry|Remove tag')" + data-container="body" + v-tooltip + @click="handleDeleteRegistry(item)" + > + <i + class="fa fa-trash" + aria-hidden="true" + > + </i> + </button> + </td> + </tr> + </tbody> + </table> - <table-pagination - v-if="shouldRenderPagination" - :change="onPageChange" - :page-info="repo.pagination" + <table-pagination + v-if="shouldRenderPagination" + :change="onPageChange" + :page-info="repo.pagination" /> -</div> + </div> </template> diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index 6ee4d487c0b..839f9ec88b9 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -1,48 +1,51 @@ <script> -import Flash from '../../../flash'; -import editForm from './edit_form.vue'; -import Icon from '../../../vue_shared/components/icon.vue'; + import Flash from '../../../flash'; + import editForm from './edit_form.vue'; + import Icon from '../../../vue_shared/components/icon.vue'; -export default { - components: { - editForm, - Icon, - }, - props: { - isConfidential: { - required: true, - type: Boolean, + export default { + components: { + editForm, + Icon, }, - isEditable: { - required: true, - type: Boolean, + props: { + isConfidential: { + required: true, + type: Boolean, + }, + isEditable: { + required: true, + type: Boolean, + }, + service: { + required: true, + type: Object, + }, }, - service: { - required: true, - type: Object, + data() { + return { + edit: false, + }; }, - }, - data() { - return { - edit: false, - }; - }, - computed: { - confidentialityIcon() { - return this.isConfidential ? 'eye-slash' : 'eye'; + computed: { + confidentialityIcon() { + return this.isConfidential ? 'eye-slash' : 'eye'; + }, }, - }, - methods: { - toggleForm() { - this.edit = !this.edit; + methods: { + toggleForm() { + this.edit = !this.edit; + }, + updateConfidentialAttribute(confidential) { + this.service.update('issue', { confidential }) + .then(() => location.reload()) + .catch(() => { + Flash(`Something went wrong trying to + change the confidentiality of this issue`); + }); + }, }, - updateConfidentialAttribute(confidential) { - this.service.update('issue', { confidential }) - .then(() => location.reload()) - .catch(() => new Flash('Something went wrong trying to change the confidentiality of this issue')); - }, - }, -}; + }; </script> <template> @@ -51,8 +54,8 @@ export default { <icon :name="confidentialityIcon" :size="16" - aria-hidden="true"> - </icon> + aria-hidden="true" + /> </div> <div class="title hide-collapsed"> Confidentiality @@ -72,22 +75,26 @@ export default { :is-confidential="isConfidential" :update-confidential-attribute="updateConfidentialAttribute" /> - <div v-if="!isConfidential" class="no-value sidebar-item-value"> + <div + v-if="!isConfidential" + class="no-value sidebar-item-value"> <icon name="eye" :size="16" aria-hidden="true" - class="sidebar-item-icon inline"> - </icon> + class="sidebar-item-icon inline" + /> Not confidential </div> - <div v-else class="value sidebar-item-value hide-collapsed"> + <div + v-else + class="value sidebar-item-value hide-collapsed"> <icon name="eye-slash" :size="16" aria-hidden="true" - class="sidebar-item-icon inline is-active"> - </icon> + class="sidebar-item-icon inline is-active" + /> This issue is confidential </div> </div> diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue index dd17b5abd46..6a81235a1a7 100644 --- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue +++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue @@ -1,26 +1,25 @@ <script> -import editFormButtons from './edit_form_buttons.vue'; + import editFormButtons from './edit_form_buttons.vue'; -export default { - props: { - isConfidential: { - required: true, - type: Boolean, + export default { + components: { + editFormButtons, }, - toggleForm: { - required: true, - type: Function, + props: { + isConfidential: { + required: true, + type: Boolean, + }, + toggleForm: { + required: true, + type: Function, + }, + updateConfidentialAttribute: { + required: true, + type: Function, + }, }, - updateConfidentialAttribute: { - required: true, - type: Function, - }, - }, - - components: { - editFormButtons, - }, -}; + }; </script> <template> diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue index 242e826d471..e7a87636aa7 100644 --- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue +++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue @@ -1,45 +1,47 @@ <script> -import editFormButtons from './edit_form_buttons.vue'; -import issuableMixin from '../../../vue_shared/mixins/issuable'; + import editFormButtons from './edit_form_buttons.vue'; + import issuableMixin from '../../../vue_shared/mixins/issuable'; -export default { - props: { - isLocked: { - required: true, - type: Boolean, + export default { + components: { + editFormButtons, }, - - toggleForm: { - required: true, - type: Function, - }, - - updateLockedAttribute: { - required: true, - type: Function, + mixins: [ + issuableMixin, + ], + props: { + isLocked: { + required: true, + type: Boolean, + }, + + toggleForm: { + required: true, + type: Function, + }, + + updateLockedAttribute: { + required: true, + type: Function, + }, }, - }, - - mixins: [ - issuableMixin, - ], - - components: { - editFormButtons, - }, -}; + }; </script> <template> <div class="dropdown open"> <div class="dropdown-menu sidebar-item-warning-message"> - <p class="text" v-if="isLocked"> + <p + class="text" + v-if="isLocked"> Unlock this {{ issuableDisplayName }}? <strong>Everyone</strong> will be able to comment. </p> - <p class="text" v-else> + <p + class="text" + v-else> Lock this {{ issuableDisplayName }}? Only <strong>project members</strong> diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue index 04c3a96bf74..02876a6c175 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -1,63 +1,63 @@ <script> -/* global Flash */ -import editForm from './edit_form.vue'; -import issuableMixin from '../../../vue_shared/mixins/issuable'; -import Icon from '../../../vue_shared/components/icon.vue'; + import Flash from '../../../flash'; + import editForm from './edit_form.vue'; + import issuableMixin from '../../../vue_shared/mixins/issuable'; + import Icon from '../../../vue_shared/components/icon.vue'; -export default { - props: { - isLocked: { - required: true, - type: Boolean, + export default { + components: { + editForm, + Icon, }, + mixins: [ + issuableMixin, + ], - isEditable: { - required: true, - type: Boolean, - }, - - mediator: { - required: true, - type: Object, - validator(mediatorObject) { - return mediatorObject.service && mediatorObject.service.update && mediatorObject.store; + props: { + isLocked: { + required: true, + type: Boolean, }, - }, - }, - - mixins: [ - issuableMixin, - ], - components: { - editForm, - Icon, - }, + isEditable: { + required: true, + type: Boolean, + }, - computed: { - lockIcon() { - return this.isLocked ? 'lock' : 'lock-open'; + mediator: { + required: true, + type: Object, + validator(mediatorObject) { + return mediatorObject.service && mediatorObject.service.update && mediatorObject.store; + }, + }, }, - isLockDialogOpen() { - return this.mediator.store.isLockDialogOpen; - }, - }, + computed: { + lockIcon() { + return this.isLocked ? 'lock' : 'lock-open'; + }, - methods: { - toggleForm() { - this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; + isLockDialogOpen() { + return this.mediator.store.isLockDialogOpen; + }, }, - updateLockedAttribute(locked) { - this.mediator.service.update(this.issuableType, { - discussion_locked: locked, - }) - .then(() => location.reload()) - .catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName}`))); + methods: { + toggleForm() { + this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; + }, + + updateLockedAttribute(locked) { + this.mediator.service.update(this.issuableType, { + discussion_locked: locked, + }) + .then(() => location.reload()) + .catch(() => Flash(this.__(`Something went wrong trying to + change the locked state of this ${this.issuableDisplayName}`))); + }, }, - }, -}; + }; </script> <template> @@ -67,8 +67,8 @@ export default { :name="lockIcon" :size="16" aria-hidden="true" - class="sidebar-item-icon is-active"> - </icon> + class="sidebar-item-icon is-active" + /> </div> <div class="title hide-collapsed"> @@ -100,8 +100,8 @@ export default { name="lock" :size="16" aria-hidden="true" - class="sidebar-item-icon inline is-active"> - </icon> + class="sidebar-item-icon inline is-active" + /> {{ __('Locked') }} </div> @@ -113,8 +113,8 @@ export default { name="lock-open" :size="16" aria-hidden="true" - class="sidebar-item-icon inline"> - </icon> + class="sidebar-item-icon inline" + /> {{ __('Unlocked') }} </div> </div> diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index b8510a6ce3a..006a6d2905d 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -1,73 +1,73 @@ <script> -import { __, n__, sprintf } from '../../../locale'; -import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; -import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue'; + import { __, n__, sprintf } from '../../../locale'; + import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; + import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue'; -export default { - props: { - loading: { - type: Boolean, - required: false, - default: false, + export default { + components: { + loadingIcon, + userAvatarImage, }, - participants: { - type: Array, - required: false, - default: () => [], + props: { + loading: { + type: Boolean, + required: false, + default: false, + }, + participants: { + type: Array, + required: false, + default: () => [], + }, + numberOfLessParticipants: { + type: Number, + required: false, + default: 7, + }, }, - numberOfLessParticipants: { - type: Number, - required: false, - default: 7, + data() { + return { + isShowingMoreParticipants: false, + }; }, - }, - data() { - return { - isShowingMoreParticipants: false, - }; - }, - components: { - loadingIcon, - userAvatarImage, - }, - computed: { - lessParticipants() { - return this.participants.slice(0, this.numberOfLessParticipants); - }, - visibleParticipants() { - return this.isShowingMoreParticipants ? this.participants : this.lessParticipants; - }, - hasMoreParticipants() { - return this.participants.length > this.numberOfLessParticipants; - }, - toggleLabel() { - let label = ''; - if (this.isShowingMoreParticipants) { - label = __('- show less'); - } else { - label = sprintf(__('+ %{moreCount} more'), { - moreCount: this.participants.length - this.numberOfLessParticipants, - }); - } + computed: { + lessParticipants() { + return this.participants.slice(0, this.numberOfLessParticipants); + }, + visibleParticipants() { + return this.isShowingMoreParticipants ? this.participants : this.lessParticipants; + }, + hasMoreParticipants() { + return this.participants.length > this.numberOfLessParticipants; + }, + toggleLabel() { + let label = ''; + if (this.isShowingMoreParticipants) { + label = __('- show less'); + } else { + label = sprintf(__('+ %{moreCount} more'), { + moreCount: this.participants.length - this.numberOfLessParticipants, + }); + } - return label; - }, - participantLabel() { - return sprintf( - n__('%{count} participant', '%{count} participants', this.participants.length), - { count: this.loading ? '' : this.participantCount }, - ); - }, - participantCount() { - return this.participants.length; + return label; + }, + participantLabel() { + return sprintf( + n__('%{count} participant', '%{count} participants', this.participants.length), + { count: this.loading ? '' : this.participantCount }, + ); + }, + participantCount() { + return this.participants.length; + }, }, - }, - methods: { - toggleMoreParticipants() { - this.isShowingMoreParticipants = !this.isShowingMoreParticipants; + methods: { + toggleMoreParticipants() { + this.isShowingMoreParticipants = !this.isShowingMoreParticipants; + }, }, - }, -}; + }; </script> <template> @@ -75,14 +75,17 @@ export default { <div class="sidebar-collapsed-icon"> <i class="fa fa-users" - aria-hidden="true"> + aria-hidden="true" + > </i> <loading-icon v-if="loading" - class="js-participants-collapsed-loading-icon" /> + class="js-participants-collapsed-loading-icon" + /> <span v-else - class="js-participants-collapsed-count"> + class="js-participants-collapsed-count" + > {{ participantCount }} </span> </div> @@ -90,34 +93,40 @@ export default { <loading-icon v-if="loading" :inline="true" - class="js-participants-expanded-loading-icon" /> + class="js-participants-expanded-loading-icon" + /> {{ participantLabel }} </div> <div class="participants-list hide-collapsed"> <div v-for="participant in visibleParticipants" :key="participant.id" - class="participants-author js-participants-author"> + class="participants-author js-participants-author" + > <a class="author_link" - :href="participant.web_url"> + :href="participant.web_url" + > <user-avatar-image :lazy="true" :img-src="participant.avatar_url" css-classes="avatar-inline" :size="24" :tooltip-text="participant.name" - tooltip-placement="bottom" /> + tooltip-placement="bottom" + /> </a> </div> </div> <div v-if="hasMoreParticipants" - class="participants-more hide-collapsed"> + class="participants-more hide-collapsed" + > <button type="button" class="btn-transparent btn-blank js-toggle-participants-button" - @click="toggleMoreParticipants"> + @click="toggleMoreParticipants" + > {{ toggleLabel }} </button> </div> diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue index 6fcd2f95309..5c1ead1a8ac 100644 --- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue @@ -1,23 +1,23 @@ <script> -import Store from '../../stores/sidebar_store'; -import participants from './participants.vue'; + import Store from '../../stores/sidebar_store'; + import participants from './participants.vue'; -export default { - data() { - return { - store: new Store(), - }; - }, - props: { - mediator: { - type: Object, - required: true, + export default { + components: { + participants, }, - }, - components: { - participants, - }, -}; + props: { + mediator: { + type: Object, + required: true, + }, + }, + data() { + return { + store: new Store(), + }; + }, + }; </script> <template> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue index f4bae1d3dd5..3e8cc7a6630 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue @@ -6,10 +6,8 @@ import { __ } from '../../../locale'; import subscriptions from './subscriptions.vue'; export default { - data() { - return { - store: new Store(), - }; + components: { + subscriptions, }, props: { mediator: { @@ -17,10 +15,17 @@ export default { required: true, }, }, - components: { - subscriptions, + data() { + return { + store: new Store(), + }; + }, + created() { + eventHub.$on('toggleSubscription', this.onToggleSubscription); + }, + beforeDestroy() { + eventHub.$off('toggleSubscription', this.onToggleSubscription); }, - methods: { onToggleSubscription() { this.mediator.toggleSubscription() @@ -29,14 +34,6 @@ export default { }); }, }, - - created() { - eventHub.$on('toggleSubscription', this.onToggleSubscription); - }, - - beforeDestroy() { - eventHub.$off('toggleSubscription', this.onToggleSubscription); - }, }; </script> @@ -44,6 +41,7 @@ export default { <div class="block subscriptions"> <subscriptions :loading="store.isFetching.subscriptions" - :subscribed="store.subscribed" /> + :subscribed="store.subscribed" + /> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue index 940e1764f3d..7226076a8fc 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue @@ -1,45 +1,46 @@ <script> -import { __ } from '../../../locale'; -import eventHub from '../../event_hub'; -import loadingButton from '../../../vue_shared/components/loading_button.vue'; + /* eslint-disable vue/require-default-prop */ + import { __ } from '../../../locale'; + import eventHub from '../../event_hub'; + import loadingButton from '../../../vue_shared/components/loading_button.vue'; -export default { - props: { - loading: { - type: Boolean, - required: false, - default: false, + export default { + components: { + loadingButton, }, - subscribed: { - type: Boolean, - required: false, + props: { + loading: { + type: Boolean, + required: false, + default: false, + }, + subscribed: { + type: Boolean, + required: false, + }, + id: { + type: Number, + required: false, + }, }, - id: { - type: Number, - required: false, - }, - }, - components: { - loadingButton, - }, - computed: { - buttonLabel() { - let label; - if (this.subscribed === false) { - label = __('Subscribe'); - } else if (this.subscribed === true) { - label = __('Unsubscribe'); - } + computed: { + buttonLabel() { + let label; + if (this.subscribed === false) { + label = __('Subscribe'); + } else if (this.subscribed === true) { + label = __('Unsubscribe'); + } - return label; + return label; + }, }, - }, - methods: { - toggleSubscription() { - eventHub.$emit('toggleSubscription', this.id); + methods: { + toggleSubscription() { + eventHub.$emit('toggleSubscription', this.id); + }, }, - }, -}; + }; </script> <template> @@ -47,7 +48,8 @@ export default { <div class="sidebar-collapsed-icon"> <i class="fa fa-rss" - aria-hidden="true"> + aria-hidden="true" + > </i> </div> <span class="issuable-header-text hide-collapsed pull-left"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index dbc65462377..109a302a172 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -1,10 +1,16 @@ <script> + /* eslint-disable vue/require-default-prop */ import pipelineStage from '../../pipelines/components/stage.vue'; import ciIcon from '../../vue_shared/components/ci_icon.vue'; import icon from '../../vue_shared/components/icon.vue'; export default { name: 'MRWidgetPipeline', + components: { + pipelineStage, + ciIcon, + icon, + }, props: { pipeline: { type: Object, @@ -21,11 +27,6 @@ required: false, }, }, - components: { - pipelineStage, - ciIcon, - icon, - }, computed: { hasPipeline() { return this.pipeline && Object.keys(this.pipeline).length > 0; @@ -62,7 +63,8 @@ <template v-else-if="hasPipeline"> <a class="append-right-10" - :href="this.status.details_path"> + :href="status.details_path" + > <ci-icon :status="status" /> </a> @@ -70,33 +72,37 @@ Pipeline <a :href="pipeline.path" - class="pipeline-id"> - #{{pipeline.id}} + class="pipeline-id" + > + #{{ pipeline.id }} </a> - {{pipeline.details.status.label}} for + {{ pipeline.details.status.label }} for <a :href="pipeline.commit.commit_path" - class="commit-sha js-commit-link"> - {{pipeline.commit.short_id}}</a>. + class="commit-sha js-commit-link" + > + {{ pipeline.commit.short_id }}</a>. <span class="mr-widget-pipeline-graph"> - <span class="stage-cell"> + <span + class="stage-cell" + v-if="hasStages" + > <div - v-if="hasStages" v-for="(stage, i) in pipeline.details.stages" :key="i" - class="stage-container dropdown js-mini-pipeline-graph"> + class="stage-container dropdown js-mini-pipeline-graph" + > <pipeline-stage :stage="stage" /> </div> </span> </span> <template v-if="pipeline.coverage"> - Coverage {{pipeline.coverage}}% + Coverage {{ pipeline.coverage }}% </template> - </div> </template> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index 09276ba2769..52dd0245ff0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -7,6 +7,10 @@ export default { name: 'MRWidgetRebase', + components: { + statusIcon, + loadingIcon, + }, props: { mr: { type: Object, @@ -17,10 +21,6 @@ required: true, }, }, - components: { - statusIcon, - loadingIcon, - }, data() { return { isMakingRequest: false, @@ -88,7 +88,7 @@ <status-icon :status="status" :show-disabled-button="showDisabledButton" - /> + /> <div class="rebase-state-find-class-convention media media-body space-children"> <template v-if="mr.rebaseInProgress || isMakingRequest"> @@ -100,23 +100,27 @@ <span class="bold"> Fast-forward merge is not possible. Rebase the source branch onto - <span class="label-branch">{{mr.targetBranch}}</span> + <span class="label-branch">{{ mr.targetBranch }}</span> to allow this merge request to be merged. </span> </template> <template v-if="!mr.rebaseInProgress && mr.canPushToSourceBranch && !isMakingRequest"> - <div class="accept-merge-holder clearfix js-toggle-container accept-action media space-children"> + <div + class="accept-merge-holder clearfix +js-toggle-container accept-action media space-children"> <button type="button" class="btn btn-sm btn-reopen btn-success" :disabled="isMakingRequest" - @click="rebase"> + @click="rebase" + > <loading-icon v-if="isMakingRequest" /> Rebase </button> <span v-if="!rebasingError" - class="bold"> + class="bold" + > Fast-forward merge is not possible. Rebase the source branch onto the target branch or merge target branch into source branch to allow this merge request to be merged. @@ -124,7 +128,7 @@ <span v-else class="bold danger"> - {{rebasingError}} + {{ rebasingError }} </span> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue index fc795936abf..5324d5dc797 100644 --- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue +++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue @@ -23,6 +23,12 @@ */ export default { + components: { + ciIcon, + }, + directives: { + tooltip, + }, props: { status: { type: Object, @@ -34,12 +40,6 @@ default: true, }, }, - components: { - ciIcon, - }, - directives: { - tooltip, - }, computed: { cssClass() { const className = this.status.group; @@ -53,11 +53,12 @@ :href="status.details_path" :class="cssClass" v-tooltip - :title="!showText ? status.text : ''"> + :title="!showText ? status.text : ''" + > <ci-icon :status="status" /> <template v-if="showText"> - {{status.text}} + {{ status.text }} </template> </a> </template> diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index 2a018f38366..8fea746f4de 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -23,6 +23,9 @@ * - Jobs show view sidebar */ export default { + components: { + icon, + }, props: { status: { type: Object, @@ -30,10 +33,6 @@ }, }, - components: { - icon, - }, - computed: { cssClass() { const status = this.status.group; @@ -43,9 +42,7 @@ }; </script> <template> - <span - :class="cssClass"> - <icon - :name="status.icon"/> + <span :class="cssClass"> + <icon :name="status.icon" /> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue index 3a7143c450e..e18852af6e9 100644 --- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -4,7 +4,7 @@ */ export default { - name: 'clipboardButton', + name: 'ClipboardButton', props: { text: { type: String, @@ -23,10 +23,12 @@ type="button" class="btn btn-transparent btn-clipboard" :data-title="title" - :data-clipboard-text="text"> - <i - aria-hidden="true" - class="fa fa-clipboard"> - </i> + :data-clipboard-text="text" + > + <i + aria-hidden="true" + class="fa fa-clipboard" + > + </i> </button> </template> diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index 59ca9a0a6d4..6d1fe7ee8ca 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -2,9 +2,16 @@ import commitIconSvg from 'icons/_icon_commit.svg'; import userAvatarLink from './user_avatar/user_avatar_link.vue'; import tooltip from '../directives/tooltip'; - import Icon from '../../vue_shared/components/icon.vue'; + import icon from '../../vue_shared/components/icon.vue'; export default { + directives: { + tooltip, + }, + components: { + userAvatarLink, + icon, + }, props: { /** * Indicates the existance of a tag. @@ -103,13 +110,6 @@ this.author.username ? `${this.author.username}'s avatar` : null; }, }, - directives: { - tooltip, - }, - components: { - userAvatarLink, - Icon, - }, created() { this.commitIconSvg = commitIconSvg; }, @@ -118,17 +118,17 @@ <template> <div class="branch-commit"> <template v-if="hasCommitRef && showBranch"> - <div - class="icon-container hidden-xs"> + <div class="icon-container hidden-xs"> <i v-if="tag" class="fa fa-tag" - aria-hidden="true"> + aria-hidden="true" + > </i> <icon v-if="!tag" - name="fork"> - </icon> + name="fork" + /> </div> <a @@ -136,25 +136,29 @@ :href="commitRef.ref_url" v-tooltip data-container="body" - :title="commitRef.name"> - {{commitRef.name}} + :title="commitRef.name" + > + {{ commitRef.name }} </a> </template> <div v-html="commitIconSvg" - class="commit-icon js-commit-icon"> + class="commit-icon js-commit-icon" + > </div> <a class="commit-sha" - :href="commitUrl"> - {{shortSha}} + :href="commitUrl" + > + {{ shortSha }} </a> <div class="commit-title flex-truncate-parent"> <span v-if="title" - class="flex-truncate-child"> + class="flex-truncate-child" + > <user-avatar-link v-if="hasAuthor" class="avatar-image-container" @@ -165,8 +169,9 @@ /> <a class="commit-row-message" - :href="commitUrl"> - {{title}} + :href="commitUrl" + > + {{ title }} </a> </span> <span v-else> diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue index 05e48ed297f..3595a9389e9 100644 --- a/app/assets/javascripts/vue_shared/components/expand_button.vue +++ b/app/assets/javascripts/vue_shared/components/expand_button.vue @@ -11,7 +11,7 @@ * </expand-button> */ export default { - name: 'expandButton', + name: 'ExpandButton', data() { return { isCollapsed: true, diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue index 65c64967fdc..c9d7c0f4999 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/file_icon.vue @@ -16,6 +16,10 @@ */ export default { + components: { + loadingIcon, + icon, + }, props: { fileName: { type: String, @@ -52,10 +56,6 @@ default: '', }, }, - components: { - loadingIcon, - icon, - }, computed: { spriteHref() { const iconName = getIconForFile(this.fileName) || 'file'; @@ -75,9 +75,9 @@ <span> <svg :class="[iconSizeClass, cssClasses]" - v-if="!loading && !folder"> - <use - v-bind="{'xlink:href':spriteHref}"/> + v-if="!loading && !folder" + > + <use v-bind="{ 'xlink:href':spriteHref }" /> </svg> <icon v-if="!loading && folder" diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 2209bc0f9cf..1f72dea1b33 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -1,80 +1,78 @@ <script> -import ciIconBadge from './ci_badge_link.vue'; -import loadingIcon from './loading_icon.vue'; -import timeagoTooltip from './time_ago_tooltip.vue'; -import tooltip from '../directives/tooltip'; -import userAvatarImage from './user_avatar/user_avatar_image.vue'; - -/** - * Renders header component for job and pipeline page based on UI mockups - * - * Used in: - * - job show page - * - pipeline show page - */ -export default { - props: { - status: { - type: Object, - required: true, - }, - itemName: { - type: String, - required: true, - }, - itemId: { - type: Number, - required: true, - }, - time: { - type: String, - required: true, - }, - user: { - type: Object, - required: false, - default: () => ({}), + import ciIconBadge from './ci_badge_link.vue'; + import loadingIcon from './loading_icon.vue'; + import timeagoTooltip from './time_ago_tooltip.vue'; + import tooltip from '../directives/tooltip'; + import userAvatarImage from './user_avatar/user_avatar_image.vue'; + + /** + * Renders header component for job and pipeline page based on UI mockups + * + * Used in: + * - job show page + * - pipeline show page + */ + export default { + components: { + ciIconBadge, + loadingIcon, + timeagoTooltip, + userAvatarImage, }, - actions: { - type: Array, - required: false, - default: () => [], + directives: { + tooltip, }, - hasSidebarButton: { - type: Boolean, - required: false, - default: false, + props: { + status: { + type: Object, + required: true, + }, + itemName: { + type: String, + required: true, + }, + itemId: { + type: Number, + required: true, + }, + time: { + type: String, + required: true, + }, + user: { + type: Object, + required: false, + default: () => ({}), + }, + actions: { + type: Array, + required: false, + default: () => [], + }, + hasSidebarButton: { + type: Boolean, + required: false, + default: false, + }, + shouldRenderTriggeredLabel: { + type: Boolean, + required: false, + default: true, + }, }, - shouldRenderTriggeredLabel: { - type: Boolean, - required: false, - default: true, - }, - }, - - directives: { - tooltip, - }, - - components: { - ciIconBadge, - loadingIcon, - timeagoTooltip, - userAvatarImage, - }, - computed: { - userAvatarAltText() { - return `${this.user.name}'s avatar`; + computed: { + userAvatarAltText() { + return `${this.user.name}'s avatar`; + }, }, - }, - methods: { - onClickAction(action) { - this.$emit('actionClicked', action); + methods: { + onClickAction(action) { + this.$emit('actionClicked', action); + }, }, - }, -}; + }; </script> <template> @@ -84,7 +82,7 @@ export default { <ci-icon-badge :status="status" /> <strong> - {{itemName}} #{{itemId}} + {{ itemName }} #{{ itemId }} </strong> <template v-if="shouldRenderTriggeredLabel"> @@ -103,16 +101,17 @@ export default { v-tooltip :href="user.path" :title="user.email" - class="js-user-link commit-committer-link"> + class="js-user-link commit-committer-link" + > <user-avatar-image :img-src="user.avatar_url" :img-alt="userAvatarAltText" :tooltip-text="user.name" :img-size="24" - /> + /> - {{user.name}} + {{ user.name }} </a> </template> </section> @@ -121,12 +120,15 @@ export default { class="header-action-buttons" v-if="actions.length"> <template - v-for="action in actions"> + v-for="(action, i) in actions" + > <a v-if="action.type === 'link'" :href="action.path" - :class="action.cssClass"> - {{action.label}} + :class="action.cssClass" + :key="i" + > + {{ action.label }} </a> <a @@ -134,8 +136,10 @@ export default { :href="action.path" data-method="post" rel="nofollow" - :class="action.cssClass"> - {{action.label}} + :class="action.cssClass" + :key="i" + > + {{ action.label }} </a> <button @@ -143,25 +147,31 @@ export default { @click="onClickAction(action)" :disabled="action.isLoading" :class="action.cssClass" - type="button"> - {{action.label}} + type="button" + :key="i" + > + {{ action.label }} <i v-show="action.isLoading" class="fa fa-spin fa-spinner" - aria-hidden="true"> + aria-hidden="true" + > </i> </button> </template> <button v-if="hasSidebarButton" type="button" - class="btn btn-default visible-xs-block visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header" + class="btn btn-default visible-xs-block +visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header" aria-label="Toggle Sidebar" - id="toggleSidebar"> + id="toggleSidebar" + > <i class="fa fa-angle-double-left" aria-hidden="true" - aria-labelledby="toggleSidebar"> + aria-labelledby="toggleSidebar" + > </i> </button> </section> diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue index 365229ea274..6a2e05000e1 100644 --- a/app/assets/javascripts/vue_shared/components/icon.vue +++ b/app/assets/javascripts/vue_shared/components/icon.vue @@ -1,17 +1,17 @@ <script> -/* This is a re-usable vue component for rendering a svg sprite - icon + /* This is a re-usable vue component for rendering a svg sprite + icon - Sample configuration: + Sample configuration: - <icon - name="retry" - :size="32" - css-classes="top" - /> + <icon + name="retry" + :size="32" + css-classes="top" + /> -*/ + */ // only allow classes in images.scss e.g. s12 const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; @@ -80,7 +80,6 @@ :height="height" :x="x" :y="y"> - <use - v-bind="{'xlink:href':spriteHref}"/> + <use v-bind="{ 'xlink:href':spriteHref }" /> </svg> </template> diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue index 7cf2e029cf6..0a30f467b08 100644 --- a/app/assets/javascripts/vue_shared/components/identicon.vue +++ b/app/assets/javascripts/vue_shared/components/identicon.vue @@ -46,6 +46,6 @@ export default { class="avatar identicon" :class="sizeClass" :style="identiconStyles"> - {{identiconTitle}} + {{ identiconTitle }} </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue index 564fc5029af..b48828ae81f 100644 --- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue +++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue @@ -1,7 +1,10 @@ <script> - import Icon from '../../../vue_shared/components/icon.vue'; + import icon from '../../../vue_shared/components/icon.vue'; export default { + components: { + icon, + }, props: { isLocked: { type: Boolean, @@ -16,10 +19,6 @@ }, }, - components: { - Icon, - }, - computed: { warningIcon() { if (this.isConfidential) return 'eye-slash'; @@ -37,16 +36,17 @@ <template> <div class="issuable-note-warning"> <icon - :name="warningIcon" - :size="16" - class="icon inline" - aria-hidden="true" - v-if="!isLockedAndConfidential"> - </icon> + :name="warningIcon" + :size="16" + class="icon inline" + aria-hidden="true" + v-if="!isLockedAndConfidential" + /> <span v-if="isLockedAndConfidential"> {{ __('This issue is confidential and locked.') }} - {{ __('People without permission will never get a notification and won\'t be able to comment.') }} + {{ __(`People without permission will never +get a notification and won't be able to comment.`) }} </span> <span v-else-if="isConfidential"> diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue index 247943f83e6..ff8c0f7c1d2 100644 --- a/app/assets/javascripts/vue_shared/components/loading_button.vue +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -1,55 +1,56 @@ <script> + /* eslint-disable vue/require-default-prop */ -/* This is a re-usable vue component for rendering a button - that will probably be sending off ajax requests and need - to show the loading status by setting the `loading` option. - This can also be used for initial page load when you don't - know the action of the button yet by setting - `loading: true, label: undefined`. + /* This is a re-usable vue component for rendering a button + that will probably be sending off ajax requests and need + to show the loading status by setting the `loading` option. + This can also be used for initial page load when you don't + know the action of the button yet by setting + `loading: true, label: undefined`. - Sample configuration: + Sample configuration: - <loading-button - :loading="true" - :label="Hello" - @click="..." - /> + <loading-button + :loading="true" + :label="Hello" + @click="..." + /> -*/ + */ -import loadingIcon from './loading_icon.vue'; + import loadingIcon from './loading_icon.vue'; -export default { - props: { - loading: { - type: Boolean, - required: false, - default: false, + export default { + components: { + loadingIcon, }, - disabled: { - type: Boolean, - required: false, - default: false, + props: { + loading: { + type: Boolean, + required: false, + default: false, + }, + disabled: { + type: Boolean, + required: false, + default: false, + }, + label: { + type: String, + required: false, + }, + containerClass: { + type: String, + required: false, + default: 'btn btn-align-content', + }, }, - label: { - type: String, - required: false, + methods: { + onClick(e) { + this.$emit('click', e); + }, }, - containerClass: { - type: String, - required: false, - default: 'btn btn-align-content', - }, - }, - components: { - loadingIcon, - }, - methods: { - onClick(e) { - this.$emit('click', e); - }, - }, -}; + }; </script> <template> @@ -59,23 +60,23 @@ export default { :class="containerClass" :disabled="loading || disabled" > - <transition name="fade"> - <loading-icon - v-if="loading" - :inline="true" - class="js-loading-button-icon" - :class="{ - 'append-right-5': label - }" - /> - </transition> - <transition name="fade"> - <span - v-if="label" - class="js-loading-button-label" - > - {{ label }} - </span> - </transition> + <transition name="fade"> + <loading-icon + v-if="loading" + :inline="true" + class="js-loading-button-icon" + :class="{ + 'append-right-5': label + }" + /> + </transition> + <transition name="fade"> + <span + v-if="label" + class="js-loading-button-label" + > + {{ label }} + </span> + </transition> </button> </template> diff --git a/app/assets/javascripts/vue_shared/components/loading_icon.vue b/app/assets/javascripts/vue_shared/components/loading_icon.vue index 15581d5c2a0..1eba117b18f 100644 --- a/app/assets/javascripts/vue_shared/components/loading_icon.vue +++ b/app/assets/javascripts/vue_shared/components/loading_icon.vue @@ -32,13 +32,14 @@ </script> <template> <component - :is="this.rootElementType" + :is="rootElementType" class="text-center"> <i class="fa fa-spin fa-spinner" :class="cssClass" aria-hidden="true" - :aria-label="label"> + :aria-label="label" + > </i> </component> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 15e3d713448..1371dca0c35 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -6,6 +6,11 @@ import icon from '../icon.vue'; export default { + components: { + markdownHeader, + markdownToolbar, + icon, + }, props: { markdownPreviewPath: { type: String, @@ -24,6 +29,7 @@ quickActionsDocsPath: { type: String, required: false, + default: '', }, canAttachFile: { type: Boolean, @@ -45,17 +51,24 @@ previewMarkdown: false, }; }, - components: { - markdownHeader, - markdownToolbar, - icon, - }, computed: { shouldShowReferencedUsers() { const referencedUsersThreshold = 10; return this.referencedUsers.length >= referencedUsersThreshold; }, }, + mounted() { + /* + GLForm class handles all the toolbar buttons + */ + return new GLForm($(this.$refs['gl-form']), this.enableAutocomplete); + }, + beforeDestroy() { + const glForm = $(this.$refs['gl-form']).data('gl-form'); + if (glForm) { + glForm.destroy(); + } + }, methods: { showPreviewTab() { if (this.previewMarkdown) return; @@ -98,18 +111,6 @@ }); }, }, - mounted() { - /* - GLForm class handles all the toolbar buttons - */ - return new GLForm($(this.$refs['gl-form']), this.enableAutocomplete); - }, - beforeDestroy() { - const glForm = $(this.$refs['gl-form']).data('gl-form'); - if (glForm) { - glForm.destroy(); - } - }, }; </script> @@ -121,34 +122,39 @@ <markdown-header :preview-markdown="previewMarkdown" @preview-markdown="showPreviewTab" - @write-markdown="showWriteTab" /> + @write-markdown="showWriteTab" + /> <div class="md-write-holder" - v-show="!previewMarkdown"> + v-show="!previewMarkdown" + > <div class="zen-backdrop"> <slot name="textarea"></slot> <a class="zen-control zen-control-leave js-zen-leave" href="#" - aria-label="Enter zen mode"> + aria-label="Enter zen mode" + > <icon name="screen-normal" - :size="32"> - </icon> + :size="32" + /> </a> <markdown-toolbar :markdown-docs-path="markdownDocsPath" :quick-actions-docs-path="quickActionsDocsPath" :can-attach-file="canAttachFile" - /> + /> </div> </div> <div class="md md-preview-holder md-preview" - v-show="previewMarkdown"> + v-show="previewMarkdown" + > <div ref="markdown-preview" - v-html="markdownPreview"> + v-html="markdownPreview" + > </div> <span v-if="markdownPreviewLoading"> Loading... @@ -158,23 +164,27 @@ <div v-if="referencedCommands" v-html="referencedCommands" - class="referenced-commands"></div> + class="referenced-commands" + > + </div> <div v-if="shouldShowReferencedUsers" - class="referenced-users"> - <span> - <i - class="fa fa-exclamation-triangle" - aria-hidden="true"> - </i> - You are about to add - <strong> - <span class="js-referenced-users-count"> - {{referencedUsers.length}} - </span> - </strong> people to the discussion. Proceed with caution. - </span> - </div> + class="referenced-users" + > + <span> + <i + class="fa fa-exclamation-triangle" + aria-hidden="true" + > + </i> + You are about to add + <strong> + <span class="js-referenced-users-count"> + {{ referencedUsers.length }} + </span> + </strong> people to the discussion. Proceed with caution. + </span> + </div> </template> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 36d2d1dc164..f65eab11a27 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -4,18 +4,26 @@ import icon from '../icon.vue'; export default { + directives: { + tooltip, + }, + components: { + toolbarButton, + icon, + }, props: { previewMarkdown: { type: Boolean, required: true, }, }, - directives: { - tooltip, + mounted() { + $(document).on('markdown-preview:show.vue', this.previewMarkdownTab); + $(document).on('markdown-preview:hide.vue', this.writeMarkdownTab); }, - components: { - toolbarButton, - icon, + beforeDestroy() { + $(document).off('markdown-preview:show.vue', this.previewMarkdownTab); + $(document).off('markdown-preview:hide.vue', this.writeMarkdownTab); }, methods: { isMarkdownForm(form) { @@ -36,14 +44,6 @@ this.$emit('write-markdown'); }, }, - mounted() { - $(document).on('markdown-preview:show.vue', this.previewMarkdownTab); - $(document).on('markdown-preview:hide.vue', this.writeMarkdownTab); - }, - beforeDestroy() { - $(document).off('markdown-preview:show.vue', this.previewMarkdownTab); - $(document).off('markdown-preview:hide.vue', this.writeMarkdownTab); - }, }; </script> @@ -52,12 +52,14 @@ <ul class="nav-links clearfix"> <li class="md-header-tab" - :class="{ active: !previewMarkdown }"> + :class="{ active: !previewMarkdown }" + > <a class="js-write-link" href="#md-write-holder" tabindex="-1" - @click.prevent="writeMarkdownTab($event)"> + @click.prevent="writeMarkdownTab($event)" + > Write </a> </li> @@ -68,46 +70,55 @@ class="js-preview-link" href="#md-preview-holder" tabindex="-1" - @click.prevent="previewMarkdownTab($event)"> + @click.prevent="previewMarkdownTab($event)" + > Preview </a> </li> <li class="md-header-toolbar" - :class="{ active: !previewMarkdown }"> + :class="{ active: !previewMarkdown }" + > <toolbar-button tag="**" button-title="Add bold text" - icon="bold" /> + icon="bold" + /> <toolbar-button tag="*" button-title="Add italic text" - icon="italic" /> + icon="italic" + /> <toolbar-button tag="> " :prepend="true" button-title="Insert a quote" - icon="quote" /> + icon="quote" + /> <toolbar-button tag="`" tag-block="```" button-title="Insert code" - icon="code" /> + icon="code" + /> <toolbar-button tag="* " :prepend="true" button-title="Add a bullet list" - icon="list-bulleted" /> + icon="list-bulleted" + /> <toolbar-button tag="1. " :prepend="true" button-title="Add a numbered list" - icon="list-numbered" /> + icon="list-numbered" + /> <toolbar-button tag="* [ ] " :prepend="true" button-title="Add a task list" - icon="task-done" /> + icon="task-done" + /> <button v-tooltip aria-label="Go full screen" @@ -115,10 +126,11 @@ data-container="body" tabindex="-1" title="Go full screen" - type="button"> + type="button" + > <icon - name="screen-full"> - </icon> + name="screen-full" + /> </button> </li> </ul> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index ea2509d2839..c0ee88bbf72 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -8,6 +8,7 @@ quickActionsDocsPath: { type: String, required: false, + default: '', }, canAttachFile: { type: Boolean, @@ -15,32 +16,40 @@ default: true, }, }, + computed: { + hasQuickActionsDocsPath() { + return this.quickActionsDocsPath !== ''; + }, + }, }; </script> <template> <div class="comment-toolbar clearfix"> <div class="toolbar-text"> - <template v-if="!quickActionsDocsPath && markdownDocsPath"> + <template v-if="!hasQuickActionsDocsPath && markdownDocsPath"> <a :href="markdownDocsPath" target="_blank" - tabindex="-1"> + tabindex="-1" + > Markdown is supported </a> </template> - <template v-if="quickActionsDocsPath && markdownDocsPath"> - <a + <template v-if="hasQuickActionsDocsPath && markdownDocsPath"> + <a :href="markdownDocsPath" target="_blank" - tabindex="-1"> + tabindex="-1" + > Markdown </a> and - <a + <a :href="quickActionsDocsPath" target="_blank" - tabindex="-1"> + tabindex="-1" + > quick actions </a> are supported @@ -53,46 +62,58 @@ <span class="uploading-progress-container hide"> <i class="fa fa-file-image-o toolbar-button-icon" - aria-hidden="true"></i> + aria-hidden="true" + > + </i> <span class="attaching-file-message"></span> <span class="uploading-progress">0%</span> <span class="uploading-spinner"> <i class="fa fa-spinner fa-spin toolbar-button-icon" - aria-hidden="true"></i> + aria-hidden="true" + > + </i> </span> </span> <span class="uploading-error-container hide"> <span class="uploading-error-icon"> <i class="fa fa-file-image-o toolbar-button-icon" - aria-hidden="true"></i> + aria-hidden="true" + > + </i> </span> <span class="uploading-error-message"></span> <button class="retry-uploading-link" - type="button"> - Try again + type="button" + > + Try again </button> or <button class="attach-new-file markdown-selector" - type="button"> + type="button" + > attach a new file </button> </span> <button class="markdown-selector button-attach-file" tabindex="-1" - type="button"> + type="button" + > <i class="fa fa-file-image-o toolbar-button-icon" - aria-hidden="true"></i> + aria-hidden="true" + > + </i> Attach a file </button> <button class="btn btn-default btn-xs hide button-cancel-uploading-files" - type="button"> + type="button" + > Cancel </button> </span> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index e3e41f8f0ca..2d2d69ebeb2 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -3,6 +3,12 @@ import icon from '../icon.vue'; export default { + components: { + icon, + }, + directives: { + tooltip, + }, props: { buttonTitle: { type: String, @@ -27,12 +33,6 @@ default: false, }, }, - components: { - icon, - }, - directives: { - tooltip, - }, }; </script> @@ -47,9 +47,10 @@ :data-md-block="tagBlock" :data-md-prepend="prepend" :title="buttonTitle" - :aria-label="buttonTitle"> + :aria-label="buttonTitle" + > <icon - :name="icon"> - </icon> + :name="icon" + /> </button> </template> diff --git a/app/assets/javascripts/vue_shared/components/modal.vue b/app/assets/javascripts/vue_shared/components/modal.vue index 00089dfef38..c103c45c7dd 100644 --- a/app/assets/javascripts/vue_shared/components/modal.vue +++ b/app/assets/javascripts/vue_shared/components/modal.vue @@ -1,143 +1,153 @@ <script> -export default { - name: 'modal', + /* eslint-disable vue/require-default-prop */ + export default { + name: 'Modal', - props: { - id: { - type: String, - required: false, + props: { + id: { + type: String, + required: false, + }, + title: { + type: String, + required: false, + }, + text: { + type: String, + required: false, + }, + hideFooter: { + type: Boolean, + required: false, + default: false, + }, + kind: { + type: String, + required: false, + default: 'primary', + }, + modalDialogClass: { + type: String, + required: false, + default: '', + }, + closeKind: { + type: String, + required: false, + default: 'default', + }, + closeButtonLabel: { + type: String, + required: false, + default: 'Cancel', + }, + primaryButtonLabel: { + type: String, + required: false, + default: '', + }, + submitDisabled: { + type: Boolean, + required: false, + default: false, + }, }, - title: { - type: String, - required: false, - }, - text: { - type: String, - required: false, - }, - hideFooter: { - type: Boolean, - required: false, - default: false, - }, - kind: { - type: String, - required: false, - default: 'primary', - }, - modalDialogClass: { - type: String, - required: false, - default: '', - }, - closeKind: { - type: String, - required: false, - default: 'default', - }, - closeButtonLabel: { - type: String, - required: false, - default: 'Cancel', - }, - primaryButtonLabel: { - type: String, - required: false, - default: '', - }, - submitDisabled: { - type: Boolean, - required: false, - default: false, - }, - }, - computed: { - btnKindClass() { - return { - [`btn-${this.kind}`]: true, - }; + computed: { + btnKindClass() { + return { + [`btn-${this.kind}`]: true, + }; + }, + btnCancelKindClass() { + return { + [`btn-${this.closeKind}`]: true, + }; + }, }, - btnCancelKindClass() { - return { - [`btn-${this.closeKind}`]: true, - }; - }, - }, - methods: { - emitCancel(event) { - this.$emit('cancel', event); - }, - emitSubmit(event) { - this.$emit('submit', event); + methods: { + emitCancel(event) { + this.$emit('cancel', event); + }, + emitSubmit(event) { + this.$emit('submit', event); + }, }, - }, -}; + }; </script> <template> -<div class="modal-open"> - <div - :id="id" - class="modal" - :class="id ? '' : 'show'" - role="dialog" - tabindex="-1" - > + <div class="modal-open"> <div - :class="modalDialogClass" - class="modal-dialog" - role="document" + :id="id" + class="modal" + :class="id ? '' : 'show'" + role="dialog" + tabindex="-1" > - <div class="modal-content"> - <div class="modal-header"> - <slot name="header"> - <h4 class="modal-title pull-left"> - {{this.title}} - </h4> + <div + :class="modalDialogClass" + class="modal-dialog" + role="document" + > + <div class="modal-content"> + <div class="modal-header"> + <slot name="header"> + <h4 class="modal-title pull-left"> + {{ title }} + </h4> + <button + type="button" + class="close pull-right" + @click="emitCancel($event)" + data-dismiss="modal" + aria-label="Close" + > + <span aria-hidden="true">×</span> + </button> + </slot> + </div> + <div class="modal-body"> + <slot + name="body" + :text="text" + > + <p>{{ text }}</p> + </slot> + </div> + <div + class="modal-footer" + v-if="!hideFooter" + > <button type="button" - class="close pull-right" + class="btn pull-left" + :class="btnCancelKindClass" @click="emitCancel($event)" data-dismiss="modal" - aria-label="Close" > - <span aria-hidden="true">×</span> + {{ closeButtonLabel }} </button> - </slot> - </div> - <div class="modal-body"> - <slot name="body" :text="text"> - <p>{{this.text}}</p> - </slot> - </div> - <div class="modal-footer" v-if="!hideFooter"> - <button - type="button" - class="btn pull-left" - :class="btnCancelKindClass" - @click="emitCancel($event)" - data-dismiss="modal"> - {{ closeButtonLabel }} - </button> - <button - v-if="primaryButtonLabel" - type="button" - class="btn pull-right js-primary-button" - :disabled="submitDisabled" - :class="btnKindClass" - @click="emitSubmit($event)" - data-dismiss="modal"> - {{ primaryButtonLabel }} - </button> + <button + v-if="primaryButtonLabel" + type="button" + class="btn pull-right js-primary-button" + :disabled="submitDisabled" + :class="btnKindClass" + @click="emitSubmit($event)" + data-dismiss="modal" + > + {{ primaryButtonLabel }} + </button> + </div> </div> </div> </div> + <div + v-if="!id" + class="modal-backdrop fade in" + > + </div> </div> - <div - v-if="!id" - class="modal-backdrop fade in"> - </div> -</div> </template> diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue index a2ddd565170..cb8e6072a9b 100644 --- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue +++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue @@ -45,7 +45,7 @@ this.$emit('onChangeTab', tab.scope); }, }, -}; + }; </script> <template> <ul class="nav-links scrolling-tabs"> @@ -55,21 +55,20 @@ :class="{ active: tab.isActive, }" - > + > <a role="button" @click="onTabClick(tab)" :class="`js-${scope}-tab-${tab.scope}`" - > + > {{ tab.name }} <span v-if="shouldRenderBadge(tab.count)" class="badge" - > - {{tab.count}} + > + {{ tab.count }} </span> - </a> </li> </ul> diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue index e467ca56704..50b1508691b 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -20,16 +20,16 @@ import userAvatarLink from '../user_avatar/user_avatar_link.vue'; export default { - name: 'placeholderNote', + name: 'PlaceholderNote', + components: { + userAvatarLink, + }, props: { note: { type: Object, required: true, }, }, - components: { - userAvatarLink, - }, computed: { ...mapGetters([ 'getUserData', @@ -46,7 +46,7 @@ :link-href="getUserData.path" :img-src="getUserData.avatar_url" :img-size="40" - /> + /> </div> <div :class="{ discussion: !note.individual_note }" @@ -54,14 +54,14 @@ <div class="note-header"> <div class="note-header-info"> <a :href="getUserData.path"> - <span class="hidden-xs">{{getUserData.name}}</span> - <span class="note-headline-light">@{{getUserData.username}}</span> + <span class="hidden-xs">{{ getUserData.name }}</span> + <span class="note-headline-light">@{{ getUserData.username }}</span> </a> </div> </div> <div class="note-body"> <div class="note-text"> - <p>{{note.body}}</p> + <p>{{ note.body }}</p> </div> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue index d805fea8006..95e2b38e292 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue @@ -8,7 +8,7 @@ * /> */ export default { - name: 'placeholderSystemNote', + name: 'PlaceholderSystemNote', props: { note: { type: Object, @@ -20,10 +20,10 @@ <template> <li class="note system-note timeline-entry being-posted fade-in-half"> - <div class="timeline-entry-inner"> - <div class="timeline-content"> - <em>{{note.body}}</em> - </div> - </div> + <div class="timeline-entry-inner"> + <div class="timeline-content"> + <em>{{ note.body }}</em> + </div> + </div> </li> </template> diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 2248699c399..aac10f84087 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -21,16 +21,16 @@ import { spriteIcon } from '../../../lib/utils/common_utils'; export default { - name: 'systemNote', + name: 'SystemNote', + components: { + noteHeader, + }, props: { note: { type: Object, required: true, }, }, - components: { - noteHeader, - }, computed: { ...mapGetters([ 'targetNoteHash', diff --git a/app/assets/javascripts/vue_shared/components/panel_resizer.vue b/app/assets/javascripts/vue_shared/components/panel_resizer.vue index 4371534d345..abbe9a22717 100644 --- a/app/assets/javascripts/vue_shared/components/panel_resizer.vue +++ b/app/assets/javascripts/vue_shared/components/panel_resizer.vue @@ -1,87 +1,87 @@ <script> -export default { - props: { - startSize: { - type: Number, - required: true, + export default { + props: { + startSize: { + type: Number, + required: true, + }, + side: { + type: String, + required: true, + }, + minSize: { + type: Number, + required: false, + default: 0, + }, + maxSize: { + type: Number, + required: false, + default: Number.MAX_VALUE, + }, + enabled: { + type: Boolean, + required: false, + default: true, + }, }, - side: { - type: String, - required: true, + data() { + return { + size: this.startSize, + }; }, - minSize: { - type: Number, - required: false, - default: 0, + computed: { + className() { + return `drag${this.side}`; + }, + cursorStyle() { + if (this.enabled) { + return { cursor: 'ew-resize' }; + } + return {}; + }, }, - maxSize: { - type: Number, - required: false, - default: Number.MAX_VALUE, - }, - enabled: { - type: Boolean, - required: false, - default: true, - }, - }, - data() { - return { - size: this.startSize, - }; - }, - computed: { - className() { - return `drag${this.side}`; - }, - cursorStyle() { - if (this.enabled) { - return { cursor: 'ew-resize' }; - } - return {}; - }, - }, - methods: { - resetSize(e) { - e.preventDefault(); - this.size = this.startSize; - this.$emit('update:size', this.size); - }, - startDrag(e) { - if (this.enabled) { + methods: { + resetSize(e) { e.preventDefault(); - this.startPos = e.clientX; - this.currentStartSize = this.size; - document.addEventListener('mousemove', this.drag); - document.addEventListener('mouseup', this.endDrag, { once: true }); - this.$emit('resize-start', this.size); - } - }, - drag(e) { - e.preventDefault(); - let moved = e.clientX - this.startPos; - if (this.side === 'left') moved = -moved; - let newSize = this.currentStartSize + moved; - if (newSize < this.minSize) { - newSize = this.minSize; - } else if (newSize > this.maxSize) { - newSize = this.maxSize; - } - this.size = newSize; + this.size = this.startSize; + this.$emit('update:size', this.size); + }, + startDrag(e) { + if (this.enabled) { + e.preventDefault(); + this.startPos = e.clientX; + this.currentStartSize = this.size; + document.addEventListener('mousemove', this.drag); + document.addEventListener('mouseup', this.endDrag, { once: true }); + this.$emit('resize-start', this.size); + } + }, + drag(e) { + e.preventDefault(); + let moved = e.clientX - this.startPos; + if (this.side === 'left') moved = -moved; + let newSize = this.currentStartSize + moved; + if (newSize < this.minSize) { + newSize = this.minSize; + } else if (newSize > this.maxSize) { + newSize = this.maxSize; + } + this.size = newSize; - this.$emit('update:size', newSize); - }, - endDrag(e) { - e.preventDefault(); - document.removeEventListener('mousemove', this.drag); - this.$emit('resize-end', this.size); + this.$emit('update:size', newSize); + }, + endDrag(e) { + e.preventDefault(); + document.removeEventListener('mousemove', this.drag); + this.$emit('resize-end', this.size); + }, }, - }, -}; + }; </script> <template> - <div + <div class="dragHandle" :class="className" :style="cursorStyle" diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue index d8d974a2ff7..bfeece12077 100644 --- a/app/assets/javascripts/vue_shared/components/pikaday.vue +++ b/app/assets/javascripts/vue_shared/components/pikaday.vue @@ -3,7 +3,7 @@ import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix'; export default { - name: 'datePicker', + name: 'DatePicker', props: { label: { type: String, @@ -13,22 +13,17 @@ selectedDate: { type: Date, required: false, + default: null, }, minDate: { type: Date, required: false, + default: null, }, maxDate: { type: Date, required: false, - }, - }, - methods: { - selected(dateText) { - this.$emit('newDateSelected', this.calendar.toString(dateText)); - }, - toggled() { - this.$emit('hidePicker'); + default: null, }, }, mounted() { @@ -53,6 +48,14 @@ beforeDestroy() { this.calendar.destroy(); }, + methods: { + selected(dateText) { + this.$emit('newDateSelected', this.calendar.toString(dateText)); + }, + toggled() { + this.$emit('hidePicker'); + }, + }, }; </script> @@ -66,7 +69,7 @@ @click="toggled" > <span class="dropdown-toggle-text"> - {{label}} + {{ label }} </span> <i class="fa fa-chevron-down" diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue index dce23bd65f6..279cc1de5bb 100644 --- a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue +++ b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue @@ -1,85 +1,85 @@ <script> -/* This is a re-usable vue component for rendering a project avatar that - does not need to link to the project's profile. The image and an optional - tooltip can be configured by props passed to this component. + /* This is a re-usable vue component for rendering a project avatar that + does not need to link to the project's profile. The image and an optional + tooltip can be configured by props passed to this component. - Sample configuration: + Sample configuration: - <project-avatar-image - :lazy="true" - :img-src="projectAvatarSrc" - :img-alt="tooltipText" - :tooltip-text="tooltipText" - tooltip-placement="top" - /> + <project-avatar-image + :lazy="true" + :img-src="projectAvatarSrc" + :img-alt="tooltipText" + :tooltip-text="tooltipText" + tooltip-placement="top" + /> -*/ + */ -import defaultAvatarUrl from 'images/no_avatar.png'; -import { placeholderImage } from '../../../lazy_loader'; -import tooltip from '../../directives/tooltip'; + import defaultAvatarUrl from 'images/no_avatar.png'; + import { placeholderImage } from '../../../lazy_loader'; + import tooltip from '../../directives/tooltip'; -export default { - name: 'ProjectAvatarImage', - props: { - lazy: { - type: Boolean, - required: false, - default: false, - }, - imgSrc: { - type: String, - required: false, - default: defaultAvatarUrl, - }, - cssClasses: { - type: String, - required: false, - default: '', - }, - imgAlt: { - type: String, - required: false, - default: 'project avatar', - }, - size: { - type: Number, - required: false, - default: 20, - }, - tooltipText: { - type: String, - required: false, - default: '', - }, - tooltipPlacement: { - type: String, - required: false, - default: 'top', - }, - }, - directives: { - tooltip, - }, - computed: { - // API response sends null when gravatar is disabled and - // we provide an empty string when we use it inside project avatar link. - // In both cases we should render the defaultAvatarUrl - sanitizedSource() { - return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc; - }, - resultantSrcAttribute() { - return this.lazy ? placeholderImage : this.sanitizedSource; + export default { + name: 'ProjectAvatarImage', + directives: { + tooltip, }, - tooltipContainer() { - return this.tooltipText ? 'body' : null; + props: { + lazy: { + type: Boolean, + required: false, + default: false, + }, + imgSrc: { + type: String, + required: false, + default: defaultAvatarUrl, + }, + cssClasses: { + type: String, + required: false, + default: '', + }, + imgAlt: { + type: String, + required: false, + default: 'project avatar', + }, + size: { + type: Number, + required: false, + default: 20, + }, + tooltipText: { + type: String, + required: false, + default: '', + }, + tooltipPlacement: { + type: String, + required: false, + default: 'top', + }, }, - avatarSizeClass() { - return `s${this.size}`; + computed: { + // API response sends null when gravatar is disabled and + // we provide an empty string when we use it inside project avatar link. + // In both cases we should render the defaultAvatarUrl + sanitizedSource() { + return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc; + }, + resultantSrcAttribute() { + return this.lazy ? placeholderImage : this.sanitizedSource; + }, + tooltipContainer() { + return this.tooltipText ? 'body' : null; + }, + avatarSizeClass() { + return `s${this.size}`; + }, }, - }, -}; + }; </script> <template> @@ -87,7 +87,7 @@ export default { v-tooltip class="avatar" :class="{ - lazy, + lazy: lazy, [avatarSizeClass]: true, [cssClasses]: true }" diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue index 16d60bb2876..c35621c9ef3 100644 --- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue +++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue @@ -1,85 +1,86 @@ <script> -import modal from './modal.vue'; + import modal from './modal.vue'; -export default { - name: 'recaptcha-modal', + export default { + name: 'RecaptchaModal', - props: { - html: { - type: String, - required: false, - default: '', + components: { + modal, }, - }, - data() { - return { - script: {}, - scriptSrc: 'https://www.google.com/recaptcha/api.js', - }; - }, + props: { + html: { + type: String, + required: false, + default: '', + }, + }, - components: { - modal, - }, + data() { + return { + script: {}, + scriptSrc: 'https://www.google.com/recaptcha/api.js', + }; + }, - methods: { - appendRecaptchaScript() { - this.removeRecaptchaScript(); + watch: { + html() { + this.appendRecaptchaScript(); + }, + }, - const script = document.createElement('script'); - script.src = this.scriptSrc; - script.classList.add('js-recaptcha-script'); - script.async = true; - script.defer = true; + mounted() { + window.recaptchaDialogCallback = this.submit.bind(this); + }, - this.script = script; + methods: { + appendRecaptchaScript() { + this.removeRecaptchaScript(); - document.body.appendChild(script); - }, + const script = document.createElement('script'); + script.src = this.scriptSrc; + script.classList.add('js-recaptcha-script'); + script.async = true; + script.defer = true; - removeRecaptchaScript() { - if (this.script instanceof Element) this.script.remove(); - }, + this.script = script; - close() { - this.removeRecaptchaScript(); - this.$emit('close'); - }, + document.body.appendChild(script); + }, - submit() { - this.$el.querySelector('form').submit(); - }, - }, + removeRecaptchaScript() { + if (this.script instanceof Element) this.script.remove(); + }, - watch: { - html() { - this.appendRecaptchaScript(); - }, - }, + close() { + this.removeRecaptchaScript(); + this.$emit('close'); + }, - mounted() { - window.recaptchaDialogCallback = this.submit.bind(this); - }, -}; + submit() { + this.$el.querySelector('form').submit(); + }, + }, + }; </script> <template> -<modal - kind="warning" - class="recaptcha-modal js-recaptcha-modal" - :hide-footer="true" - :title="__('Please solve the reCAPTCHA')" - @cancel="close" -> - <div slot="body"> - <p> - {{__('We want to be sure it is you, please confirm you are not a robot.')}} - </p> - <div - ref="recaptcha" - v-html="html" - ></div> - </div> -</modal> + <modal + kind="warning" + class="recaptcha-modal js-recaptcha-modal" + :hide-footer="true" + :title="__('Please solve the reCAPTCHA')" + @cancel="close" + > + <div slot="body"> + <p> + {{ __('We want to be sure it is you, please confirm you are not a robot.') }} + </p> + <div + ref="recaptcha" + v-html="html" + > + </div> + </div> + </modal> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue index a88e1310131..7f1eb6bcec4 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue @@ -1,6 +1,6 @@ <script> export default { - name: 'collapsedCalendarIcon', + name: 'CollapsedCalendarIcon', props: { containerClass: { type: String, diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue index 9ede5553bc5..dac438a702d 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue @@ -4,7 +4,11 @@ import collapsedCalendarIcon from './collapsed_calendar_icon.vue'; export default { - name: 'sidebarCollapsedGroupedDatePicker', + name: 'SidebarCollapsedGroupedDatePicker', + components: { + toggleSidebar, + collapsedCalendarIcon, + }, props: { collapsed: { type: Boolean, @@ -19,10 +23,12 @@ minDate: { type: Date, required: false, + default: null, }, maxDate: { type: Date, required: false, + default: null, }, disableClickableIcons: { type: Boolean, @@ -30,10 +36,6 @@ default: false, }, }, - components: { - toggleSidebar, - collapsedCalendarIcon, - }, computed: { hasMinAndMaxDates() { return this.minDate && this.maxDate; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue index 9c3413377a3..1413dd69f24 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue @@ -6,7 +6,13 @@ import { dateInWords } from '../../../lib/utils/datetime_utility'; export default { - name: 'sidebarDatePicker', + name: 'SidebarDatePicker', + components: { + datePicker, + toggleSidebar, + loadingIcon, + collapsedCalendarIcon, + }, props: { collapsed: { type: Boolean, @@ -36,14 +42,17 @@ selectedDate: { type: Date, required: false, + default: null, }, minDate: { type: Date, required: false, + default: null, }, maxDate: { type: Date, required: false, + default: null, }, }, data() { @@ -51,12 +60,6 @@ editing: false, }; }, - components: { - datePicker, - toggleSidebar, - loadingIcon, - collapsedCalendarIcon, - }, computed: { selectedAndEditable() { return this.selectedDate && this.editable; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue index 5ae76adad71..8211d425b1f 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue @@ -1,6 +1,6 @@ <script> export default { - name: 'toggleSidebar', + name: 'ToggleSidebar', props: { collapsed: { type: Boolean, @@ -24,7 +24,11 @@ <i aria-label="toggle collapse" class="fa" - :class="{ 'fa-angle-double-right': !collapsed, 'fa-angle-double-left': collapsed }" - ></i> + :class="{ + 'fa-angle-double-right': !collapsed, + 'fa-angle-double-left': collapsed + }" + > + </i> </button> </template> diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue index 33096b53cf8..c44c606a8b2 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.vue +++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue @@ -1,132 +1,125 @@ <script> -import { s__ } from '../../locale'; - -const PAGINATION_UI_BUTTON_LIMIT = 4; -const UI_LIMIT = 6; -const SPREAD = '...'; -const PREV = s__('Pagination|Prev'); -const NEXT = s__('Pagination|Next'); -const FIRST = s__('Pagination|« First'); -const LAST = s__('Pagination|Last »'); - -export default { - props: { - /** - This function will take the information given by the pagination component - - Here is an example `change` method: - - change(pagenum) { - gl.utils.visitUrl(`?page=${pagenum}`); + import { s__ } from '../../locale'; + + const PAGINATION_UI_BUTTON_LIMIT = 4; + const UI_LIMIT = 6; + const SPREAD = '...'; + const PREV = s__('Pagination|Prev'); + const NEXT = s__('Pagination|Next'); + const FIRST = s__('Pagination|« First'); + const LAST = s__('Pagination|Last »'); + + export default { + props: { + /** + This function will take the information given by the pagination component + */ + change: { + type: Function, + required: true, }, - */ - change: { - type: Function, - required: true, - }, - /** - pageInfo will come from the headers of the API call - in the `.then` clause of the VueResource API call - there should be a function that contructs the pageInfo for this component - - This is an example: - - const pageInfo = headers => ({ - perPage: +headers['X-Per-Page'], - page: +headers['X-Page'], - total: +headers['X-Total'], - totalPages: +headers['X-Total-Pages'], - nextPage: +headers['X-Next-Page'], - previousPage: +headers['X-Prev-Page'], - }); - */ - pageInfo: { - type: Object, - required: true, - }, - }, - methods: { - changePage(e) { - if (e.target.parentElement.classList.contains('disabled')) return; - - const text = e.target.innerText; - const { totalPages, nextPage, previousPage } = this.pageInfo; - - switch (text) { - case SPREAD: - break; - case LAST: - this.change(totalPages); - break; - case NEXT: - this.change(nextPage); - break; - case PREV: - this.change(previousPage); - break; - case FIRST: - this.change(1); - break; - default: - this.change(+text); - break; - } - }, - }, - computed: { - prev() { - return this.pageInfo.previousPage; - }, - next() { - return this.pageInfo.nextPage; + /** + pageInfo will come from the headers of the API call + in the `.then` clause of the VueResource API call + there should be a function that contructs the pageInfo for this component + + This is an example: + + const pageInfo = headers => ({ + perPage: +headers['X-Per-Page'], + page: +headers['X-Page'], + total: +headers['X-Total'], + totalPages: +headers['X-Total-Pages'], + nextPage: +headers['X-Next-Page'], + previousPage: +headers['X-Prev-Page'], + }); + */ + pageInfo: { + type: Object, + required: true, + }, }, - getItems() { - const total = this.pageInfo.totalPages; - const page = this.pageInfo.page; - const items = []; - - if (page > 1) { - items.push({ title: FIRST, first: true }); - } - - if (page > 1) { - items.push({ title: PREV, prev: true }); - } else { - items.push({ title: PREV, disabled: true, prev: true }); - } - - if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true }); - - const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); - const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); - - for (let i = start; i <= end; i += 1) { - const isActive = i === page; - items.push({ title: i, active: isActive, page: true }); - } - - if (total - page > PAGINATION_UI_BUTTON_LIMIT) { - items.push({ title: SPREAD, separator: true, page: true }); - } - - if (page === total) { - items.push({ title: NEXT, disabled: true, next: true }); - } else if (total - page >= 1) { - items.push({ title: NEXT, next: true }); - } - - if (total - page >= 1) { - items.push({ title: LAST, last: true }); - } - - return items; + computed: { + prev() { + return this.pageInfo.previousPage; + }, + next() { + return this.pageInfo.nextPage; + }, + getItems() { + const total = this.pageInfo.totalPages; + const page = this.pageInfo.page; + const items = []; + + if (page > 1) { + items.push({ title: FIRST, first: true }); + } + + if (page > 1) { + items.push({ title: PREV, prev: true }); + } else { + items.push({ title: PREV, disabled: true, prev: true }); + } + + if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true }); + + const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); + const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); + + for (let i = start; i <= end; i += 1) { + const isActive = i === page; + items.push({ title: i, active: isActive, page: true }); + } + + if (total - page > PAGINATION_UI_BUTTON_LIMIT) { + items.push({ title: SPREAD, separator: true, page: true }); + } + + if (page === total) { + items.push({ title: NEXT, disabled: true, next: true }); + } else if (total - page >= 1) { + items.push({ title: NEXT, next: true }); + } + + if (total - page >= 1) { + items.push({ title: LAST, last: true }); + } + + return items; + }, + showPagination() { + return this.pageInfo.totalPages > 1; + }, }, - showPagination() { - return this.pageInfo.totalPages > 1; + methods: { + changePage(text, isDisabled) { + if (isDisabled) return; + + const { totalPages, nextPage, previousPage } = this.pageInfo; + + switch (text) { + case SPREAD: + break; + case LAST: + this.change(totalPages); + break; + case NEXT: + this.change(nextPage); + break; + case PREV: + this.change(previousPage); + break; + case FIRST: + this.change(1); + break; + default: + this.change(+text); + break; + } + }, }, - }, -}; + }; </script> <template> <div @@ -135,7 +128,8 @@ export default { > <ul class="pagination clearfix"> <li - v-for="item in getItems" + v-for="(item, index) in getItems" + :key="index" :class="{ page: item.page, 'js-previous-button': item.prev, @@ -145,8 +139,11 @@ export default { separator: item.separator, active: item.active, disabled: item.disabled - }"> - <a @click.prevent="changePage($event)">{{item.title}}</a> + }" + > + <a @click.prevent="changePage(item.title, item.disabled)"> + {{ item.title }} + </a> </li> </ul> </div> diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue index 3ff7f6e2c4e..bec4e7c99b6 100644 --- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue +++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue @@ -8,6 +8,12 @@ import '../../lib/utils/datetime_utility'; */ export default { + directives: { + tooltip, + }, + mixins: [ + timeagoMixin, + ], props: { time: { type: String, @@ -26,14 +32,6 @@ export default { default: '', }, }, - - mixins: [ - timeagoMixin, - ], - - directives: { - tooltip, - }, }; </script> <template> @@ -43,6 +41,6 @@ export default { :title="tooltipTitle(time)" :data-placement="tooltipPlacement" data-container="body"> - {{timeFormated(time)}} + {{ timeFormated(time) }} </time> </template> diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue index 4277d9281a0..2b12718ae96 100644 --- a/app/assets/javascripts/vue_shared/components/toggle_button.vue +++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue @@ -9,6 +9,16 @@ const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF'); export default { + components: { + icon, + loadingIcon, + }, + + model: { + prop: 'value', + event: 'change', + }, + props: { name: { type: String, @@ -31,16 +41,6 @@ }, }, - components: { - icon, - loadingIcon, - }, - - model: { - prop: 'value', - event: 'change', - }, - computed: { toggleIcon() { return this.value ? ICON_ON : ICON_OFF; diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue index 1ac61a3c39b..cc9cc46bb4c 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue @@ -22,6 +22,9 @@ import tooltip from '../../directives/tooltip'; export default { name: 'UserAvatarImage', + directives: { + tooltip, + }, props: { lazy: { type: Boolean, @@ -59,9 +62,6 @@ export default { default: 'top', }, }, - directives: { - tooltip, - }, computed: { // API response sends null when gravatar is disabled and // we provide an empty string when we use it inside user avatar link. @@ -87,7 +87,7 @@ export default { v-tooltip class="avatar" :class="{ - lazy, + lazy: lazy, [avatarSizeClass]: true, [cssClasses]: true }" diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue index dc32e783258..6955d164def 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue @@ -26,6 +26,9 @@ export default { components: { userAvatarImage, }, + directives: { + tooltip, + }, props: { linkHref: { type: String, @@ -76,9 +79,6 @@ export default { return this.shouldShowUsername ? '' : this.tooltipText; }, }, - directives: { - tooltip, - }, }; </script> @@ -98,6 +98,6 @@ export default { v-tooltip :title="tooltipText" :tooltip-placement="tooltipPlacement" - >{{username}}</span> + >{{ username }}</span> </a> </template> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue index d2ff2ac006e..ef3b16edf5f 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue @@ -39,7 +39,7 @@ export default { :class="avatarSizeClass" :height="size" :width="size" - v-html="svg"> - </svg> + v-html="svg" + /> </template> diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index bc907a390d8..d1b3754d4ef 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -28,7 +28,9 @@ .dropdown-menu, .dropdown-menu-nav { @include set-visible; - min-height: 40px; + min-height: $dropdown-min-height; + max-height: $dropdown-max-height; + overflow: auto; @media (max-width: $screen-xs-max) { width: 100%; diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 2d7465401f1..621a4adc0cb 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -260,7 +260,7 @@ } .filtered-search-input-dropdown-menu { - max-height: 260px; + max-height: $dropdown-max-height; max-width: 280px; overflow: auto; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index f7853909f56..ef1520f1f63 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -334,7 +334,8 @@ $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-San * Dropdowns */ $dropdown-width: 300px; -$dropdown-max-height: 215px; +$dropdown-min-height: 40px; +$dropdown-max-height: 312px; $dropdown-vertical-offset: 4px; $dropdown-link-color: #555; $dropdown-link-hover-bg: $row-hover; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 60b07537799..1d081b58f62 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -651,15 +651,13 @@ min-width: 0; } - .diff-changed-file-name, - .diff-changed-file-path { + .diff-changed-file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .diff-changed-file-path { - direction: rtl; color: $gl-text-color-tertiary; } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index e1637618ab2..ae9a8b0182c 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -303,7 +303,6 @@ .gutter-toggle { margin-top: 7px; border-left: 1px solid $border-gray-normal; - padding-left: 0; text-align: center; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 05c1033c5f7..a35ebd48887 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -48,7 +48,7 @@ } .dropdown-menu { - max-height: 250px; + max-height: $dropdown-max-height; overflow-y: auto; } @@ -993,3 +993,11 @@ button.mini-pipeline-graph-dropdown-toggle { font-weight: $gl-font-weight-normal; line-height: 1.5; } + +.legend-all { + color: $gl-text-color-secondary; +} + +.legend-success { + color: $green-500; +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 6f4c678c4b8..61a76d0387a 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -322,13 +322,6 @@ } } -.project-repo-buttons { - .project-action-button .dropdown-menu { - max-height: 250px; - overflow-y: auto; - } -} - .split-one { display: inline-table; margin-right: 12px; diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index b12ea760668..45f7d29eb05 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -146,6 +146,7 @@ module ApplicationSettingsHelper :after_sign_up_text, :akismet_api_key, :akismet_enabled, + :authorized_keys_enabled, :auto_devops_enabled, :circuitbreaker_access_retries, :circuitbreaker_check_interval, diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 1ce487e6592..0f5fc2823a3 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -226,4 +226,12 @@ module DiffHelper diffs.overflow? end + + def diff_file_path_text(diff_file, max: 60) + path = diff_file.new_path + + return path unless path.size > max && max > 3 + + "...#{path[-(max - 3)..-1]}" + end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 253e213af81..8ab338d873d 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -261,6 +261,7 @@ class ApplicationSetting < ActiveRecord::Base { after_sign_up_text: nil, akismet_enabled: false, + authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand container_registry_token_expire_delay: 5, default_artifacts_expire_in: '30 days', default_branch_protection: Settings.gitlab['default_branch_protection'], diff --git a/app/models/repository.rb b/app/models/repository.rb index 9c879e2006b..b36e756c07c 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -103,6 +103,10 @@ class Repository "#<#{self.class.name}:#{@disk_path}>" end + def create_hooks + Gitlab::Git::Repository.create_hooks(path_to_repo, Gitlab.config.gitlab_shell.hooks_path) + end + def commit(ref = 'HEAD') return nil unless exists? return ref if ref.is_a?(::Commit) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 3e2dbb07a6c..ba4ca88a8a9 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -775,6 +775,22 @@ = link_to icon('question-circle'), help_page_path('administration/polling') %fieldset + %legend Performance optimization + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :authorized_keys_enabled do + = f.check_box :authorized_keys_enabled + Write to "authorized_keys" file + .help-block + By default, we write to the "authorized_keys" file to support Git + over SSH without additional configuration. GitLab can be optimized + to authenticate SSH keys via the database file. Only uncheck this + if you have configured your OpenSSH server to use the + AuthorizedKeysCommand. Click on the help icon for more details. + = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup') + + %fieldset %legend User and IP Rate Limits .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml index 73c316472e3..dbeddf6689a 100644 --- a/app/views/projects/diffs/_file_header.html.haml +++ b/app/views/projects/diffs/_file_header.html.haml @@ -35,3 +35,6 @@ - if diff_file.mode_changed? %small #{diff_file.a_mode} → #{diff_file.b_mode} + + - if diff_file.stored_externally? && diff_file.external_storage == :lfs + %span.label.label-lfs.append-right-5 LFS diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index dd473ebe580..325159dd9a7 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -25,7 +25,7 @@ = sprite_icon(diff_file_changed_icon(diff_file), size: 16, css_class: "#{diff_file_changed_icon_color(diff_file)} diff-file-changed-icon append-right-8") %span.diff-changed-file-content.append-right-8 %strong.diff-changed-file-name= diff_file.blob.name - %span.diff-changed-file-path.prepend-top-5= diff_file.new_path + %span.diff-changed-file-path.prepend-top-5= diff_file_path_text(diff_file) %span.diff-changed-stats %span.cgreen< +#{diff_file.added_lines} diff --git a/app/views/projects/pipelines/charts/_pipelines.haml b/app/views/projects/pipelines/charts/_pipelines.haml index 7a100843f5e..41dc2f6cf9d 100644 --- a/app/views/projects/pipelines/charts/_pipelines.haml +++ b/app/views/projects/pipelines/charts/_pipelines.haml @@ -4,11 +4,11 @@ %h4= _("Pipelines charts") %p - %span.cgreen + %span.legend-success = icon("circle") = s_("Pipeline|success") - %span.cgray + %span.legend-all = icon("circle") = s_("Pipeline|all") diff --git a/changelogs/unreleased/36906-reordering-issues-to-the-bottom.yml b/changelogs/unreleased/36906-reordering-issues-to-the-bottom.yml new file mode 100644 index 00000000000..0ab765a43b7 --- /dev/null +++ b/changelogs/unreleased/36906-reordering-issues-to-the-bottom.yml @@ -0,0 +1,5 @@ +--- +title: "Issue board: fix for dragging an issue to the very bottom in long lists" +merge_request: 16250 +author: David Kuri +type: fixed
\ No newline at end of file diff --git a/changelogs/unreleased/changes-dropdown-ellipsis.yml b/changelogs/unreleased/changes-dropdown-ellipsis.yml new file mode 100644 index 00000000000..7e3f378cc33 --- /dev/null +++ b/changelogs/unreleased/changes-dropdown-ellipsis.yml @@ -0,0 +1,5 @@ +--- +title: Fixed chanages dropdown ellipsis positioning +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/da-verify-integrity-of-uploaded-files.yml b/changelogs/unreleased/da-verify-integrity-of-uploaded-files.yml new file mode 100644 index 00000000000..5b850c92d17 --- /dev/null +++ b/changelogs/unreleased/da-verify-integrity-of-uploaded-files.yml @@ -0,0 +1,5 @@ +--- +title: Add rake task to check integrity of uploaded files +merge_request: +author: +type: added diff --git a/changelogs/unreleased/fj-41477-fix-bug-wiki-last-version.yml b/changelogs/unreleased/fj-41477-fix-bug-wiki-last-version.yml new file mode 100644 index 00000000000..e4b1343876a --- /dev/null +++ b/changelogs/unreleased/fj-41477-fix-bug-wiki-last-version.yml @@ -0,0 +1,5 @@ +--- +title: Fixing bug when wiki last version +merge_request: 16197 +author: +type: fixed diff --git a/changelogs/unreleased/fj-41681-add-param-disable-commit-stats-api.yml b/changelogs/unreleased/fj-41681-add-param-disable-commit-stats-api.yml new file mode 100644 index 00000000000..dca4dec224c --- /dev/null +++ b/changelogs/unreleased/fj-41681-add-param-disable-commit-stats-api.yml @@ -0,0 +1,5 @@ +--- +title: Added option to disable commits stats in the commit endpoint +merge_request: 16309 +author: +type: added diff --git a/changelogs/unreleased/jej-backport-authorized-keys-to-ce.yml b/changelogs/unreleased/jej-backport-authorized-keys-to-ce.yml new file mode 100644 index 00000000000..4386c631f59 --- /dev/null +++ b/changelogs/unreleased/jej-backport-authorized-keys-to-ce.yml @@ -0,0 +1,5 @@ +--- +title: Backport fast database lookup of SSH authorized_keys from EE +merge_request: 16014 +author: +type: added diff --git a/changelogs/unreleased/sh-fix-bare-import-hooks.yml b/changelogs/unreleased/sh-fix-bare-import-hooks.yml new file mode 100644 index 00000000000..deb6c62f738 --- /dev/null +++ b/changelogs/unreleased/sh-fix-bare-import-hooks.yml @@ -0,0 +1,5 @@ +--- +title: Fix hooks not being set up properly for bare import Rake task +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-store-user-in-api-logs.yml b/changelogs/unreleased/sh-store-user-in-api-logs.yml new file mode 100644 index 00000000000..d904dcaf6d3 --- /dev/null +++ b/changelogs/unreleased/sh-store-user-in-api-logs.yml @@ -0,0 +1,5 @@ +--- +title: Save user ID and username in Grape API log (api_json.log) +merge_request: +author: +type: changed diff --git a/config/initializers/gollum.rb b/config/initializers/gollum.rb index f1066f83dd9..0b86cac51a7 100644 --- a/config/initializers/gollum.rb +++ b/config/initializers/gollum.rb @@ -36,6 +36,26 @@ module Gollum end end end + + module Git + class Git + def tree_entry(commit, path) + pathname = Pathname.new(path) + tmp_entry = nil + + pathname.each_filename do |dir| + tmp_entry = if tmp_entry.nil? + commit.tree[dir] + else + @repo.lookup(tmp_entry[:oid])[dir] + end + + return nil unless tmp_entry + end + tmp_entry + end + end + end end Rails.application.configure do diff --git a/db/migrate/20160301174731_add_fingerprint_index.rb b/db/migrate/20160301174731_add_fingerprint_index.rb new file mode 100644 index 00000000000..f2c3d1ba1ea --- /dev/null +++ b/db/migrate/20160301174731_add_fingerprint_index.rb @@ -0,0 +1,17 @@ +# rubocop:disable all +class AddFingerprintIndex < ActiveRecord::Migration + disable_ddl_transaction! + + DOWNTIME = false + + # https://gitlab.com/gitlab-org/gitlab-ee/issues/764 + def change + args = [:keys, :fingerprint] + + if Gitlab::Database.postgresql? + args << { algorithm: :concurrently } + end + + add_index(*args) unless index_exists?(:keys, :fingerprint) + end +end diff --git a/db/migrate/20170531180233_add_authorized_keys_enabled_to_application_settings.rb b/db/migrate/20170531180233_add_authorized_keys_enabled_to_application_settings.rb new file mode 100644 index 00000000000..1d86a531eb3 --- /dev/null +++ b/db/migrate/20170531180233_add_authorized_keys_enabled_to_application_settings.rb @@ -0,0 +1,19 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddAuthorizedKeysEnabledToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :application_settings, :authorized_keys_enabled, :boolean, default: true, allow_null: false + end + + def down + remove_column :application_settings, :authorized_keys_enabled + end +end diff --git a/db/schema.rb b/db/schema.rb index e6a2ea4c862..a16f756ccfb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -154,6 +154,7 @@ ActiveRecord::Schema.define(version: 20171230123729) do t.integer "gitaly_timeout_default", default: 55, null: false t.integer "gitaly_timeout_medium", default: 30, null: false t.integer "gitaly_timeout_fast", default: 10, null: false + t.boolean "authorized_keys_enabled", default: true, null: false end create_table "audit_events", force: :cascade do |t| diff --git a/doc/administration/operations/fast_ssh_key_lookup.md b/doc/administration/operations/fast_ssh_key_lookup.md new file mode 100644 index 00000000000..835ed8c8006 --- /dev/null +++ b/doc/administration/operations/fast_ssh_key_lookup.md @@ -0,0 +1,170 @@ +# Fast lookup of authorized SSH keys in the database + +Regular SSH operations become slow as the number of users grows because OpenSSH +searches for a key to authorize a user via a linear search. In the worst case, +such as when the user is not authorized to access GitLab, OpenSSH will scan the +entire file to search for a key. This can take significant time and disk I/O, +which will delay users attempting to push or pull to a repository. Making +matters worse, if users add or remove keys frequently, the operating system may +not be able to cache the `authorized_keys` file, which causes the disk to be +accessed repeatedly. + +GitLab Shell solves this by providing a way to authorize SSH users via a fast, +indexed lookup in the GitLab database. This page describes how to enable the fast +lookup of authorized SSH keys. + +> **Warning:** OpenSSH version 6.9+ is required because +`AuthorizedKeysCommand` must be able to accept a fingerprint. These +instructions will break installations using older versions of OpenSSH, such as +those included with CentOS 6 as of September 2017. If you want to use this +feature for CentOS 6, follow [the instructions on how to build and install a custom OpenSSH package](#compiling-a-custom-version-of-openssh-for-centos-6) before continuing. + +## Setting up fast lookup via GitLab Shell + +GitLab Shell provides a way to authorize SSH users via a fast, indexed lookup +to the GitLab database. GitLab Shell uses the fingerprint of the SSH key to +check whether the user is authorized to access GitLab. + +Add the following to your `sshd_config` file. This is usuaully located at +`/etc/ssh/sshd_config`, but it will be `/assets/sshd_config` if you're using +Omnibus Docker: + +``` +AuthorizedKeysCommand /opt/embedded/gitlab-shell/bin/gitlab-shell-authorized-keys-check git %u %k +AuthorizedKeysCommandUser git +``` + +Reload OpenSSH: + +```bash +# Debian or Ubuntu installations +sudo service ssh reload + +# CentOS installations +sudo service sshd reload +``` + +Confirm that SSH is working by removing your user's SSH key in the UI, adding a +new one, and attempting to pull a repo. + +> **Warning:** Do not disable writes until SSH is confirmed to be working +perfectly, because the file will quickly become out-of-date. + +In the case of lookup failures (which are not uncommon), the `authorized_keys` +file will still be scanned. So git SSH performance will still be slow for many +users as long as a large file exists. + +You can disable any more writes to the `authorized_keys` file by unchecking +`Write to "authorized_keys" file` in the Application Settings of your GitLab +installation. + +![Write to authorized keys setting](img/write_to_authorized_keys_setting.png) + +Again, confirm that SSH is working by removing your user's SSH key in the UI, +adding a new one, and attempting to pull a repo. + +Then you can backup and delete your `authorized_keys` file for best performance. + +## How to go back to using the `authorized_keys` file + +This is a brief overview. Please refer to the above instructions for more context. + +1. [Rebuild the `authorized_keys` file](../raketasks/maintenance.md#rebuild-authorized_keys-file) +1. Enable writes to the `authorized_keys` file in Application Settings +1. Remove the `AuthorizedKeysCommand` lines from `/etc/ssh/sshd_config` or from `/assets/sshd_config` if you are using Omnibus Docker. +1. Reload sshd: `sudo service sshd reload` +1. Remove the `/opt/gitlab-shell/authorized_keys` file + +## Compiling a custom version of OpenSSH for CentOS 6 + +Building a custom version of OpenSSH is not necessary for Ubuntu 16.04 users, +since Ubuntu 16.04 ships with OpenSSH 7.2. + +It is also unnecessary for CentOS 7.4 users, as that version ships with +OpenSSH 7.4. If you are using CentOS 7.0 - 7.3, we strongly recommend that you +upgrade to CentOS 7.4 instead of following this procedure. This should be as +simple as running `yum update`. + +CentOS 6 users must build their own OpenSSH package to enable SSH lookups via +the database. The following instructions can be used to build OpenSSH 7.5: + +1. First, download the package and install the required packages: + + ``` + sudo su - + cd /tmp + curl --remote-name https://mirrors.evowise.com/pub/OpenBSD/OpenSSH/portable/openssh-7.5p1.tar.gz + tar xzvf openssh-7.5p1.tar.gz + yum install rpm-build gcc make wget openssl-devel krb5-devel pam-devel libX11-devel xmkmf libXt-devel + ``` + +3. Prepare the build by copying files to the right place: + + ``` + mkdir -p /root/rpmbuild/{SOURCES,SPECS} + cp ./openssh-7.5p1/contrib/redhat/openssh.spec /root/rpmbuild/SPECS/ + cp openssh-7.5p1.tar.gz /root/rpmbuild/SOURCES/ + cd /root/rpmbuild/SPECS + ``` + +3. Next, set the spec settings properly: + + ``` + sed -i -e "s/%define no_gnome_askpass 0/%define no_gnome_askpass 1/g" openssh.spec + sed -i -e "s/%define no_x11_askpass 0/%define no_x11_askpass 1/g" openssh.spec + sed -i -e "s/BuildPreReq/BuildRequires/g" openssh.spec + ``` + +3. Build the RPMs: + + ``` + rpmbuild -bb openssh.spec + ``` + +4. Ensure the RPMs were built: + + ``` + ls -al /root/rpmbuild/RPMS/x86_64/ + ``` + + You should see something as the following: + + ``` + total 1324 + drwxr-xr-x. 2 root root 4096 Jun 20 19:37 . + drwxr-xr-x. 3 root root 19 Jun 20 19:37 .. + -rw-r--r--. 1 root root 470828 Jun 20 19:37 openssh-7.5p1-1.x86_64.rpm + -rw-r--r--. 1 root root 490716 Jun 20 19:37 openssh-clients-7.5p1-1.x86_64.rpm + -rw-r--r--. 1 root root 17020 Jun 20 19:37 openssh-debuginfo-7.5p1-1.x86_64.rpm + -rw-r--r--. 1 root root 367516 Jun 20 19:37 openssh-server-7.5p1-1.x86_64.rpm + ``` + +5. Install the packages. OpenSSH packages will replace `/etc/pam.d/sshd` + with its own version, which may prevent users from logging in, so be sure + that the file is backed up and restored after installation: + + ``` + timestamp=$(date +%s) + cp /etc/pam.d/sshd pam-ssh-conf-$timestamp + rpm -Uvh /root/rpmbuild/RPMS/x86_64/*.rpm + yes | cp pam-ssh-conf-$timestamp /etc/pam.d/sshd + ``` + +6. Verify the installed version. In another window, attempt to login to the server: + + ``` + ssh -v <your-centos-machine> + ``` + + You should see a line that reads: "debug1: Remote protocol version 2.0, remote software version OpenSSH_7.5" + + If not, you may need to restart sshd (e.g. `systemctl restart sshd.service`). + +7. *IMPORTANT!* Open a new SSH session to your server before exiting to make + sure everything is working! If you need to downgrade, simple install the + older package: + + ``` + # Only run this if you run into a problem logging in + yum downgrade openssh-server openssh openssh-clients + ``` diff --git a/doc/administration/operations/img/write_to_authorized_keys_setting.png b/doc/administration/operations/img/write_to_authorized_keys_setting.png Binary files differnew file mode 100644 index 00000000000..232765f1917 --- /dev/null +++ b/doc/administration/operations/img/write_to_authorized_keys_setting.png diff --git a/doc/administration/operations/index.md b/doc/administration/operations/index.md index 320d71a9527..5655b7efec6 100644 --- a/doc/administration/operations/index.md +++ b/doc/administration/operations/index.md @@ -13,4 +13,5 @@ by GitLab to another file system or another server. that to prioritize important jobs. - [Sidekiq MemoryKiller](sidekiq_memory_killer.md): Configure Sidekiq MemoryKiller to restart Sidekiq. -- [Unicorn](unicorn.md): Understand Unicorn and unicorn-worker-killer.
\ No newline at end of file +- [Unicorn](unicorn.md): Understand Unicorn and unicorn-worker-killer. +- [Speed up SSH operations](fast_ssh_key_lookup.md): Authorize SSH users via a fast, indexed lookup to the GitLab database. diff --git a/doc/administration/operations/speed_up_ssh.md b/doc/administration/operations/speed_up_ssh.md new file mode 100644 index 00000000000..89265b3018b --- /dev/null +++ b/doc/administration/operations/speed_up_ssh.md @@ -0,0 +1 @@ +This document was moved to [another location](fast_ssh_key_lookup.md). diff --git a/doc/administration/raketasks/check.md b/doc/administration/raketasks/check.md index c39cb49b1c6..d1ed152b58c 100644 --- a/doc/administration/raketasks/check.md +++ b/doc/administration/raketasks/check.md @@ -76,6 +76,39 @@ Example output: ![gitlab:user:check_repos output](../img/raketasks/check_repos_output.png) +## Uploaded Files Integrity + +The uploads check Rake task will loop through all uploads in the database +and run two checks to determine the integrity of each file: + +1. Check if the file exist on the file system. +1. Check if the checksum of the file on the file system matches the checksum in the database. + +**Omnibus Installation** + +``` +sudo gitlab-rake gitlab:uploads:check +``` + +**Source Installation** + +```bash +sudo -u git -H bundle exec rake gitlab:uploads:check RAILS_ENV=production +``` + +This task also accepts some environment variables which you can use to override +certain values: + +Variable | Type | Description +-------- | ---- | ----------- +`BATCH` | integer | Specifies the size of the batch. Defaults to 200. +`ID_FROM` | integer | Specifies the ID to start from, inclusive of the value. +`ID_TO` | integer | Specifies the ID value to end at, inclusive of the value. + +```bash +sudo gitlab-rake gitlab:uploads:check BATCH=100 ID_FROM=50 ID_TO=250 +``` + ## LDAP Check The LDAP check Rake task will test the bind_dn and password credentials diff --git a/doc/api/commits.md b/doc/api/commits.md index c9b72d4a1dd..63554c63057 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -159,6 +159,7 @@ Parameters: | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | `sha` | string | yes | The commit hash or name of a repository branch or tag | +| `stats` | boolean | no | Include commit stats. Default is true | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits/master diff --git a/doc/api/repositories.md b/doc/api/repositories.md index 03b32577872..5fb25e40ed7 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -113,7 +113,7 @@ GET /projects/:id/repository/archive Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user -- `sha` (optional) - The commit SHA to download defaults to the tip of the default branch +- `sha` (optional) - The commit SHA to download. A tag, branch reference or sha can be used. This defaults to the tip of the default branch if not specified ## Compare branches, tags or commits diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md index 6a5821762cc..f919ed3c797 100644 --- a/doc/ci/examples/code_climate.md +++ b/doc/ci/examples/code_climate.md @@ -16,7 +16,8 @@ codequality: - docker:dind script: - docker pull codeclimate/codeclimate - - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json || true + - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 init + - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 analyze -f json > codeclimate.json || true artifacts: paths: [codeclimate.json] ``` diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index df0e1521150..b8df0bfba20 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -181,7 +181,7 @@ before_script: ## Assuming you created the SSH_KNOWN_HOSTS variable, uncomment the ## following two lines. ## - - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts' + - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts - chmod 644 ~/.ssh/known_hosts ## diff --git a/doc/development/architecture.md b/doc/development/architecture.md index 54029e00507..d1ba7d3dfc3 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -133,8 +133,6 @@ Usage: /etc/init.d/postgresql {start|stop|restart|reload|force-reload|status} [v ### Log locations of the services -Note: `/home/git/` is shorthand for `/home/git`. - gitlabhq (includes Unicorn and Sidekiq logs) - `/home/git/gitlab/log/` contains `application.log`, `production.log`, `sidekiq.log`, `unicorn.stdout.log`, `githost.log` and `unicorn.stderr.log` normally. diff --git a/doc/development/changelog.md b/doc/development/changelog.md index 48cffc0dd18..18f4177a5e5 100644 --- a/doc/development/changelog.md +++ b/doc/development/changelog.md @@ -127,7 +127,7 @@ type: If you're working on the GitLab EE repository, the entry will be added to `changelogs/unreleased-ee/` instead. -#### Arguments +### Arguments | Argument | Shorthand | Purpose | | ----------------- | --------- | ---------------------------------------------------------------------------------------------------------- | diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md index 1cd66f27492..02773162801 100644 --- a/doc/development/fe_guide/style_guide_js.md +++ b/doc/development/fe_guide/style_guide_js.md @@ -101,16 +101,16 @@ followed by any global declarations, then a blank newline prior to any imports o ``` Import statements are following usual naming guidelines, for example object literals use camel case: - + ```javascript // some_object file export default { key: 'value', }; - + // bad import ObjectLiteral from 'some_object'; - + // good import objectLiteral from 'some_object'; ``` @@ -255,6 +255,10 @@ A forEach will cause side effects, it will be mutating the array being iterated. ### Vue.js +#### `eslint-vue-plugin` +We default to [eslint-vue-plugin][eslint-plugin-vue], with the `plugin:vue/recommended`. +Please check this [rules][eslint-plugin-vue-rules] for more documentation. + #### Basic Rules 1. The service has it's own file 1. The store has it's own file @@ -360,6 +364,10 @@ A forEach will cause side effects, it will be mutating the array being iterated. <component bar="bar" /> + + // bad + <component + bar="bar" /> ``` #### Quotes @@ -509,25 +517,7 @@ On those a default key should not be provided. ``` 1. Properties in a Vue Component: - 1. `name` - 1. `props` - 1. `mixins` - 1. `directives` - 1. `data` - 1. `components` - 1. `computedProps` - 1. `methods` - 1. `beforeCreate` - 1. `created` - 1. `beforeMount` - 1. `mounted` - 1. `beforeUpdate` - 1. `updated` - 1. `activated` - 1. `deactivated` - 1. `beforeDestroy` - 1. `destroyed` - + Check [order of properties in components rule][vue-order]. #### Vue and Bootstrap @@ -582,3 +572,6 @@ The goal of this accord is to make sure we are all on the same page. [eslintrc]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc [eslint-this]: http://eslint.org/docs/rules/class-methods-use-this [eslint-new]: http://eslint.org/docs/rules/no-new +[eslint-plugin-vue]: https://github.com/vuejs/eslint-plugin-vue +[eslint-plugin-vue-rules]: https://github.com/vuejs/eslint-plugin-vue#bulb-rules +[vue-order]: https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/order-in-components.md diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 4fa83388d0c..708d07fcec9 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -200,7 +200,7 @@ instance and project. In addition, all admins can use the admin interface under |---------------------------------------|-----------------|-------------|----------|--------| | See commits and jobs | ✓ | ✓ | ✓ | ✓ | | Retry or cancel job | | ✓ | ✓ | ✓ | -| Erase job artifacts and trace | | ✓ [^7] | ✓ | ✓ | +| Erase job artifacts and trace | | ✓ [^5] | ✓ | ✓ | | Remove project | | | ✓ | ✓ | | Create project | | | ✓ | ✓ | | Change project configuration | | | ✓ | ✓ | @@ -223,13 +223,13 @@ users: | Run CI job | | ✓ | ✓ | ✓ | | Clone source and LFS from current project | | ✓ | ✓ | ✓ | | Clone source and LFS from public projects | | ✓ | ✓ | ✓ | -| Clone source and LFS from internal projects | | ✓ [^5] | ✓ [^5] | ✓ | -| Clone source and LFS from private projects | | ✓ [^6] | ✓ [^6] | ✓ [^6] | +| Clone source and LFS from internal projects | | ✓ [^6] | ✓ [^6] | ✓ | +| Clone source and LFS from private projects | | ✓ [^7] | ✓ [^7] | ✓ [^7] | | Push source and LFS | | | | | | Pull container images from current project | | ✓ | ✓ | ✓ | | Pull container images from public projects | | ✓ | ✓ | ✓ | -| Pull container images from internal projects| | ✓ [^5] | ✓ [^5] | ✓ | -| Pull container images from private projects | | ✓ [^6] | ✓ [^6] | ✓ [^6] | +| Pull container images from internal projects| | ✓ [^6] | ✓ [^6] | ✓ | +| Pull container images from private projects | | ✓ [^7] | ✓ [^7] | ✓ [^7] | | Push container images to current project | | ✓ | ✓ | ✓ | | Push container images to other projects | | | | | @@ -259,12 +259,13 @@ with the permissions described on the documentation on [auditor users permission Auditor users are available in [GitLab Enterprise Edition Premium](https://about.gitlab.com/gitlab-ee/) only. -[^1]: On public and internal projects, all users are able to perform this action. +[^1]: On public and internal projects, all users are able to perform this action [^2]: Guest users can only view the confidential issues they created themselves [^3]: If **Public pipelines** is enabled in **Project Settings > CI/CD** [^4]: Not allowed for Guest, Reporter, Developer, Master, or Owner -[^5]: Only if user is not external one. -[^6]: Only if user is a member of the project. -[^7]: Only if the build was triggered by the user +[^5]: Only if the job was triggered by the user +[^6]: Only if user is not external one +[^7]: Only if user is a member of the project + [ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994 [new-mod]: project/new_ci_build_permissions_model.md diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index d5619c7b563..5f14d232cb1 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -2,9 +2,6 @@ > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/35954) in 10.1. -CAUTION: **Warning:** -The Cluster integration is currently in **Beta**. - With a cluster associated to your project, you can use Review Apps, deploy your applications, run your pipelines, and much more, in an easy way. diff --git a/doc/user/project/integrations/irker.md b/doc/user/project/integrations/irker.md index c63ea1316fe..ecdd83ce8f0 100644 --- a/doc/user/project/integrations/irker.md +++ b/doc/user/project/integrations/irker.md @@ -47,4 +47,8 @@ Irker accepts channel names of the form `chan` and `#chan`, both for the case, `Aorimn` is treated as a nick and no more as a channel name. Irker can also join password-protected channels. Users need to append -`?key=thesecretpassword` to the chan name. +`?key=thesecretpassword` to the chan name. When using this feature remember to +**not** put the `#` sign in front of the channel name; failing to do so will +result on irker joining a channel literally named `#chan?key=password` henceforth +leaking the channel key through the `/whois` IRC command (depending on IRC server +configuration). This is due to a long standing irker bug. diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index eafdd28071d..82175c70e49 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -54,6 +54,12 @@ Below are described the supported events. Triggered when you push to the repository except when pushing tags. +> **Note:** When more than 20 commits are pushed at once, the `commits` web hook + attribute will only contain the first 20 for performance reasons. Loading + detailed commit data is expensive. Note that despite only 20 commits being + present in the `commits` attribute, the `total_commits_count` attribute will + contain the actual total. + **Request header**: ``` diff --git a/lib/api/api.rb b/lib/api/api.rb index e0d14281c96..ae161efb358 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -13,7 +13,8 @@ module API formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new, include: [ GrapeLogging::Loggers::FilterParameters.new, - GrapeLogging::Loggers::ClientEnv.new + GrapeLogging::Loggers::ClientEnv.new, + Gitlab::GrapeLogging::Loggers::UserLogger.new ] allow_access_with_scope :api diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 38e05074353..d8fd6a6eb06 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -82,13 +82,14 @@ module API end params do requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' + optional :stats, type: Boolean, default: true, desc: 'Include commit stats' end get ':id/repository/commits/:sha', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do commit = user_project.commit(params[:sha]) not_found! 'Commit' unless commit - present commit, with: Entities::CommitDetail + present commit, with: Entities::CommitDetail, stats: params[:stats] end desc 'Get the diff for a specific commit of a project' do diff --git a/lib/api/entities.rb b/lib/api/entities.rb index bd0c54a1b04..f574858be02 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -278,7 +278,7 @@ module API end class CommitDetail < Commit - expose :stats, using: Entities::CommitStats + expose :stats, using: Entities::CommitStats, if: :stats expose :status expose :last_pipeline, using: 'API::Entities::PipelineBasic' end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index bf388163ec8..d6ce368efd5 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -5,6 +5,7 @@ module API SUDO_HEADER = "HTTP_SUDO".freeze SUDO_PARAM = :sudo + API_USER_ENV = 'gitlab.api.user'.freeze def declared_params(options = {}) options = { include_parent_namespaces: false }.merge(options) @@ -48,10 +49,16 @@ module API validate_access_token!(scopes: scopes_registered_for_endpoint) unless sudo? + save_current_user_in_env(@current_user) if @current_user + @current_user end # rubocop:enable Gitlab/ModuleWithInstanceVariables + def save_current_user_in_env(user) + env[API_USER_ENV] = { user_id: user.id, username: user.username } + end + def sudo? initial_current_user != current_user end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 79b302aae70..8bf53939751 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -82,6 +82,18 @@ module API end # + # Get a ssh key using the fingerprint + # + get "/authorized_keys" do + fingerprint = params.fetch(:fingerprint) do + Gitlab::InsecureKeyFingerprint.new(params.fetch(:key)).fingerprint + end + key = Key.find_by(fingerprint: fingerprint) + not_found!("Key") if key.nil? + present key, with: Entities::SSHKey + end + + # # Discover user by ssh key or user id # get "/discover" do diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb index 0ef26aa696a..4f6ea8f502e 100644 --- a/lib/api/v3/commits.rb +++ b/lib/api/v3/commits.rb @@ -71,13 +71,14 @@ module API end params do requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' + optional :stats, type: Boolean, default: true, desc: 'Include commit stats' end get ":id/repository/commits/:sha", requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do commit = user_project.commit(params[:sha]) not_found! "Commit" unless commit - present commit, with: ::API::Entities::CommitDetail + present commit, with: ::API::Entities::CommitDetail, stats: params[:stats] end desc 'Get the diff for a specific commit of a project' do diff --git a/lib/gitlab/bare_repository_import/importer.rb b/lib/gitlab/bare_repository_import/importer.rb index 709a901aa77..884a3de8f62 100644 --- a/lib/gitlab/bare_repository_import/importer.rb +++ b/lib/gitlab/bare_repository_import/importer.rb @@ -63,6 +63,7 @@ module Gitlab log " * Created #{project.name} (#{project_full_path})".color(:green) project.write_repository_config + project.repository.create_hooks ProjectCacheWorker.perform_async(project.id) else diff --git a/lib/gitlab/git/gitlab_projects.rb b/lib/gitlab/git/gitlab_projects.rb index cba638c06db..976fa1ddfe6 100644 --- a/lib/gitlab/git/gitlab_projects.rb +++ b/lib/gitlab/git/gitlab_projects.rb @@ -41,36 +41,6 @@ module Gitlab io.read end - def rm_project - logger.info "Removing repository <#{repository_absolute_path}>." - FileUtils.rm_rf(repository_absolute_path) - end - - # Move repository from one directory to another - # - # Example: gitlab/gitlab-ci.git -> randx/six.git - # - # Won't work if target namespace directory does not exist - # - def mv_project(new_path) - new_absolute_path = File.join(shard_path, new_path) - - # verify that the source repo exists - unless File.exist?(repository_absolute_path) - logger.error "mv-project failed: source path <#{repository_absolute_path}> does not exist." - return false - end - - # ...and that the target repo does not exist - if File.exist?(new_absolute_path) - logger.error "mv-project failed: destination path <#{new_absolute_path}> already exists." - return false - end - - logger.info "Moving repository from <#{repository_absolute_path}> to <#{new_absolute_path}>." - FileUtils.mv(repository_absolute_path, new_absolute_path) - end - # Import project via git clone --bare # URL must be publicly cloneable def import_project(source, timeout) diff --git a/lib/gitlab/gitaly_client/remote_service.rb b/lib/gitlab/gitaly_client/remote_service.rb index 559a901b9a3..e58f641d69a 100644 --- a/lib/gitlab/gitaly_client/remote_service.rb +++ b/lib/gitlab/gitaly_client/remote_service.rb @@ -7,10 +7,12 @@ module Gitlab @storage = repository.storage end - def add_remote(name, url, mirror_refmap) + def add_remote(name, url, mirror_refmaps) request = Gitaly::AddRemoteRequest.new( - repository: @gitaly_repo, name: name, url: url, - mirror_refmap: mirror_refmap.to_s + repository: @gitaly_repo, + name: name, + url: url, + mirror_refmaps: Array.wrap(mirror_refmaps).map(&:to_s) ) GitalyClient.call(@storage, :remote_service, :add_remote, request) diff --git a/lib/gitlab/grape_logging/loggers/user_logger.rb b/lib/gitlab/grape_logging/loggers/user_logger.rb new file mode 100644 index 00000000000..fa172861967 --- /dev/null +++ b/lib/gitlab/grape_logging/loggers/user_logger.rb @@ -0,0 +1,18 @@ +# This grape_logging module (https://github.com/aserafin/grape_logging) makes it +# possible to log the user who performed the Grape API action by retrieving +# the user context from the request environment. +module Gitlab + module GrapeLogging + module Loggers + class UserLogger < ::GrapeLogging::Loggers::Base + def parameters(request, _) + params = request.env[::API::Helpers::API_USER_ENV] + + return {} unless params + + params.slice(:user_id, :username) + end + end + end + end +end diff --git a/lib/gitlab/insecure_key_fingerprint.rb b/lib/gitlab/insecure_key_fingerprint.rb new file mode 100644 index 00000000000..f85b6e9197f --- /dev/null +++ b/lib/gitlab/insecure_key_fingerprint.rb @@ -0,0 +1,23 @@ +module Gitlab + # + # Calculates the fingerprint of a given key without using + # openssh key validations. For this reason, only use + # for calculating the fingerprint to find the key with it. + # + # DO NOT use it for checking the validity of a ssh key. + # + class InsecureKeyFingerprint + attr_accessor :key + + # + # Gets the base64 encoded string representing a rsa or dsa key + # + def initialize(key_base64) + @key = key_base64 + end + + def fingerprint + OpenSSL::Digest::MD5.hexdigest(Base64.decode64(@key)).scan(/../).join(':') + end + end +end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 2c7b8af83f2..0002c7da8f1 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -37,7 +37,7 @@ module Gitlab end def environment_name_regex_chars - 'a-zA-Z0-9_/\\$\\{\\}\\. -' + 'a-zA-Z0-9_/\\$\\{\\}\\. \\-' end def environment_name_regex diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index a8a4ec996c4..f4a41dc3eda 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -136,7 +136,10 @@ module Gitlab end end - # Move repository + # Move repository reroutes to mv_directory which is an alias for + # mv_namespace. Given the underlying implementation is a move action, + # indescriminate of what the folders might be. + # # storage - project's storage path # path - project disk path # new_path - new project disk path @@ -146,7 +149,9 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/873 def mv_repository(storage, path, new_path) - gitlab_projects(storage, "#{path}.git").mv_project("#{new_path}.git") + return false if path.empty? || new_path.empty? + + !!mv_directory(storage, "#{path}.git", "#{new_path}.git") end # Fork repository to new path @@ -164,7 +169,9 @@ module Gitlab .fork_repository(forked_to_storage, "#{forked_to_disk_path}.git") end - # Remove repository from file system + # Removes a repository from file system, using rm_diretory which is an alias + # for rm_namespace. Given the underlying implementation removes the name + # passed as second argument on the passed storage. # # storage - project's storage path # name - project disk path @@ -174,7 +181,12 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/873 def remove_repository(storage, name) - gitlab_projects(storage, "#{name}.git").rm_project + return false if name.empty? + + !!rm_directory(storage, "#{name}.git") + rescue ArgumentError => e + Rails.logger.warn("Repository does not exist: #{e} at: #{name}.git") + false end # Add new key to gitlab-shell @@ -183,6 +195,8 @@ module Gitlab # add_key("key-42", "sha-rsa ...") # def add_key(key_id, key_content) + return unless self.authorized_keys_enabled? + gitlab_shell_fast_execute([gitlab_shell_keys_path, 'add-key', key_id, self.class.strip_key(key_content)]) end @@ -192,6 +206,8 @@ module Gitlab # Ex. # batch_add_keys { |adder| adder.add_key("key-42", "sha-rsa ...") } def batch_add_keys(&block) + return unless self.authorized_keys_enabled? + IO.popen(%W(#{gitlab_shell_path}/bin/gitlab-keys batch-add-keys), 'w') do |io| yield(KeyAdder.new(io)) end @@ -202,10 +218,11 @@ module Gitlab # Ex. # remove_key("key-342", "sha-rsa ...") # - def remove_key(key_id, key_content) + def remove_key(key_id, key_content = nil) + return unless self.authorized_keys_enabled? + args = [gitlab_shell_keys_path, 'rm-key', key_id] args << key_content if key_content - gitlab_shell_fast_execute(args) end @@ -215,9 +232,62 @@ module Gitlab # remove_all_keys # def remove_all_keys + return unless self.authorized_keys_enabled? + gitlab_shell_fast_execute([gitlab_shell_keys_path, 'clear']) end + # Remove ssh keys from gitlab shell that are not in the DB + # + # Ex. + # remove_keys_not_found_in_db + # + def remove_keys_not_found_in_db + return unless self.authorized_keys_enabled? + + Rails.logger.info("Removing keys not found in DB") + + batch_read_key_ids do |ids_in_file| + ids_in_file.uniq! + keys_in_db = Key.where(id: ids_in_file) + + next unless ids_in_file.size > keys_in_db.count # optimization + + ids_to_remove = ids_in_file - keys_in_db.pluck(:id) + ids_to_remove.each do |id| + Rails.logger.info("Removing key-#{id} not found in DB") + remove_key("key-#{id}") + end + end + end + + # Iterate over all ssh key IDs from gitlab shell, in batches + # + # Ex. + # batch_read_key_ids { |batch| keys = Key.where(id: batch) } + # + def batch_read_key_ids(batch_size: 100, &block) + return unless self.authorized_keys_enabled? + + list_key_ids do |key_id_stream| + key_id_stream.lazy.each_slice(batch_size) do |lines| + key_ids = lines.map { |l| l.chomp.to_i } + yield(key_ids) + end + end + end + + # Stream all ssh key IDs from gitlab shell, separated by newlines + # + # Ex. + # list_key_ids + # + def list_key_ids(&block) + return unless self.authorized_keys_enabled? + + IO.popen(%W(#{gitlab_shell_path}/bin/gitlab-keys list-key-ids), &block) + end + # Add empty directory for storing repositories # # Ex. @@ -255,6 +325,7 @@ module Gitlab rescue GRPC::InvalidArgument => e raise ArgumentError, e.message end + alias_method :rm_directory, :rm_namespace # Move namespace directory inside repositories storage # @@ -274,6 +345,7 @@ module Gitlab rescue GRPC::InvalidArgument false end + alias_method :mv_directory, :mv_namespace def url_to_repo(path) Gitlab.config.gitlab_shell.ssh_path_prefix + "#{path}.git" @@ -333,6 +405,14 @@ module Gitlab File.join(gitlab_shell_path, 'bin', 'gitlab-keys') end + def authorized_keys_enabled? + # Return true if nil to ensure the authorized_keys methods work while + # fixing the authorized_keys file during migration. + return true if Gitlab::CurrentSettings.current_application_settings.authorized_keys_enabled.nil? + + Gitlab::CurrentSettings.current_application_settings.authorized_keys_enabled + end + private def gitlab_projects(shard_path, disk_path) diff --git a/lib/tasks/gitlab/uploads.rake b/lib/tasks/gitlab/uploads.rake new file mode 100644 index 00000000000..df31567ce64 --- /dev/null +++ b/lib/tasks/gitlab/uploads.rake @@ -0,0 +1,44 @@ +namespace :gitlab do + namespace :uploads do + desc 'GitLab | Uploads | Check integrity of uploaded files' + task check: :environment do + puts 'Checking integrity of uploaded files' + + uploads_batches do |batch| + batch.each do |upload| + puts "- Checking file (#{upload.id}): #{upload.absolute_path}".color(:green) + + if upload.exist? + check_checksum(upload) + else + puts " * File does not exist on the file system".color(:red) + end + end + end + + puts 'Done!' + end + + def batch_size + ENV.fetch('BATCH', 200).to_i + end + + def calculate_checksum(absolute_path) + Digest::SHA256.file(absolute_path).hexdigest + end + + def check_checksum(upload) + checksum = calculate_checksum(upload.absolute_path) + + if checksum != upload.checksum + puts " * File checksum (#{checksum}) does not match the one in the database (#{upload.checksum})".color(:red) + end + end + + def uploads_batches(&block) + Upload.all.in_batches(of: batch_size, start: ENV['ID_FROM'], finish: ENV['ID_TO']) do |relation| # rubocop: disable Cop/InBatches + yield relation + end + end + end +end diff --git a/package.json b/package.json index 1f83f62e7f7..4759ae76817 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "dependencies": { "autosize": "^4.0.0", "axios": "^0.17.1", - "axios-mock-adapter": "^1.10.0", "babel-core": "^6.26.0", "babel-eslint": "^8.0.2", "babel-loader": "^7.1.2", @@ -45,7 +44,6 @@ "document-register-element": "1.3.0", "dropzone": "^4.2.0", "emoji-unicode-version": "^0.2.1", - "eslint-plugin-html": "^2.0.1", "exports-loader": "^0.6.4", "file-loader": "^0.11.1", "fuzzaldrin-plus": "^0.5.0", @@ -89,14 +87,17 @@ }, "devDependencies": { "@gitlab-org/gitlab-svgs": "^1.5.0", + "axios-mock-adapter": "^1.10.0", "babel-plugin-istanbul": "^4.1.5", - "eslint": "^3.10.1", + "eslint": "^3.18.0", "eslint-config-airbnb-base": "^10.0.1", "eslint-import-resolver-webpack": "^0.8.3", "eslint-plugin-filenames": "^1.1.0", + "eslint-plugin-html": "2.0.1", "eslint-plugin-import": "^2.2.0", "eslint-plugin-jasmine": "^2.1.0", "eslint-plugin-promise": "^3.5.0", + "eslint-plugin-vue": "^4.0.1", "istanbul": "^0.4.5", "jasmine-core": "^2.6.3", "jasmine-jquery": "^2.1.1", diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index f9c31ac61d8..15cbe36ae76 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -266,4 +266,14 @@ describe DiffHelper do end end end + + context '#diff_file_path_text' do + it 'returns full path by default' do + expect(diff_file_path_text(diff_file)).to eq(diff_file.new_path) + end + + it 'returns truncated path' do + expect(diff_file_path_text(diff_file, max: 10)).to eq("...open.rb") + end + end end diff --git a/spec/initializers/gollum_spec.rb b/spec/initializers/gollum_spec.rb new file mode 100644 index 00000000000..adf824a8947 --- /dev/null +++ b/spec/initializers/gollum_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe 'gollum' do + let(:project) { create(:project) } + let(:user) { project.owner } + let(:wiki) { ProjectWiki.new(project, user) } + let(:gollum_wiki) { Gollum::Wiki.new(wiki.repository.path) } + + before do + create_page(page_name, 'content1') + end + + after do + destroy_page(page_name) + end + + context 'with simple paths' do + let(:page_name) { 'page1' } + + it 'returns the entry hash if it matches the file name' do + expect(tree_entry(page_name)).not_to be_nil + end + + it 'returns nil if the path does not fit completely' do + expect(tree_entry("foo/#{page_name}")).to be_nil + end + end + + context 'with complex paths' do + let(:page_name) { '/foo/bar/page2' } + + it 'returns the entry hash if it matches the file name' do + expect(tree_entry(page_name)).not_to be_nil + end + + it 'returns nil if the path does not fit completely' do + expect(tree_entry("foo1/bar/page2")).to be_nil + expect(tree_entry("foo/bar1/page2")).to be_nil + end + end + + def tree_entry(name) + gollum_wiki.repo.git.tree_entry(wiki_commits[0].commit, name + '.md') + end + + def wiki_commits + gollum_wiki.repo.commits + end + + def commit_details + Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit") + end + + def create_page(name, content) + wiki.wiki.write_page(name, :markdown, content, commit_details) + end + + def destroy_page(name) + page = wiki.find_page(name).page + wiki.delete_page(page, "test commit") + end +end diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index 7c5888b6d82..b7cc3a8813e 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -154,6 +154,18 @@ describe('Board list component', () => { }); }); + it('sets data attribute with invalid id', (done) => { + component.showCount = true; + + Vue.nextTick(() => { + expect( + component.$el.querySelector('.board-list-count').getAttribute('data-issue-id'), + ).toBe('-1'); + + done(); + }); + }); + it('shows how many more issues to load', (done) => { component.showCount = true; component.list.issuesSize = 20; diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index 8ef221257be..278155c585e 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -45,6 +45,9 @@ describe('Issue card component', () => { component = new Vue({ el: document.querySelector('.test-container'), + components: { + 'issue-card': gl.issueBoards.IssueCardInner, + }, data() { return { list, @@ -53,9 +56,6 @@ describe('Issue card component', () => { rootPath: '/', }; }, - components: { - 'issue-card': gl.issueBoards.IssueCardInner, - }, template: ` <issue-card :issue="issue" diff --git a/spec/javascripts/cycle_analytics/banner_spec.js b/spec/javascripts/cycle_analytics/banner_spec.js index fb6b7fee168..64a76a6ee5f 100644 --- a/spec/javascripts/cycle_analytics/banner_spec.js +++ b/spec/javascripts/cycle_analytics/banner_spec.js @@ -20,8 +20,9 @@ describe('Cycle analytics banner', () => { expect( vm.$el.querySelector('h4').textContent.trim(), ).toEqual('Introducing Cycle Analytics'); + expect( - vm.$el.querySelector('p').textContent.trim(), + vm.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), ).toContain('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.'); expect( vm.$el.querySelector('a').textContent.trim(), diff --git a/spec/javascripts/cycle_analytics/total_time_component_spec.js b/spec/javascripts/cycle_analytics/total_time_component_spec.js index 31b65fd1cde..ad0fc38a856 100644 --- a/spec/javascripts/cycle_analytics/total_time_component_spec.js +++ b/spec/javascripts/cycle_analytics/total_time_component_spec.js @@ -23,7 +23,7 @@ describe('Total time component', () => { }, }); - expect(vm.$el.textContent.trim()).toEqual('3 days 4 hrs'); + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('3 days 4 hrs'); }); it('should render information for hours and minutes', () => { @@ -34,7 +34,7 @@ describe('Total time component', () => { }, }); - expect(vm.$el.textContent.trim()).toEqual('4 hrs 35 mins'); + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('4 hrs 35 mins'); }); it('should render information for seconds', () => { @@ -44,7 +44,7 @@ describe('Total time component', () => { }, }); - expect(vm.$el.textContent.trim()).toEqual('45 s'); + expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toEqual('45 s'); }); }); diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js index 6611b74594f..97f04844b3a 100644 --- a/spec/javascripts/pipelines/empty_state_spec.js +++ b/spec/javascripts/pipelines/empty_state_spec.js @@ -24,11 +24,11 @@ describe('Pipelines Empty State', () => { expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence'); expect( - component.$el.querySelector('p').textContent, + component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), ).toContain('Continous Integration can help catch bugs by running your tests automatically'); expect( - component.$el.querySelector('p').textContent, + component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), ).toContain('Continuous Deployment can help you deliver code to your product environment'); }); diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js index 43e7d9e1224..87259fe0bab 100644 --- a/spec/javascripts/registry/components/app_spec.js +++ b/spec/javascripts/registry/components/app_spec.js @@ -89,7 +89,7 @@ describe('Registry List', () => { it('should render empty message', (done) => { setTimeout(() => { expect( - vm.$el.querySelector('p').textContent.trim(), + vm.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), ).toEqual('No container images stored for this project. Add one by following the instructions above.'); done(); }, 0); diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js index 24209be83fe..5f980bbf36c 100644 --- a/spec/javascripts/vue_shared/components/markdown/field_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js @@ -12,14 +12,14 @@ describe('Markdown field component', () => { beforeEach((done) => { vm = new Vue({ + components: { + fieldComponent, + }, data() { return { text: 'testing\n123', }; }, - components: { - fieldComponent, - }, template: ` <field-component markdown-preview-path="/preview" diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js index 1465ef5855f..c63f15e5880 100644 --- a/spec/javascripts/vue_shared/components/table_pagination_spec.js +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js @@ -32,7 +32,7 @@ describe('Pagination component', () => { change: spy, }); - expect(component.$el.innerHTML).not.toBeDefined(); + expect(component.$el.childNodes.length).toEqual(0); }); describe('prev button', () => { @@ -72,7 +72,6 @@ describe('Pagination component', () => { }); component.$el.querySelector('.js-previous-button a').click(); - expect(spy).toHaveBeenCalledWith(1); }); }); diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb index b5d86df09d2..f302e412a6e 100644 --- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb +++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb @@ -74,14 +74,18 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do importer.create_project_if_needed end - it 'creates the Git repo in disk' do + it 'creates the Git repo on disk with the proper symlink for hooks' do create_bare_repository("#{project_path}.git") importer.create_project_if_needed project = Project.find_by_full_path(project_path) + repo_path = File.join(project.repository_storage_path, project.disk_path + '.git') + hook_path = File.join(repo_path, 'hooks') - expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.git')) + expect(File).to exist(repo_path) + expect(File.symlink?(hook_path)).to be true + expect(File.readlink(hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path) end context 'hashed storage enabled' do diff --git a/spec/lib/gitlab/git/gitlab_projects_spec.rb b/spec/lib/gitlab/git/gitlab_projects_spec.rb index a798b188a0d..beef843537d 100644 --- a/spec/lib/gitlab/git/gitlab_projects_spec.rb +++ b/spec/lib/gitlab/git/gitlab_projects_spec.rb @@ -25,51 +25,6 @@ describe Gitlab::Git::GitlabProjects do it { expect(gl_projects.logger).to eq(logger) } end - describe '#mv_project' do - let(:new_repo_path) { File.join(tmp_repos_path, 'repo.git') } - - it 'moves a repo directory' do - expect(File.exist?(tmp_repo_path)).to be_truthy - - message = "Moving repository from <#{tmp_repo_path}> to <#{new_repo_path}>." - expect(logger).to receive(:info).with(message) - - expect(gl_projects.mv_project('repo.git')).to be_truthy - - expect(File.exist?(tmp_repo_path)).to be_falsy - expect(File.exist?(new_repo_path)).to be_truthy - end - - it "fails if the source path doesn't exist" do - expected_source_path = File.join(tmp_repos_path, 'bad-src.git') - expect(logger).to receive(:error).with("mv-project failed: source path <#{expected_source_path}> does not exist.") - - result = build_gitlab_projects(tmp_repos_path, 'bad-src.git').mv_project('repo.git') - expect(result).to be_falsy - end - - it 'fails if the destination path already exists' do - FileUtils.mkdir_p(File.join(tmp_repos_path, 'already-exists.git')) - - expected_distination_path = File.join(tmp_repos_path, 'already-exists.git') - message = "mv-project failed: destination path <#{expected_distination_path}> already exists." - expect(logger).to receive(:error).with(message) - - expect(gl_projects.mv_project('already-exists.git')).to be_falsy - end - end - - describe '#rm_project' do - it 'removes a repo directory' do - expect(File.exist?(tmp_repo_path)).to be_truthy - expect(logger).to receive(:info).with("Removing repository <#{tmp_repo_path}>.") - - expect(gl_projects.rm_project).to be_truthy - - expect(File.exist?(tmp_repo_path)).to be_falsy - end - end - describe '#push_branches' do let(:remote_name) { 'remote-name' } let(:branch_name) { 'master' } diff --git a/spec/lib/gitlab/insecure_key_fingerprint_spec.rb b/spec/lib/gitlab/insecure_key_fingerprint_spec.rb new file mode 100644 index 00000000000..6532579b1c9 --- /dev/null +++ b/spec/lib/gitlab/insecure_key_fingerprint_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Gitlab::InsecureKeyFingerprint do + let(:key) do + 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn' \ + '1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qk' \ + 'r8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMg' \ + 'Jw0=' + end + + let(:fingerprint) { "3f:a2:ee:de:b5:de:53:c3:aa:2f:9c:45:24:4c:47:7b" } + + describe "#fingerprint" do + it "generates the key's fingerprint" do + expect(described_class.new(key.split[1]).fingerprint).to eq(fingerprint) + end + end +end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 68a57826647..8b54d72d6f7 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::Regex do it { is_expected.not_to match('?gitlab') } end - describe '.environment_slug_regex' do + describe '.environment_name_regex' do subject { described_class.environment_name_regex } it { is_expected.to match('foo') } @@ -24,6 +24,7 @@ describe Gitlab::Regex do it { is_expected.to match('foo.1') } it { is_expected.not_to match('9&foo') } it { is_expected.not_to match('foo-^') } + it { is_expected.not_to match('!!()()') } end describe '.environment_slug_regex' do diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index ffd2d2c7afc..2b61ce38418 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -52,6 +52,311 @@ describe Gitlab::Shell do end end + describe '#add_key' do + context 'when authorized_keys_enabled is true' do + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] + ) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + + context 'when authorized_keys_enabled is false' do + before do + stub_application_setting(authorized_keys_enabled: false) + end + + it 'does nothing' do + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + + context 'when authorized_keys_enabled is nil' do + before do + stub_application_setting(authorized_keys_enabled: nil) + end + + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] + ) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + end + + describe '#batch_add_keys' do + context 'when authorized_keys_enabled is true' do + it 'instantiates KeyAdder' do + expect_any_instance_of(Gitlab::Shell::KeyAdder).to receive(:add_key).with('key-123', 'ssh-rsa foobar') + + gitlab_shell.batch_add_keys do |adder| + adder.add_key('key-123', 'ssh-rsa foobar') + end + end + end + + context 'when authorized_keys_enabled is false' do + before do + stub_application_setting(authorized_keys_enabled: false) + end + + it 'does nothing' do + expect_any_instance_of(Gitlab::Shell::KeyAdder).not_to receive(:add_key) + + gitlab_shell.batch_add_keys do |adder| + adder.add_key('key-123', 'ssh-rsa foobar') + end + end + end + + context 'when authorized_keys_enabled is nil' do + before do + stub_application_setting(authorized_keys_enabled: nil) + end + + it 'instantiates KeyAdder' do + expect_any_instance_of(Gitlab::Shell::KeyAdder).to receive(:add_key).with('key-123', 'ssh-rsa foobar') + + gitlab_shell.batch_add_keys do |adder| + adder.add_key('key-123', 'ssh-rsa foobar') + end + end + end + end + + describe '#remove_key' do + context 'when authorized_keys_enabled is true' do + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'rm-key', 'key-123', 'ssh-rsa foobar'] + ) + + gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + end + end + + context 'when authorized_keys_enabled is false' do + before do + stub_application_setting(authorized_keys_enabled: false) + end + + it 'does nothing' do + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + + gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + end + end + + context 'when authorized_keys_enabled is nil' do + before do + stub_application_setting(authorized_keys_enabled: nil) + end + + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'rm-key', 'key-123', 'ssh-rsa foobar'] + ) + + gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + end + end + + context 'when key content is not given' do + it 'calls rm-key with only one argument' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'rm-key', 'key-123'] + ) + + gitlab_shell.remove_key('key-123') + end + end + end + + describe '#remove_all_keys' do + context 'when authorized_keys_enabled is true' do + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with([:gitlab_shell_keys_path, 'clear']) + + gitlab_shell.remove_all_keys + end + end + + context 'when authorized_keys_enabled is false' do + before do + stub_application_setting(authorized_keys_enabled: false) + end + + it 'does nothing' do + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + + gitlab_shell.remove_all_keys + end + end + + context 'when authorized_keys_enabled is nil' do + before do + stub_application_setting(authorized_keys_enabled: nil) + end + + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'clear'] + ) + + gitlab_shell.remove_all_keys + end + end + end + + describe '#remove_keys_not_found_in_db' do + context 'when keys are in the file that are not in the DB' do + before do + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') + @another_key = create(:key) # this one IS in the DB + end + + it 'removes the keys' do + expect(find_in_authorized_keys_file(1234)).to be_truthy + expect(find_in_authorized_keys_file(9876)).to be_truthy + expect(find_in_authorized_keys_file(@another_key.id)).to be_truthy + gitlab_shell.remove_keys_not_found_in_db + expect(find_in_authorized_keys_file(1234)).to be_falsey + expect(find_in_authorized_keys_file(9876)).to be_falsey + expect(find_in_authorized_keys_file(@another_key.id)).to be_truthy + end + end + + context 'when keys there are duplicate keys in the file that are not in the DB' do + before do + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + end + + it 'removes the keys' do + expect(find_in_authorized_keys_file(1234)).to be_truthy + gitlab_shell.remove_keys_not_found_in_db + expect(find_in_authorized_keys_file(1234)).to be_falsey + end + + it 'does not run remove more than once per key (in a batch)' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234').once + gitlab_shell.remove_keys_not_found_in_db + end + end + + context 'when keys there are duplicate keys in the file that ARE in the DB' do + before do + gitlab_shell.remove_all_keys + @key = create(:key) + gitlab_shell.add_key(@key.shell_id, @key.key) + end + + it 'does not remove the key' do + gitlab_shell.remove_keys_not_found_in_db + expect(find_in_authorized_keys_file(@key.id)).to be_truthy + end + + it 'does not need to run a SELECT query for that batch, on account of that key' do + expect_any_instance_of(ActiveRecord::Relation).not_to receive(:pluck) + gitlab_shell.remove_keys_not_found_in_db + end + end + + unless ENV['CI'] # Skip in CI, it takes 1 minute + context 'when the first batch can be skipped, but the next batch has keys that are not in the DB' do + before do + gitlab_shell.remove_all_keys + 100.times { |i| create(:key) } # first batch is all in the DB + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + end + + it 'removes the keys not in the DB' do + expect(find_in_authorized_keys_file(1234)).to be_truthy + gitlab_shell.remove_keys_not_found_in_db + expect(find_in_authorized_keys_file(1234)).to be_falsey + end + end + end + end + + describe '#batch_read_key_ids' do + context 'when there are keys in the authorized_keys file' do + before do + gitlab_shell.remove_all_keys + (1..4).each do |i| + gitlab_shell.add_key("key-#{i}", "ssh-rsa ASDFASDF#{i}") + end + end + + it 'iterates over the key IDs in the file, in batches' do + loop_count = 0 + first_batch = [1, 2] + second_batch = [3, 4] + + gitlab_shell.batch_read_key_ids(batch_size: 2) do |batch| + expected = (loop_count == 0 ? first_batch : second_batch) + expect(batch).to eq(expected) + loop_count += 1 + end + end + end + end + + describe '#list_key_ids' do + context 'when there are keys in the authorized_keys file' do + before do + gitlab_shell.remove_all_keys + (1..4).each do |i| + gitlab_shell.add_key("key-#{i}", "ssh-rsa ASDFASDF#{i}") + end + end + + it 'outputs the key IDs in the file, separated by newlines' do + ids = [] + gitlab_shell.list_key_ids do |io| + io.each do |line| + ids << line + end + end + + expect(ids).to eq(%W{1\n 2\n 3\n 4\n}) + end + end + + context 'when there are no keys in the authorized_keys file' do + before do + gitlab_shell.remove_all_keys + end + + it 'outputs nothing, not even an empty string' do + ids = [] + gitlab_shell.list_key_ids do |io| + io.each do |line| + ids << line + end + end + + expect(ids).to eq([]) + end + end + end + describe Gitlab::Shell::KeyAdder do describe '#add_key' do it 'removes trailing garbage' do @@ -97,17 +402,6 @@ describe Gitlab::Shell do allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800) end - describe '#add_key' do - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] - ) - - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') - end - end - describe '#add_repository' do shared_examples '#add_repository' do let(:repository_storage) { 'default' } @@ -149,32 +443,44 @@ describe Gitlab::Shell do end describe '#remove_repository' do - subject { gitlab_shell.remove_repository(project.repository_storage_path, project.disk_path) } + let!(:project) { create(:project, :repository) } + let(:disk_path) { "#{project.disk_path}.git" } it 'returns true when the command succeeds' do - expect(gitlab_projects).to receive(:rm_project) { true } + expect(gitlab_shell.exists?(project.repository_storage_path, disk_path)).to be(true) - is_expected.to be_truthy + expect(gitlab_shell.remove_repository(project.repository_storage_path, project.disk_path)).to be(true) + + expect(gitlab_shell.exists?(project.repository_storage_path, disk_path)).to be(false) end - it 'returns false when the command fails' do - expect(gitlab_projects).to receive(:rm_project) { false } + it 'keeps the namespace directory' do + gitlab_shell.remove_repository(project.repository_storage_path, project.disk_path) - is_expected.to be_falsy + expect(gitlab_shell.exists?(project.repository_storage_path, disk_path)).to be(false) + expect(gitlab_shell.exists?(project.repository_storage_path, project.disk_path.gsub(project.name, ''))).to be(true) end end describe '#mv_repository' do + let!(:project2) { create(:project, :repository) } + it 'returns true when the command succeeds' do - expect(gitlab_projects).to receive(:mv_project).with('project/newpath.git') { true } + old_path = project2.disk_path + new_path = "project/new_path" + + expect(gitlab_shell.exists?(project2.repository_storage_path, "#{old_path}.git")).to be(true) + expect(gitlab_shell.exists?(project2.repository_storage_path, "#{new_path}.git")).to be(false) + + expect(gitlab_shell.mv_repository(project2.repository_storage_path, old_path, new_path)).to be_truthy - expect(gitlab_shell.mv_repository(project.repository_storage_path, project.disk_path, 'project/newpath')).to be_truthy + expect(gitlab_shell.exists?(project2.repository_storage_path, "#{old_path}.git")).to be(false) + expect(gitlab_shell.exists?(project2.repository_storage_path, "#{new_path}.git")).to be(true) end it 'returns false when the command fails' do - expect(gitlab_projects).to receive(:mv_project).with('project/newpath.git') { false } - - expect(gitlab_shell.mv_repository(project.repository_storage_path, project.disk_path, 'project/newpath')).to be_falsy + expect(gitlab_shell.mv_repository(project2.repository_storage_path, project2.disk_path, '')).to be_falsy + expect(gitlab_shell.exists?(project2.repository_storage_path, "#{project2.disk_path}.git")).to be(true) end end @@ -412,4 +718,12 @@ describe Gitlab::Shell do end end end + + def find_in_authorized_keys_file(key_id) + gitlab_shell.batch_read_key_ids do |ids| + return true if ids.include?(key_id) + end + + false + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index c0db2c1b386..edd981752d9 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -412,6 +412,28 @@ describe Repository do end end + describe '#create_hooks' do + let(:hook_path) { File.join(repository.path_to_repo, 'hooks') } + + it 'symlinks the global hooks directory' do + repository.create_hooks + + expect(File.symlink?(hook_path)).to be true + expect(File.readlink(hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path) + end + + it 'replaces existing symlink with the right directory' do + FileUtils.mkdir_p(hook_path) + + expect(File.symlink?(hook_path)).to be false + + repository.create_hooks + + expect(File.symlink?(hook_path)).to be true + expect(File.readlink(hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path) + end + end + describe "#create_dir" do it "commits a change that creates a new directory" do expect do diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 0d2bd3207c0..34db50dc082 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -512,6 +512,31 @@ describe API::Commits do end end + context 'when stat param' do + let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}" } + + it 'is not present return stats by default' do + get api(route, user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to include 'stats' + end + + it "is false it does not include stats" do + get api(route, user), stats: false + + expect(response).to have_gitlab_http_status(200) + expect(json_response).not_to include 'stats' + end + + it "is true it includes stats" do + get api(route, user), stats: true + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to include 'stats' + end + end + context 'when unauthenticated', 'and project is public' do let(:project) { create(:project, :public, :repository) } diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 0462f494e15..837389451e8 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -68,6 +68,12 @@ describe API::Helpers do end it { is_expected.to eq(user) } + + it 'sets the environment with data of the current user' do + subject + + expect(env[API::Helpers::API_USER_ENV]).to eq({ user_id: subject.id, username: subject.username }) + end end context "HEAD request" do diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 7b25047ea8f..2783c51b8df 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -192,6 +192,54 @@ describe API::Internal do end end + describe "GET /internal/authorized_keys" do + context "using an existing key's fingerprint" do + it "finds the key" do + get(api('/internal/authorized_keys'), fingerprint: key.fingerprint, secret_token: secret_token) + + expect(response.status).to eq(200) + expect(json_response["key"]).to eq(key.key) + end + end + + context "non existing key's fingerprint" do + it "returns 404" do + get(api('/internal/authorized_keys'), fingerprint: "no:t-:va:li:d0", secret_token: secret_token) + + expect(response.status).to eq(404) + end + end + + context "using a partial fingerprint" do + it "returns 404" do + get(api('/internal/authorized_keys'), fingerprint: "#{key.fingerprint[0..5]}%", secret_token: secret_token) + + expect(response.status).to eq(404) + end + end + + context "sending the key" do + it "finds the key" do + get(api('/internal/authorized_keys'), key: key.key.split[1], secret_token: secret_token) + + expect(response.status).to eq(200) + expect(json_response["key"]).to eq(key.key) + end + + it "returns 404 with a partial key" do + get(api('/internal/authorized_keys'), key: key.key.split[1][0...-3], secret_token: secret_token) + + expect(response.status).to eq(404) + end + + it "returns 404 with an not valid base64 string" do + get(api('/internal/authorized_keys'), key: "whatever!", secret_token: secret_token) + + expect(response.status).to eq(404) + end + end + end + describe "POST /internal/allowed", :clean_gitlab_redis_shared_state do context "access granted" do around do |example| diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb index 8b115e01f47..34c543bffe8 100644 --- a/spec/requests/api/v3/commits_spec.rb +++ b/spec/requests/api/v3/commits_spec.rb @@ -403,6 +403,33 @@ describe API::V3::Commits do expect(response).to have_gitlab_http_status(200) expect(json_response['status']).to eq("created") end + + context 'when stat param' do + let(:project_id) { project.id } + let(:commit_id) { project.repository.commit.id } + let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}" } + + it 'is not present return stats by default' do + get v3_api(route, user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to include 'stats' + end + + it "is false it does not include stats" do + get v3_api(route, user), stats: false + + expect(response).to have_gitlab_http_status(200) + expect(json_response).not_to include 'stats' + end + + it "is true it includes stats" do + get v3_api(route, user), stats: true + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to include 'stats' + end + end end context "unauthorized user" do diff --git a/spec/tasks/gitlab/uploads_rake_spec.rb b/spec/tasks/gitlab/uploads_rake_spec.rb new file mode 100644 index 00000000000..ac0005e51e0 --- /dev/null +++ b/spec/tasks/gitlab/uploads_rake_spec.rb @@ -0,0 +1,27 @@ +require 'rake_helper' + +describe 'gitlab:uploads rake tasks' do + describe 'check' do + let!(:upload) { create(:upload, path: Rails.root.join('spec/fixtures/banana_sample.gif')) } + + before do + Rake.application.rake_require 'tasks/gitlab/uploads' + end + + it 'outputs the integrity check for each uploaded file' do + expect { run_rake_task('gitlab:uploads:check') }.to output(/Checking file \(#{upload.id}\): #{Regexp.quote(upload.absolute_path)}/).to_stdout + end + + it 'errors out about missing files on the file system' do + create(:upload) + + expect { run_rake_task('gitlab:uploads:check') }.to output(/File does not exist on the file system/).to_stdout + end + + it 'errors out about invalid checksum' do + upload.update_column(:checksum, '01a3156db2cf4f67ec823680b40b7302f89ab39179124ad219f94919b8a1769e') + + expect { run_rake_task('gitlab:uploads:check') }.to output(/File checksum \(9e697aa09fe196909813ee36103e34f721fe47a5fdc8aac0e4e4ac47b9b38282\) does not match the one in the database \(#{upload.checksum}\)/).to_stdout + end + end +end diff --git a/spec/workers/gitlab_shell_worker_spec.rb b/spec/workers/gitlab_shell_worker_spec.rb new file mode 100644 index 00000000000..6b222af454d --- /dev/null +++ b/spec/workers/gitlab_shell_worker_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe GitlabShellWorker do + let(:worker) { described_class.new } + + describe '#perform with add_key' do + it 'calls add_key on Gitlab::Shell' do + expect_any_instance_of(Gitlab::Shell).to receive(:add_key).with('foo', 'bar') + worker.perform(:add_key, 'foo', 'bar') + end + end +end diff --git a/yarn.lock b/yarn.lock index da9e50739cd..5d40e833889 100644 --- a/yarn.lock +++ b/yarn.lock @@ -97,6 +97,10 @@ acorn@^5.0.0, acorn@^5.0.3, acorn@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.1.tgz#53fe161111f912ab999ee887a90a0bc52822fd75" +acorn@^5.2.1: + version "5.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.3.0.tgz#7446d39459c54fb49a80e6ee6478149b940ec822" + after@0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" @@ -2069,14 +2073,14 @@ domelementtype@~1.1.1: resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" domhandler@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738" + version "2.4.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259" dependencies: domelementtype "1" domutils@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + version "1.6.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff" dependencies: dom-serializer "0" domelementtype "1" @@ -2368,7 +2372,7 @@ eslint-plugin-filenames@^1.1.0: lodash.kebabcase "4.0.1" lodash.snakecase "4.0.1" -eslint-plugin-html@^2.0.1: +eslint-plugin-html@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-2.0.1.tgz#3a829510e82522f1e2e44d55d7661a176121fce1" dependencies: @@ -2397,7 +2401,25 @@ eslint-plugin-promise@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.5.0.tgz#78fbb6ffe047201627569e85a6c5373af2a68fca" -eslint@^3.10.1: +eslint-plugin-vue@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-4.0.1.tgz#afda92cfd7e7363b1fbdb1a772dd63359a9ce96a" + dependencies: + require-all "^2.2.0" + vue-eslint-parser "^2.0.1" + +eslint-scope@^3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-visitor-keys@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" + +eslint@^3.18.0: version "3.19.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc" dependencies: @@ -2444,6 +2466,13 @@ espree@^3.4.0: acorn "^5.1.1" acorn-jsx "^3.0.0" +espree@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca" + dependencies: + acorn "^5.2.1" + acorn-jsx "^3.0.0" + esprima@2.7.x, esprima@^2.6.0, esprima@^2.7.1: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" @@ -5546,6 +5575,10 @@ request@^2.81.0: tunnel-agent "^0.6.0" uuid "^3.0.0" +require-all@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/require-all/-/require-all-2.2.0.tgz#b4420c233ac0282d0ff49b277fb880a8b5de0894" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -6504,6 +6537,17 @@ void-elements@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" +vue-eslint-parser@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.1.tgz#30135771c4fad00fdbac4542a2d59f3b1d776834" + dependencies: + debug "^3.1.0" + eslint-scope "^3.7.1" + eslint-visitor-keys "^1.0.0" + espree "^3.5.2" + esquery "^1.0.0" + lodash "^4.17.4" + vue-hot-reload-api@^2.2.0: version "2.2.4" resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.2.4.tgz#683bd1d026c0d3b3c937d5875679e9a87ec6cd8f" |