diff options
44 files changed, 489 insertions, 539 deletions
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index 86d99dd87da..c7dc6863160 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -1,29 +1,25 @@ -/* eslint-disable no-param-reassign */ - import Vue from 'vue'; -import VueResource from 'vue-resource'; -import CommitPipelinesTable from './pipelines_table'; - -Vue.use(VueResource); +import commitPipelinesTable from './pipelines_table.vue'; /** - * Commits View > Pipelines Tab > Pipelines Table. - * - * Renders Pipelines table in pipelines tab in the commits show view. + * Used in: + * - Commit details View > Pipelines Tab > Pipelines Table. + * - Merge Request details View > Pipelines Tab > Pipelines Table. + * - New Merge Request View > Pipelines Tab > Pipelines Table. */ -// export for use in merge_request_tabs.js (TODO: remove this hack) -window.gl = window.gl || {}; -window.gl.CommitPipelinesTable = CommitPipelinesTable; - -$(() => { - gl.commits = gl.commits || {}; - gl.commits.pipelines = gl.commits.pipelines || {}; +const CommitPipelinesTable = Vue.extend(commitPipelinesTable); +document.addEventListener('DOMContentLoaded', () => { const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { - gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable().$mount(); - pipelineTableViewEl.appendChild(gl.commits.pipelines.PipelinesTableBundle.$el); + const table = new CommitPipelinesTable({ + propsData: { + endpoint: pipelineTableViewEl.dataset.endpoint, + helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + }, + }).$mount(); + pipelineTableViewEl.appendChild(table.$el); } }); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js deleted file mode 100644 index 70ba83ce5b9..00000000000 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js +++ /dev/null @@ -1,191 +0,0 @@ -import Vue from 'vue'; -import Visibility from 'visibilityjs'; -import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue'; -import PipelinesService from '../../pipelines/services/pipelines_service'; -import PipelineStore from '../../pipelines/stores/pipelines_store'; -import eventHub from '../../pipelines/event_hub'; -import emptyState from '../../pipelines/components/empty_state.vue'; -import errorState from '../../pipelines/components/error_state.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import '../../lib/utils/common_utils'; -import '../../vue_shared/vue_resource_interceptor'; -import Poll from '../../lib/utils/poll'; - -/** - * - * Uses `pipelines-table-component` to render Pipelines table with an API call. - * Endpoint is provided in HTML and passed as `endpoint`. - * We need a store to store the received environemnts. - * We need a service to communicate with the server. - * - */ - -export default Vue.component('pipelines-table', { - - components: { - pipelinesTableComponent, - errorState, - emptyState, - loadingIcon, - }, - - /** - * Accesses the DOM to provide the needed data. - * Returns the necessary props to render `pipelines-table-component` component. - * - * @return {Object} - */ - data() { - const store = new PipelineStore(); - - return { - endpoint: null, - helpPagePath: null, - store, - state: store.state, - isLoading: false, - hasError: false, - isMakingRequest: false, - updateGraphDropdown: false, - hasMadeRequest: false, - }; - }, - - computed: { - shouldRenderErrorState() { - return this.hasError && !this.isLoading; - }, - - /** - * Empty state is only rendered if after the first request we receive no pipelines. - * - * @return {Boolean} - */ - shouldRenderEmptyState() { - return !this.state.pipelines.length && - !this.isLoading && - this.hasMadeRequest && - !this.hasError; - }, - - shouldRenderTable() { - return !this.isLoading && - this.state.pipelines.length > 0 && - !this.hasError; - }, - }, - - /** - * When the component is about to be mounted, tell the service to fetch the data - * - * A request to fetch the pipelines will be made. - * In case of a successfull response we will store the data in the provided - * store, in case of a failed response we need to warn the user. - * - */ - beforeMount() { - const element = document.querySelector('#commit-pipeline-table-view'); - - this.endpoint = element.dataset.endpoint; - this.helpPagePath = element.dataset.helpPagePath; - this.service = new PipelinesService(this.endpoint); - - this.poll = new Poll({ - resource: this.service, - method: 'getPipelines', - successCallback: this.successCallback, - errorCallback: this.errorCallback, - notificationCallback: this.setIsMakingRequest, - }); - - if (!Visibility.hidden()) { - this.isLoading = true; - this.poll.makeRequest(); - } else { - // If tab is not visible we need to make the first request so we don't show the empty - // state without knowing if there are any pipelines - this.fetchPipelines(); - } - - Visibility.change(() => { - if (!Visibility.hidden()) { - this.poll.restart(); - } else { - this.poll.stop(); - } - }); - - eventHub.$on('refreshPipelines', this.fetchPipelines); - }, - - beforeDestroy() { - eventHub.$off('refreshPipelines'); - }, - - destroyed() { - this.poll.stop(); - }, - - methods: { - fetchPipelines() { - this.isLoading = true; - - return this.service.getPipelines() - .then(response => this.successCallback(response)) - .catch(() => this.errorCallback()); - }, - - successCallback(resp) { - const response = resp.json(); - - this.hasMadeRequest = true; - - // depending of the endpoint the response can either bring a `pipelines` key or not. - const pipelines = response.pipelines || response; - this.store.storePipelines(pipelines); - this.isLoading = false; - this.updateGraphDropdown = true; - }, - - errorCallback() { - this.hasError = true; - this.isLoading = false; - this.updateGraphDropdown = false; - }, - - setIsMakingRequest(isMakingRequest) { - this.isMakingRequest = isMakingRequest; - - if (isMakingRequest) { - this.updateGraphDropdown = false; - } - }, - }, - - template: ` - <div class="content-list pipelines"> - - <loading-icon - label="Loading pipelines" - size="3" - v-if="isLoading" - /> - - <empty-state - v-if="shouldRenderEmptyState" - :help-page-path="helpPagePath" /> - - <error-state v-if="shouldRenderErrorState" /> - - <div - class="table-holder" - v-if="shouldRenderTable"> - <pipelines-table-component - :pipelines="state.pipelines" - :service="service" - :update-graph-dropdown="updateGraphDropdown" - /> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue new file mode 100644 index 00000000000..3c77f14d533 --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -0,0 +1,90 @@ +<script> + import PipelinesService from '../../pipelines/services/pipelines_service'; + import PipelineStore from '../../pipelines/stores/pipelines_store'; + import pipelinesMixin from '../../pipelines/mixins/pipelines'; + + export default { + props: { + endpoint: { + type: String, + required: true, + }, + helpPagePath: { + type: String, + required: true, + }, + }, + mixins: [ + pipelinesMixin, + ], + + data() { + const store = new PipelineStore(); + + return { + store, + state: store.state, + }; + }, + + computed: { + /** + * Empty state is only rendered if after the first request we receive no pipelines. + * + * @return {Boolean} + */ + shouldRenderEmptyState() { + return !this.state.pipelines.length && + !this.isLoading && + this.hasMadeRequest && + !this.hasError; + }, + + shouldRenderTable() { + return !this.isLoading && + this.state.pipelines.length > 0 && + !this.hasError; + }, + }, + created() { + this.service = new PipelinesService(this.endpoint); + }, + methods: { + successCallback(resp) { + const response = resp.json(); + + // depending of the endpoint the response can either bring a `pipelines` key or not. + const pipelines = response.pipelines || response; + this.setCommonData(pipelines); + }, + }, + }; +</script> +<template> + <div class="content-list pipelines"> + + <loading-icon + label="Loading pipelines" + size="3" + v-if="isLoading" + /> + + <empty-state + v-if="shouldRenderEmptyState" + :help-page-path="helpPagePath" + /> + + <error-state + v-if="shouldRenderErrorState" + /> + + <div + class="table-holder" + v-if="shouldRenderTable"> + <pipelines-table-component + :pipelines="state.pipelines" + :update-graph-dropdown="updateGraphDropdown" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index e14414d3f68..8473a81bc88 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -51,6 +51,11 @@ export default { required: false, default: '', }, + initialTaskStatus: { + type: String, + required: false, + default: '', + }, updatedAt: { type: String, required: false, @@ -105,6 +110,7 @@ export default { updatedAt: this.updatedAt, updatedByName: this.updatedByName, updatedByPath: this.updatedByPath, + taskStatus: this.initialTaskStatus, }); return { diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index bb95ff0101b..43db66c8e08 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -37,18 +37,7 @@ }); }, taskStatus() { - const taskRegexMatches = this.taskStatus.match(/(\d+) of (\d+)/); - const $issuableHeader = $('.issuable-meta'); - const $tasks = $('#task_status', $issuableHeader); - const $tasksShort = $('#task_status_short', $issuableHeader); - - if (taskRegexMatches) { - $tasks.text(this.taskStatus); - $tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`); - } else { - $tasks.text(''); - $tasksShort.text(''); - } + this.updateTaskStatusText(); }, }, methods: { @@ -64,9 +53,24 @@ }); } }, + updateTaskStatusText() { + const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/); + const $issuableHeader = $('.issuable-meta'); + const $tasks = $('#task_status', $issuableHeader); + const $tasksShort = $('#task_status_short', $issuableHeader); + + if (taskRegexMatches) { + $tasks.text(this.taskStatus); + $tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`); + } else { + $tasks.text(''); + $tasksShort.text(''); + } + }, }, mounted() { this.renderGFM(); + this.updateTaskStatusText(); }, }; </script> diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index 14b2a1e18e9..ad8cb6465e2 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -45,6 +45,7 @@ document.addEventListener('DOMContentLoaded', () => { updatedAt: this.updatedAt, updatedByName: this.updatedByName, updatedByPath: this.updatedByPath, + initialTaskStatus: this.initialTaskStatus, }, }); }, diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js index 27c2d349f52..f2b822f3cbb 100644 --- a/app/assets/javascripts/issue_show/stores/index.js +++ b/app/assets/javascripts/issue_show/stores/index.js @@ -1,23 +1,6 @@ export default class Store { - constructor({ - titleHtml, - titleText, - descriptionHtml, - descriptionText, - updatedAt, - updatedByName, - updatedByPath, - }) { - this.state = { - titleHtml, - titleText, - descriptionHtml, - descriptionText, - taskStatus: '', - updatedAt, - updatedByName, - updatedByPath, - }; + constructor(initialState) { + this.state = initialState; this.formState = { title: '', confidential: false, diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 894ed81b044..f503cd38c24 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -3,10 +3,12 @@ /* global Flash */ /* global notes */ +import Vue from 'vue'; import Cookies from 'js-cookie'; import './breakpoints'; import './flash'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; +import commitPipelinesTable from './commit/pipelines/pipelines_table.vue'; /* eslint-disable max-len */ // MergeRequestTabs @@ -233,11 +235,18 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; } mountPipelinesView() { - this.commitPipelinesTable = new gl.CommitPipelinesTable().$mount(); + const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); + const CommitPipelinesTable = Vue.extend(commitPipelinesTable); + this.commitPipelinesTable = new CommitPipelinesTable({ + propsData: { + endpoint: pipelineTableViewEl.dataset.endpoint, + helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + }, + }).$mount(); + // $mount(el) replaces the el with the new rendered component. We need it in order to mount // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount - document.querySelector('#commit-pipeline-table-view') - .appendChild(this.commitPipelinesTable.$el); + pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el); } loadDiff(source) { diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue index 37a6f02d8fd..abcd0c4ecea 100644 --- a/app/assets/javascripts/pipelines/components/async_button.vue +++ b/app/assets/javascripts/pipelines/components/async_button.vue @@ -1,9 +1,9 @@ <script> /* eslint-disable no-new, no-alert */ -/* global Flash */ -import '~/flash'; + import eventHub from '../event_hub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import tooltipMixin from '../../vue_shared/mixins/tooltip'; export default { props: { @@ -11,53 +11,42 @@ export default { type: String, required: true, }, - - service: { - type: Object, - required: true, - }, - title: { type: String, required: true, }, - icon: { type: String, required: true, }, - cssClass: { type: String, required: true, }, - confirmActionMessage: { type: String, required: false, }, }, - components: { loadingIcon, }, - + mixins: [ + tooltipMixin, + ], data() { return { isLoading: false, }; }, - computed: { iconClass() { return `fa fa-${this.icon}`; }, - buttonClass() { - return `btn has-tooltip ${this.cssClass}`; + return `btn ${this.cssClass}`; }, }, - methods: { onClick() { if (this.confirmActionMessage && confirm(this.confirmActionMessage)) { @@ -66,21 +55,11 @@ export default { this.makeRequest(); } }, - makeRequest() { this.isLoading = true; - $(this.$el).tooltip('destroy'); - - this.service.postAction(this.endpoint) - .then(() => { - this.isLoading = false; - eventHub.$emit('refreshPipelines'); - }) - .catch(() => { - this.isLoading = false; - new Flash('An error occured while making the request.'); - }); + $(this.$refs.tooltip).tooltip('destroy'); + eventHub.$emit('postAction', this.endpoint); }, }, }; @@ -95,10 +74,12 @@ export default { :aria-label="title" data-container="body" data-placement="top" + ref="tooltip" :disabled="isLoading"> <i :class="iconClass" - aria-hidden="true" /> + aria-hidden="true"> + </i> <loading-icon v-if="isLoading" /> </button> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index fed42d23112..01ae07aad65 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -1,15 +1,9 @@ <script> - import Visibility from 'visibilityjs'; import PipelinesService from '../services/pipelines_service'; - import eventHub from '../event_hub'; - import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue'; + import pipelinesMixin from '../mixins/pipelines'; import tablePagination from '../../vue_shared/components/table_pagination.vue'; - import emptyState from './empty_state.vue'; - import errorState from './error_state.vue'; import navigationTabs from './navigation_tabs.vue'; import navigationControls from './nav_controls.vue'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import Poll from '../../lib/utils/poll'; export default { props: { @@ -20,13 +14,12 @@ }, components: { tablePagination, - pipelinesTableComponent, - emptyState, - errorState, navigationTabs, navigationControls, - loadingIcon, }, + mixins: [ + pipelinesMixin, + ], data() { const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; @@ -47,11 +40,6 @@ state: this.store.state, apiScope: 'all', pagenum: 1, - isLoading: false, - hasError: false, - isMakingRequest: false, - updateGraphDropdown: false, - hasMadeRequest: false, }; }, computed: { @@ -62,9 +50,6 @@ const scope = gl.utils.getParameterByName('scope'); return scope === null ? 'all' : scope; }, - shouldRenderErrorState() { - return this.hasError && !this.isLoading; - }, /** * The empty state should only be rendered when the request is made to fetch all pipelines @@ -106,7 +91,6 @@ this.state.pipelines.length && this.state.pageInfo.total > this.state.pageInfo.perPage; }, - hasCiEnabled() { return this.hasCi !== undefined; }, @@ -129,37 +113,7 @@ }, created() { this.service = new PipelinesService(this.endpoint); - - const poll = new Poll({ - resource: this.service, - method: 'getPipelines', - data: { page: this.pageParameter, scope: this.scopeParameter }, - successCallback: this.successCallback, - errorCallback: this.errorCallback, - notificationCallback: this.setIsMakingRequest, - }); - - if (!Visibility.hidden()) { - this.isLoading = true; - poll.makeRequest(); - } else { - // If tab is not visible we need to make the first request so we don't show the empty - // state without knowing if there are any pipelines - this.fetchPipelines(); - } - - Visibility.change(() => { - if (!Visibility.hidden()) { - poll.restart(); - } else { - poll.stop(); - } - }); - - eventHub.$on('refreshPipelines', this.fetchPipelines); - }, - beforeDestroy() { - eventHub.$off('refreshPipelines'); + this.requestData = { page: this.pageParameter, scope: this.scopeParameter }; }, methods: { /** @@ -174,15 +128,6 @@ return param; }, - fetchPipelines() { - if (!this.isMakingRequest) { - this.isLoading = true; - - this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter }) - .then(response => this.successCallback(response)) - .catch(() => this.errorCallback()); - } - }, successCallback(resp) { const response = { headers: resp.headers, @@ -190,33 +135,14 @@ }; this.store.storeCount(response.body.count); - this.store.storePipelines(response.body.pipelines); this.store.storePagination(response.headers); - - this.isLoading = false; - this.updateGraphDropdown = true; - this.hasMadeRequest = true; - }, - - errorCallback() { - this.hasError = true; - this.isLoading = false; - this.updateGraphDropdown = false; - }, - - setIsMakingRequest(isMakingRequest) { - this.isMakingRequest = isMakingRequest; - - if (isMakingRequest) { - this.updateGraphDropdown = false; - } + this.setCommonData(response.body.pipelines); }, }, }; </script> <template> <div :class="cssClass"> - <div class="top-area scrolling-tabs-container inner-page-scroll-tabs" v-if="!isLoading && !shouldRenderEmptyState"> @@ -274,7 +200,6 @@ <pipelines-table-component :pipelines="state.pipelines" - :service="service" :update-graph-dropdown="updateGraphDropdown" /> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index 97b4de26214..a6fc4f04237 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -11,10 +11,6 @@ type: Array, required: true, }, - service: { - type: Object, - required: true, - }, }, components: { loadingIcon, @@ -31,17 +27,9 @@ $(this.$refs.tooltip).tooltip('destroy'); - this.service.postAction(endpoint) - .then(() => { - this.isLoading = false; - eventHub.$emit('refreshPipelines'); - }) - .catch(() => { - this.isLoading = false; - // eslint-disable-next-line no-new - new Flash('An error occured while making the request.'); - }); + eventHub.$emit('postAction', endpoint); }, + isActionDisabled(action) { if (action.playable === undefined) { return false; diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 884f1ce9689..5088d92209f 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -12,10 +12,6 @@ type: Array, required: true, }, - service: { - type: Object, - required: true, - }, updateGraphDropdown: { type: Boolean, required: false, @@ -57,7 +53,6 @@ v-for="model in pipelines" :key="model.id" :pipeline="model" - :service="service" :update-graph-dropdown="updateGraphDropdown" /> </div> diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 4d5ebe2e9ed..c3f1c426d8a 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -1,13 +1,13 @@ <script> /* eslint-disable no-param-reassign */ -import asyncButtonComponent from '../../pipelines/components/async_button.vue'; -import pipelinesActionsComponent from '../../pipelines/components/pipelines_actions.vue'; -import pipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts.vue'; -import ciBadge from './ci_badge_link.vue'; -import pipelineStage from '../../pipelines/components/stage.vue'; -import pipelineUrl from '../../pipelines/components/pipeline_url.vue'; -import pipelinesTimeago from '../../pipelines/components/time_ago.vue'; -import commitComponent from './commit.vue'; +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. @@ -20,10 +20,6 @@ export default { type: Object, required: true, }, - service: { - type: Object, - required: true, - }, updateGraphDropdown: { type: Boolean, required: false, @@ -271,7 +267,6 @@ export default { <pipelines-actions-component v-if="pipeline.details.manual_actions.length" :actions="pipeline.details.manual_actions" - :service="service" /> <pipelines-artifacts-component @@ -282,7 +277,6 @@ export default { <async-button-component v-if="pipeline.flags.retryable" - :service="service" :endpoint="pipeline.retry_path" css-class="js-pipelines-retry-button btn-default btn-retry" title="Retry" @@ -291,7 +285,6 @@ export default { <async-button-component v-if="pipeline.flags.cancelable" - :service="service" :endpoint="pipeline.cancel_path" css-class="js-pipelines-cancel-button btn-remove" title="Cancel" diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js new file mode 100644 index 00000000000..9adc15e6266 --- /dev/null +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -0,0 +1,103 @@ +/* global Flash */ +import '~/flash'; +import Visibility from 'visibilityjs'; +import Poll from '../../lib/utils/poll'; +import emptyState from '../components/empty_state.vue'; +import errorState from '../components/error_state.vue'; +import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import pipelinesTableComponent from '../components/pipelines_table.vue'; +import eventHub from '../event_hub'; + +export default { + components: { + pipelinesTableComponent, + errorState, + emptyState, + loadingIcon, + }, + computed: { + shouldRenderErrorState() { + return this.hasError && !this.isLoading; + }, + }, + data() { + return { + isLoading: false, + hasError: false, + isMakingRequest: false, + updateGraphDropdown: false, + hasMadeRequest: false, + }; + }, + beforeMount() { + this.poll = new Poll({ + resource: this.service, + method: 'getPipelines', + data: this.requestData ? this.requestData : undefined, + successCallback: this.successCallback, + errorCallback: this.errorCallback, + notificationCallback: this.setIsMakingRequest, + }); + + if (!Visibility.hidden()) { + this.isLoading = true; + this.poll.makeRequest(); + } else { + // If tab is not visible we need to make the first request so we don't show the empty + // state without knowing if there are any pipelines + this.fetchPipelines(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); + + eventHub.$on('refreshPipelines', this.fetchPipelines); + eventHub.$on('postAction', this.postAction); + }, + beforeDestroy() { + eventHub.$off('refreshPipelines'); + eventHub.$on('postAction', this.postAction); + }, + destroyed() { + this.poll.stop(); + }, + methods: { + fetchPipelines() { + if (!this.isMakingRequest) { + this.isLoading = true; + + this.service.getPipelines(this.requestData) + .then(response => this.successCallback(response)) + .catch(() => this.errorCallback()); + } + }, + setCommonData(pipelines) { + this.store.storePipelines(pipelines); + this.isLoading = false; + this.updateGraphDropdown = true; + this.hasMadeRequest = true; + }, + errorCallback() { + this.hasError = true; + this.isLoading = false; + this.updateGraphDropdown = false; + }, + setIsMakingRequest(isMakingRequest) { + this.isMakingRequest = isMakingRequest; + + if (isMakingRequest) { + this.updateGraphDropdown = false; + } + }, + postAction(endpoint) { + this.service.postAction(endpoint) + .then(() => eventHub.$emit('refreshPipelines')) + .catch(() => new Flash('An error occured while making the request.')); + }, + }, +}; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index b3f310ff67d..4abad3f2697 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -729,33 +729,3 @@ } } } - -.confidential-issue-warning { - background-color: $gl-gray; - border-radius: 3px; - padding: $gl-btn-padding $gl-padding; - margin-top: $gl-padding-top; - font-size: 14px; - color: $white-light; - - .fa { - margin-right: 8px; - } - - a { - color: $white-light; - text-decoration: underline; - } - - &.affix { - position: static; - width: initial; - - @media (min-width: $screen-sm-min) { - position: sticky; - position: -webkit-sticky; - top: 60px; - z-index: 200; - } - } -} diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index aa307414737..69fed4e6bf7 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -103,6 +103,42 @@ } } +.confidential-issue-warning { + background-color: $gray-normal; + border-radius: 3px; + padding: 3px 12px; + margin: auto; + margin-top: 0; + text-align: center; + font-size: 12px; + align-items: center; + + @media (max-width: $screen-md-max) { + // On smaller devices the warning becomes the fourth item in the list, + // rather than centering, and grows to span the full width of the + // comment area. + order: 4; + margin: 6px auto; + width: 100%; + } + + .fa { + margin-right: 8px; + } +} + +.right-sidebar-expanded { + .confidential-issue-warning { + // When the sidebar is open the warning becomes the fourth item in the list, + // rather than centering, and grows to span the full width of the + // comment area. + order: 4; + margin: 6px auto; + width: 100%; + } +} + + .discussion-form { padding: $gl-padding-top $gl-padding $gl-padding; background-color: $white-light; diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 5e8f0849969..3259a9c1933 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -138,8 +138,8 @@ module IssuablesHelper end output << " ".html_safe - output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm") - output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg") + output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "hidden-xs hidden-sm") + output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "hidden-md hidden-lg") output end @@ -216,7 +216,8 @@ module IssuablesHelper initialTitleHtml: markdown_field(issuable, :title), initialTitleText: issuable.title, initialDescriptionHtml: markdown_field(issuable, :description), - initialDescriptionText: issuable.description + initialDescriptionText: issuable.description, + initialTaskStatus: issuable.task_status } data.merge!(updated_at_by(issuable)) diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb index 3edc395033c..d63d4ec2b12 100644 --- a/app/models/project_services/chat_message/pipeline_message.rb +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -70,7 +70,7 @@ module ChatMessage end def branch_link - "`[#{ref}](#{branch_url})`" + "[#{ref}](#{branch_url})" end def project_link diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb index 04a59d559ca..c52dd6ef8ef 100644 --- a/app/models/project_services/chat_message/push_message.rb +++ b/app/models/project_services/chat_message/push_message.rb @@ -61,7 +61,7 @@ module ChatMessage end def removed_branch_message - "#{user_name} removed #{ref_type} `#{ref}` from #{project_link}" + "#{user_name} removed #{ref_type} #{ref} from #{project_link}" end def push_message @@ -102,7 +102,7 @@ module ChatMessage end def branch_link - "`[#{ref}](#{branch_url})`" + "[#{ref}](#{branch_url})" end def project_link diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 95dffdafabe..b21d5665970 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -325,6 +325,10 @@ = f.label :prometheus_metrics_enabled do = f.check_box :prometheus_metrics_enabled Enable Prometheus Metrics + - unless Gitlab::Metrics.metrics_folder_present? + .help-block + %strong.cred WARNING: + Environment variable `prometheus_multiproc_dir` does not exist or is not pointing to a valid directory. %fieldset %legend Background Jobs diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 07445434cf3..d0698285f84 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -9,6 +9,12 @@ %li %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } Preview + + - if defined?(@issue) && @issue.confidential? + %li.confidential-issue-warning + = icon('warning') + %span This is a confidential issue. Your comment will not be visible to the public. + %li.pull-right .toolbar-group = markdown_toolbar_button({ icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" }) diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 5f92d020eef..d909b0bfbbd 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -5,13 +5,6 @@ - can_update_issue = can?(current_user, :update_issue, @issue) - can_report_spam = @issue.submittable_as_spam_by?(current_user) -- if defined?(@issue) && @issue.confidential? - .confidential-issue-warning{ data: { spy: 'affix' } } - %span.confidential-issue-text - #{confidential_icon(@issue)} This issue is confidential. - %a{ href: help_page_path('user/project/issues/confidential_issues'), target: '_blank' } - What are confidential issues? - .clearfix.detail-page-header .issuable-header .issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) } @@ -26,6 +19,7 @@ = icon('angle-double-left') .issuable-meta + = confidential_icon(@issue) = issuable_meta(@issue, @project, "Issue") .issuable-actions diff --git a/changelogs/unreleased/33260-allow-admins-to-list-admins.yml b/changelogs/unreleased/33260-allow-admins-to-list-admins.yml new file mode 100644 index 00000000000..a3b2053e005 --- /dev/null +++ b/changelogs/unreleased/33260-allow-admins-to-list-admins.yml @@ -0,0 +1,4 @@ +--- +title: Reinstate is_admin flag in users api when authenticated user is an admin +merge_request: 12211 +author: rickettm diff --git a/config/boot.rb b/config/boot.rb index db5ab918021..16de55d7a86 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -6,7 +6,9 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) # set default directory for multiproces metrics gathering -ENV['prometheus_multiproc_dir'] ||= 'tmp/prometheus_multiproc_dir' +if ENV['RAILS_ENV'] == 'development' || ENV['RAILS_ENV'] == 'test' + ENV['prometheus_multiproc_dir'] ||= 'tmp/prometheus_multiproc_dir' +end # Default Bootsnap configuration from https://github.com/Shopify/bootsnap#usage require 'bootsnap' diff --git a/config/karma.config.js b/config/karma.config.js index 5911a9a7e10..2f571978e08 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -66,5 +66,14 @@ module.exports = function(config) { karmaConfig.customLaunchers.ChromeHeadlessCustom.flags.push('--enable-logging', '--v=1'); } + if (process.env.DEBUG) { + karmaConfig.logLevel = config.LOG_DEBUG; + process.env.CHROME_LOG_FILE = process.env.CHROME_LOG_FILE || 'chrome_debug.log'; + } + + if (process.env.CHROME_LOG_FILE) { + karmaConfig.customLaunchers.ChromeHeadlessCustom.flags.push('--enable-logging', '--v=1'); + } + config.set(karmaConfig); }; diff --git a/doc/api/README.md b/doc/api/README.md index 4f189c16673..b7f6ee69193 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -29,10 +29,10 @@ following locations: - [Labels](labels.md) - [Merge Requests](merge_requests.md) - [Milestones](milestones.md) -- [Open source license templates](templates/licenses.md) - [Namespaces](namespaces.md) - [Notes](notes.md) (comments) - [Notification settings](notification_settings.md) +- [Open source license templates](templates/licenses.md) - [Pipelines](pipelines.md) - [Pipeline Triggers](pipeline_triggers.md) - [Pipeline Schedules](pipeline_schedules.md) diff --git a/doc/api/users.md b/doc/api/users.md index b1ebd7b0c47..cf09b8f44aa 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -62,6 +62,7 @@ GET /users "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", "web_url": "http://localhost:3000/john_smith", "created_at": "2012-05-23T08:00:58Z", + "is_admin": false, "bio": null, "location": null, "skype": "", @@ -94,6 +95,7 @@ GET /users "avatar_url": "http://localhost:3000/uploads/user/avatar/2/index.jpg", "web_url": "http://localhost:3000/jack_smith", "created_at": "2012-05-23T08:01:01Z", + "is_admin": false, "bio": null, "location": null, "skype": "", @@ -197,6 +199,7 @@ Parameters: "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", "web_url": "http://localhost:3000/john_smith", "created_at": "2012-05-23T08:00:58Z", + "is_admin": false, "bio": null, "location": null, "skype": "", diff --git a/doc/user/project/issues/confidential_issues.md b/doc/user/project/issues/confidential_issues.md index 208be7d0ed5..1760b182114 100644 --- a/doc/user/project/issues/confidential_issues.md +++ b/doc/user/project/issues/confidential_issues.md @@ -43,8 +43,9 @@ next to the issues that are marked as confidential. --- -While inside the issue, you can see a persistent dark banner at the top of the -screen. +Likewise, while inside the issue, you can see the eye-slash icon right next to +the issue number, but there is also an indicator in the comment area that the +issue you are commenting on is confidential. ![Confidential issue page](img/confidential_issues_issue_page.png) diff --git a/doc/user/project/issues/img/confidential_issues_issue_page.png b/doc/user/project/issues/img/confidential_issues_issue_page.png Binary files differindex 91f7cc8d3ca..f04ec8ff32b 100755 --- a/doc/user/project/issues/img/confidential_issues_issue_page.png +++ b/doc/user/project/issues/img/confidential_issues_issue_page.png diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 412443a2405..8bce79529e6 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -43,11 +43,14 @@ module API expose :external end - class UserWithPrivateDetails < UserPublic - expose :private_token + class UserWithAdmin < UserPublic expose :admin?, as: :is_admin end + class UserWithPrivateDetails < UserWithAdmin + expose :private_token + end + class Email < Grape::Entity expose :id, :email end diff --git a/lib/api/users.rb b/lib/api/users.rb index 7257ecb5b67..bfb69d6dc18 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -59,7 +59,7 @@ module API users = UsersFinder.new(current_user, params).execute - entity = current_user.admin? ? Entities::UserPublic : Entities::UserBasic + entity = current_user.admin? ? Entities::UserWithAdmin : Entities::UserBasic present paginate(users), with: entity end diff --git a/lib/gitlab/metrics/prometheus.rb b/lib/gitlab/metrics/prometheus.rb index 60686509332..9d314a56e58 100644 --- a/lib/gitlab/metrics/prometheus.rb +++ b/lib/gitlab/metrics/prometheus.rb @@ -5,8 +5,16 @@ module Gitlab module Prometheus include Gitlab::CurrentSettings + def metrics_folder_present? + ENV.has_key?('prometheus_multiproc_dir') && + ::Dir.exist?(ENV['prometheus_multiproc_dir']) && + ::File.writable?(ENV['prometheus_multiproc_dir']) + end + def prometheus_metrics_enabled? - @prometheus_metrics_enabled ||= current_application_settings[:prometheus_metrics_enabled] || false + return @prometheus_metrics_enabled if defined?(@prometheus_metrics_enabled) + + @prometheus_metrics_enabled = prometheus_metrics_enabled_unmemoized end def registry @@ -36,6 +44,12 @@ module Gitlab NullMetric.new end end + + private + + def prometheus_metrics_enabled_unmemoized + metrics_folder_present? && current_application_settings[:prometheus_metrics_enabled] || false + end end end end diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index ebfd60198b2..694f94efcff 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -1,15 +1,15 @@ import Vue from 'vue'; -import PipelinesTable from '~/commit/pipelines/pipelines_table'; +import pipelinesTable from '~/commit/pipelines/pipelines_table.vue'; describe('Pipelines table in Commits and Merge requests', () => { const jsonFixtureName = 'pipelines/pipelines.json'; let pipeline; + let PipelinesTable; - preloadFixtures('static/pipelines_table.html.raw'); preloadFixtures(jsonFixtureName); beforeEach(() => { - loadFixtures('static/pipelines_table.html.raw'); + PipelinesTable = Vue.extend(pipelinesTable); const pipelines = getJSONFixture(jsonFixtureName).pipelines; pipeline = pipelines.find(p => p.id === 1); }); @@ -26,8 +26,11 @@ describe('Pipelines table in Commits and Merge requests', () => { Vue.http.interceptors.push(pipelinesEmptyResponse); this.component = new PipelinesTable({ - el: document.querySelector('#commit-pipeline-table-view'), - }); + propsData: { + endpoint: 'endpoint', + helpPagePath: 'foo', + }, + }).$mount(); }); afterEach(function () { @@ -58,8 +61,11 @@ describe('Pipelines table in Commits and Merge requests', () => { Vue.http.interceptors.push(pipelinesResponse); this.component = new PipelinesTable({ - el: document.querySelector('#commit-pipeline-table-view'), - }); + propsData: { + endpoint: 'endpoint', + helpPagePath: 'foo', + }, + }).$mount(); }); afterEach(() => { @@ -92,8 +98,11 @@ describe('Pipelines table in Commits and Merge requests', () => { Vue.http.interceptors.push(pipelinesErrorResponse); this.component = new PipelinesTable({ - el: document.querySelector('#commit-pipeline-table-view'), - }); + propsData: { + endpoint: 'endpoint', + helpPagePath: 'foo', + }, + }).$mount(); }); afterEach(function () { diff --git a/spec/javascripts/fixtures/pipelines_table.html.haml b/spec/javascripts/fixtures/pipelines_table.html.haml deleted file mode 100644 index ad1682704bb..00000000000 --- a/spec/javascripts/fixtures/pipelines_table.html.haml +++ /dev/null @@ -1 +0,0 @@ -#commit-pipeline-table-view{ data: { endpoint: "endpoint", "help-page-path": "foo" } } diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js index 886462c4b9a..f3fdbff01a6 100644 --- a/spec/javascripts/issue_show/components/description_spec.js +++ b/spec/javascripts/issue_show/components/description_spec.js @@ -95,6 +95,18 @@ describe('Description component', () => { done(); }); }); + + it('clears task status text when no tasks are present', (done) => { + vm.taskStatus = '0 of 0'; + + setTimeout(() => { + expect( + document.querySelector('.issuable-meta #task_status').textContent.trim(), + ).toBe(''); + + done(); + }); + }); }); it('applies syntax highlighting and math when description changed', (done) => { diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js index 28c9c7ab282..48620898357 100644 --- a/spec/javascripts/pipelines/async_button_spec.js +++ b/spec/javascripts/pipelines/async_button_spec.js @@ -1,25 +1,20 @@ import Vue from 'vue'; import asyncButtonComp from '~/pipelines/components/async_button.vue'; +import eventHub from '~/pipelines/event_hub'; describe('Pipelines Async Button', () => { let component; - let spy; let AsyncButtonComponent; beforeEach(() => { AsyncButtonComponent = Vue.extend(asyncButtonComp); - spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); - component = new AsyncButtonComponent({ propsData: { endpoint: '/foo', title: 'Foo', icon: 'fa fa-foo', cssClass: 'bar', - service: { - postAction: spy, - }, }, }).$mount(); }); @@ -33,7 +28,7 @@ describe('Pipelines Async Button', () => { }); it('should render the provided title', () => { - expect(component.$el.getAttribute('title')).toContain('Foo'); + expect(component.$el.getAttribute('data-original-title')).toContain('Foo'); expect(component.$el.getAttribute('aria-label')).toContain('Foo'); }); @@ -41,37 +36,12 @@ describe('Pipelines Async Button', () => { expect(component.$el.getAttribute('class')).toContain('bar'); }); - it('should call the service when it is clicked with the provided endpoint', () => { - component.$el.click(); - expect(spy).toHaveBeenCalledWith('/foo'); - }); - - it('should hide loading if request fails', () => { - spy = jasmine.createSpy('spy').and.returnValue(Promise.reject()); - - component = new AsyncButtonComponent({ - propsData: { - endpoint: '/foo', - title: 'Foo', - icon: 'fa fa-foo', - cssClass: 'bar', - dataAttributes: { - 'data-foo': 'foo', - }, - service: { - postAction: spy, - }, - }, - }).$mount(); - - component.$el.click(); - expect(component.$el.querySelector('.fa-spinner')).toBe(null); - }); - describe('With confirm dialog', () => { it('should call the service when confimation is positive', () => { spyOn(window, 'confirm').and.returnValue(true); - spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); + eventHub.$on('postAction', (endpoint) => { + expect(endpoint).toEqual('/foo'); + }); component = new AsyncButtonComponent({ propsData: { @@ -79,15 +49,11 @@ describe('Pipelines Async Button', () => { title: 'Foo', icon: 'fa fa-foo', cssClass: 'bar', - service: { - postAction: spy, - }, confirmActionMessage: 'bar', }, }).$mount(); component.$el.click(); - expect(spy).toHaveBeenCalledWith('/foo'); }); }); }); diff --git a/spec/javascripts/pipelines/pipelines_actions_spec.js b/spec/javascripts/pipelines/pipelines_actions_spec.js index 8a58b77f1e3..72fb0a8f9ef 100644 --- a/spec/javascripts/pipelines/pipelines_actions_spec.js +++ b/spec/javascripts/pipelines/pipelines_actions_spec.js @@ -3,7 +3,6 @@ import pipelinesActionsComp from '~/pipelines/components/pipelines_actions.vue'; describe('Pipelines Actions dropdown', () => { let component; - let spy; let actions; let ActionsComponent; @@ -22,14 +21,9 @@ describe('Pipelines Actions dropdown', () => { }, ]; - spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); - component = new ActionsComponent({ propsData: { actions, - service: { - postAction: spy, - }, }, }).$mount(); }); @@ -40,31 +34,6 @@ describe('Pipelines Actions dropdown', () => { ).toEqual(actions.length); }); - it('should call the service when an action is clicked', () => { - component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click(); - component.$el.querySelector('.js-pipeline-action-link').click(); - - expect(spy).toHaveBeenCalledWith(actions[0].path); - }); - - it('should hide loading if request fails', () => { - spy = jasmine.createSpy('spy').and.returnValue(Promise.reject()); - - component = new ActionsComponent({ - propsData: { - actions, - service: { - postAction: spy, - }, - }, - }).$mount(); - - component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click(); - component.$el.querySelector('.js-pipeline-action-link').click(); - - expect(component.$el.querySelector('.fa-spinner')).toEqual(null); - }); - it('should render a disabled action when it\'s not playable', () => { expect( component.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'), diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js index 9475ee28a03..7ce39dca112 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import tableRowComp from '~/vue_shared/components/pipelines_table_row.vue'; +import tableRowComp from '~/pipelines/components/pipelines_table_row.vue'; describe('Pipelines Table Row', () => { const jsonFixtureName = 'pipelines/pipelines.json'; diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js b/spec/javascripts/pipelines/pipelines_table_spec.js index 4c35d702004..3afe89c8db4 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import pipelinesTableComp from '~/vue_shared/components/pipelines_table.vue'; +import pipelinesTableComp from '~/pipelines/components/pipelines_table.vue'; import '~/lib/utils/datetime_utility'; describe('Pipelines Table', () => { @@ -22,7 +22,6 @@ describe('Pipelines Table', () => { component = new PipelinesTableComponent({ propsData: { pipelines: [], - service: {}, }, }).$mount(); }); @@ -48,7 +47,6 @@ describe('Pipelines Table', () => { const component = new PipelinesTableComponent({ propsData: { pipelines: [], - service: {}, }, }).$mount(); expect(component.$el.querySelectorAll('.commit.gl-responsive-table-row').length).toEqual(0); @@ -58,10 +56,8 @@ describe('Pipelines Table', () => { describe('with data', () => { it('should render rows', () => { const component = new PipelinesTableComponent({ - el: document.querySelector('.test-dom-element'), propsData: { pipelines: [pipeline], - service: {}, }, }).$mount(); diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 5a87b906609..58a84cd3fe1 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -15,6 +15,36 @@ describe Gitlab::Metrics do end end + describe '.prometheus_metrics_enabled_unmemoized' do + subject { described_class.send(:prometheus_metrics_enabled_unmemoized) } + + context 'prometheus metrics enabled in config' do + before do + allow(described_class).to receive(:current_application_settings).and_return(prometheus_metrics_enabled: true) + end + + context 'when metrics folder is present' do + before do + allow(described_class).to receive(:metrics_folder_present?).and_return(true) + end + + it 'metrics are enabled' do + expect(subject).to eq(true) + end + end + + context 'when metrics folder is missing' do + before do + allow(described_class).to receive(:metrics_folder_present?).and_return(false) + end + + it 'metrics are disabled' do + expect(subject).to eq(false) + end + end + end + end + describe '.prometheus_metrics_enabled?' do it 'returns a boolean' do expect(described_class.prometheus_metrics_enabled?).to be_in([true, false]) diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb index 7d2599dc703..43b02568cb9 100644 --- a/spec/models/project_services/chat_message/pipeline_message_spec.rb +++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb @@ -62,7 +62,7 @@ describe ChatMessage::PipelineMessage do def build_message(status_text = status, name = user[:name]) "<http://example.gitlab.com|project_name>:" \ " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \ - " of branch `<http://example.gitlab.com/commits/develop|develop>`" \ + " of branch <http://example.gitlab.com/commits/develop|develop>" \ " by #{name} #{status_text} in 02:00:10" end end @@ -81,7 +81,7 @@ describe ChatMessage::PipelineMessage do expect(subject.pretext).to be_empty expect(subject.attachments).to eq(message) expect(subject.activity).to eq({ - title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by hacker passed', + title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by hacker passed', subtitle: 'in [project_name](http://example.gitlab.com)', text: 'in 02:00:10', image: '' @@ -98,7 +98,7 @@ describe ChatMessage::PipelineMessage do expect(subject.pretext).to be_empty expect(subject.attachments).to eq(message) expect(subject.activity).to eq({ - title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by hacker failed', + title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by hacker failed', subtitle: 'in [project_name](http://example.gitlab.com)', text: 'in 02:00:10', image: '' @@ -113,7 +113,7 @@ describe ChatMessage::PipelineMessage do expect(subject.pretext).to be_empty expect(subject.attachments).to eq(message) expect(subject.activity).to eq({ - title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by API failed', + title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by API failed', subtitle: 'in [project_name](http://example.gitlab.com)', text: 'in 02:00:10', image: '' @@ -125,7 +125,7 @@ describe ChatMessage::PipelineMessage do def build_markdown_message(status_text = status, name = user[:name]) "[project_name](http://example.gitlab.com):" \ " Pipeline [#123](http://example.gitlab.com/pipelines/123)" \ - " of branch `[develop](http://example.gitlab.com/commits/develop)`" \ + " of branch [develop](http://example.gitlab.com/commits/develop)" \ " by #{name} #{status_text} in 02:00:10" end end diff --git a/spec/models/project_services/chat_message/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb index e38117b75f6..c794f659c41 100644 --- a/spec/models/project_services/chat_message/push_message_spec.rb +++ b/spec/models/project_services/chat_message/push_message_spec.rb @@ -28,7 +28,7 @@ describe ChatMessage::PushMessage, models: true do context 'without markdown' do it 'returns a message regarding pushes' do expect(subject.pretext).to eq( - 'test.user pushed to branch `<http://url.com/commits/master|master>` of '\ + 'test.user pushed to branch <http://url.com/commits/master|master> of '\ '<http://url.com|project_name> (<http://url.com/compare/before...after|Compare changes>)') expect(subject.attachments).to eq([{ text: "<http://url1.com|abcdefgh>: message1 - author1\n\n"\ @@ -45,7 +45,7 @@ describe ChatMessage::PushMessage, models: true do it 'returns a message regarding pushes' do expect(subject.pretext).to eq( - 'test.user pushed to branch `[master](http://url.com/commits/master)` of [project_name](http://url.com) ([Compare changes](http://url.com/compare/before...after))') + 'test.user pushed to branch [master](http://url.com/commits/master) of [project_name](http://url.com) ([Compare changes](http://url.com/compare/before...after))') expect(subject.attachments).to eq( "[abcdefgh](http://url1.com): message1 - author1\n\n[12345678](http://url2.com): message2 - author2") expect(subject.activity).to eq({ @@ -74,7 +74,7 @@ describe ChatMessage::PushMessage, models: true do context 'without markdown' do it 'returns a message regarding pushes' do expect(subject.pretext).to eq('test.user pushed new tag ' \ - '`<http://url.com/commits/new_tag|new_tag>` to ' \ + '<http://url.com/commits/new_tag|new_tag> to ' \ '<http://url.com|project_name>') expect(subject.attachments).to be_empty end @@ -87,7 +87,7 @@ describe ChatMessage::PushMessage, models: true do it 'returns a message regarding pushes' do expect(subject.pretext).to eq( - 'test.user pushed new tag `[new_tag](http://url.com/commits/new_tag)` to [project_name](http://url.com)') + 'test.user pushed new tag [new_tag](http://url.com/commits/new_tag) to [project_name](http://url.com)') expect(subject.attachments).to be_empty expect(subject.activity).to eq({ title: 'test.user created tag', @@ -107,7 +107,7 @@ describe ChatMessage::PushMessage, models: true do context 'without markdown' do it 'returns a message regarding a new branch' do expect(subject.pretext).to eq( - 'test.user pushed new branch `<http://url.com/commits/master|master>` to '\ + 'test.user pushed new branch <http://url.com/commits/master|master> to '\ '<http://url.com|project_name>') expect(subject.attachments).to be_empty end @@ -120,7 +120,7 @@ describe ChatMessage::PushMessage, models: true do it 'returns a message regarding a new branch' do expect(subject.pretext).to eq( - 'test.user pushed new branch `[master](http://url.com/commits/master)` to [project_name](http://url.com)') + 'test.user pushed new branch [master](http://url.com/commits/master) to [project_name](http://url.com)') expect(subject.attachments).to be_empty expect(subject.activity).to eq({ title: 'test.user created branch', @@ -140,7 +140,7 @@ describe ChatMessage::PushMessage, models: true do context 'without markdown' do it 'returns a message regarding a removed branch' do expect(subject.pretext).to eq( - 'test.user removed branch `master` from <http://url.com|project_name>') + 'test.user removed branch master from <http://url.com|project_name>') expect(subject.attachments).to be_empty end end @@ -152,7 +152,7 @@ describe ChatMessage::PushMessage, models: true do it 'returns a message regarding a removed branch' do expect(subject.pretext).to eq( - 'test.user removed branch `master` from [project_name](http://url.com)') + 'test.user removed branch master from [project_name](http://url.com)') expect(subject.attachments).to be_empty expect(subject.activity).to eq({ title: 'test.user removed branch', diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index bc869ea1108..750682bde52 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -11,7 +11,7 @@ describe API::Users do let(:not_existing_user_id) { (User.maximum('id') || 0 ) + 10 } let(:not_existing_pat_id) { (PersonalAccessToken.maximum('id') || 0 ) + 10 } - describe "GET /users" do + describe 'GET /users' do context "when unauthenticated" do it "returns authentication error" do get api("/users") @@ -76,6 +76,12 @@ describe API::Users do expect(response).to have_http_status(403) end + + it 'does not reveal the `is_admin` flag of the user' do + get api('/users', user) + + expect(json_response.first.keys).not_to include 'is_admin' + end end context "when admin" do @@ -92,6 +98,7 @@ describe API::Users do expect(json_response.first.keys).to include 'two_factor_enabled' expect(json_response.first.keys).to include 'last_sign_in_at' expect(json_response.first.keys).to include 'confirmed_at' + expect(json_response.first.keys).to include 'is_admin' end it "returns an array of external users" do diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb index e9c57f7c6c3..6d7401f9764 100644 --- a/spec/requests/api/v3/users_spec.rb +++ b/spec/requests/api/v3/users_spec.rb @@ -7,6 +7,38 @@ describe API::V3::Users do let(:email) { create(:email, user: user) } let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } + describe 'GET /users' do + context 'when authenticated' do + it 'returns an array of users' do + get v3_api('/users', user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + username = user.username + expect(json_response.detect do |user| + user['username'] == username + end['username']).to eq(username) + end + end + + context 'when authenticated as user' do + it 'does not reveal the `is_admin` flag of the user' do + get v3_api('/users', user) + + expect(json_response.first.keys).not_to include 'is_admin' + end + end + + context 'when authenticated as admin' do + it 'reveals the `is_admin` flag of the user' do + get v3_api('/users', admin) + + expect(json_response.first.keys).to include 'is_admin' + end + end + end + describe 'GET /user/:id/keys' do before { admin } |