diff options
Diffstat (limited to 'app')
293 files changed, 2569 insertions, 1299 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index ce1069276ab..000938e475f 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -11,6 +11,7 @@ const Api = { projectPath: '/api/:version/projects/:id', projectLabelsPath: '/:namespace_path/:project_path/labels', mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid', + mergeRequestsPath: '/api/:version/merge_requests', mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes', mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions', groupLabelsPath: '/groups/:namespace_path/-/labels', @@ -24,8 +25,6 @@ const Api = { commitPipelinesPath: '/:project_id/commit/:sha/pipelines', branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', createBranchPath: '/api/:version/projects/:id/repository/branches', - pipelinesPath: '/api/:version/projects/:id/pipelines', - pipelineJobsPath: '/api/:version/projects/:id/pipelines/:pipeline_id/jobs', group(groupId, callback) { const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); @@ -109,6 +108,12 @@ const Api = { return axios.get(url); }, + mergeRequests(params = {}) { + const url = Api.buildUrl(Api.mergeRequestsPath); + + return axios.get(url, { params }); + }, + mergeRequestChanges(projectPath, mergeRequestId) { const url = Api.buildUrl(Api.mergeRequestChangesPath) .replace(':id', encodeURIComponent(projectPath)) @@ -238,20 +243,6 @@ const Api = { }); }, - pipelines(projectPath, params = {}) { - const url = Api.buildUrl(this.pipelinesPath).replace(':id', encodeURIComponent(projectPath)); - - return axios.get(url, { params }); - }, - - pipelineJobs(projectPath, pipelineId, params = {}) { - const url = Api.buildUrl(this.pipelineJobsPath) - .replace(':id', encodeURIComponent(projectPath)) - .replace(':pipeline_id', pipelineId); - - return axios.get(url, { params }); - }, - buildUrl(url) { let urlRoot = ''; if (gon.relative_url_root != null) { diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index bea818010a4..ac06d79fb60 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -1,7 +1,7 @@ /* eslint-disable comma-dangle, space-before-function-paren, one-var */ import $ from 'jquery'; -import Sortable from 'vendor/Sortable'; +import Sortable from 'sortablejs'; import Vue from 'vue'; import AccessorUtilities from '../../lib/utils/accessor'; import boardList from './board_list.vue'; diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 0d03c1c419c..84a7f277227 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -1,5 +1,5 @@ <script> -import Sortable from 'vendor/Sortable'; +import Sortable from 'sortablejs'; import boardNewIssue from './board_new_issue.vue'; import boardCard from './board_card.vue'; import eventHub from '../eventhub'; diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js index eb8a66975ee..1e5f2383223 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.js +++ b/app/assets/javascripts/boards/components/modal/empty_state.js @@ -41,10 +41,10 @@ gl.issueBoards.ModalEmptyState = Vue.extend({ template: ` <section class="empty-state"> <div class="row"> - <div class="col-xs-12 col-sm-6 order-sm-last"> + <div class="col-12 col-md-6 order-md-last"> <aside class="svg-content"><img :src="emptyStateSvg"/></aside> </div> - <div class="col-xs-12 col-sm-6 order-sm-first"> + <div class="col-12 col-md-6 order-md-first"> <div class="text-content"> <h4>{{ contents.title }}</h4> <p v-html="contents.content"></p> diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js index 6c662432037..f86896d2178 100644 --- a/app/assets/javascripts/boards/components/modal/list.js +++ b/app/assets/javascripts/boards/components/modal/list.js @@ -1,5 +1,3 @@ -/* global ListIssue */ - import Vue from 'vue'; import bp from '../../../breakpoints'; import ModalStore from '../../stores/modal_store'; @@ -56,8 +54,11 @@ gl.issueBoards.ModalList = Vue.extend({ scrollHandler() { const currentPage = Math.floor(this.issues.length / this.perPage); - if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage - && currentPage === this.page) { + if ( + this.scrollTop() > this.scrollHeight() - 100 && + !this.loadingNewPage && + currentPage === this.page + ) { this.loadingNewPage = true; this.page += 1; } diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index 371774098b9..eb335f352d3 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -1,71 +1,69 @@ <script> - /* global ListIssue */ +import $ from 'jquery'; +import _ from 'underscore'; +import eventHub from '../eventhub'; +import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import Api from '../../api'; - import $ from 'jquery'; - import _ from 'underscore'; - import eventHub from '../eventhub'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import Api from '../../api'; - - export default { - name: 'BoardProjectSelect', - components: { - loadingIcon, - }, - props: { - groupId: { - type: Number, - required: true, - default: 0, - }, +export default { + name: 'BoardProjectSelect', + components: { + loadingIcon, + }, + props: { + groupId: { + type: Number, + required: true, + default: 0, }, - data() { - return { - loading: true, - selectedProject: {}, - }; + }, + data() { + return { + loading: true, + selectedProject: {}, + }; + }, + computed: { + selectedProjectName() { + return this.selectedProject.name || 'Select a project'; }, - computed: { - selectedProjectName() { - return this.selectedProject.name || 'Select a project'; + }, + mounted() { + $(this.$refs.projectsDropdown).glDropdown({ + filterable: true, + filterRemote: true, + search: { + fields: ['name_with_namespace'], }, - }, - mounted() { - $(this.$refs.projectsDropdown).glDropdown({ - filterable: true, - filterRemote: true, - search: { - fields: ['name_with_namespace'], - }, - clicked: ({ $el, e }) => { - e.preventDefault(); - this.selectedProject = { - id: $el.data('project-id'), - name: $el.data('project-name'), - }; - eventHub.$emit('setSelectedProject', this.selectedProject); - }, - selectable: true, - data: (term, callback) => { - this.loading = true; - return Api.groupProjects(this.groupId, term, (projects) => { - this.loading = false; - callback(projects); - }); - }, - renderRow(project) { - return ` + clicked: ({ $el, e }) => { + e.preventDefault(); + this.selectedProject = { + id: $el.data('project-id'), + name: $el.data('project-name'), + }; + eventHub.$emit('setSelectedProject', this.selectedProject); + }, + selectable: true, + data: (term, callback) => { + this.loading = true; + return Api.groupProjects(this.groupId, term, projects => { + this.loading = false; + callback(projects); + }); + }, + renderRow(project) { + return ` <li> <a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}"> ${_.escape(project.name)} </a> </li> `; - }, - text: project => project.name, - }); - }, - }; + }, + text: project => project.name, + }); + }, +}; </script> <template> diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 01aec4f36af..e42a3632e79 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -31,6 +31,7 @@ export default class Clusters { installHelmPath, installIngressPath, installRunnerPath, + installJupyterPath, installPrometheusPath, managePrometheusPath, clusterStatus, @@ -51,6 +52,7 @@ export default class Clusters { installIngressEndpoint: installIngressPath, installRunnerEndpoint: installRunnerPath, installPrometheusEndpoint: installPrometheusPath, + installJupyterEndpoint: installJupyterPath, }); this.installApplication = this.installApplication.bind(this); @@ -209,11 +211,12 @@ export default class Clusters { } } - installApplication(appId) { + installApplication(data) { + const appId = data.id; this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING); this.store.updateAppProperty(appId, 'requestReason', null); - this.service.installApplication(appId) + this.service.installApplication(appId, data.params) .then(() => { this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS); }) diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index fae580c091b..30567993322 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -52,6 +52,11 @@ type: String, required: false, }, + installApplicationRequestParams: { + type: Object, + required: false, + default: () => ({}), + }, }, computed: { rowJsClass() { @@ -109,7 +114,10 @@ }, methods: { installClicked() { - eventHub.$emit('installApplication', this.id); + eventHub.$emit('installApplication', { + id: this.id, + params: this.installApplicationRequestParams, + }); }, }, }; diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index bb5fcea648d..9d6be555a2c 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -121,6 +121,12 @@ export default { false, ); }, + jupyterInstalled() { + return this.applications.jupyter.status === APPLICATION_INSTALLED; + }, + jupyterHostname() { + return this.applications.jupyter.hostname; + }, }, }; </script> @@ -278,11 +284,67 @@ export default { applications to production.`) }} </div> </application-row> + <application-row + id="jupyter" + :title="applications.jupyter.title" + title-link="https://jupyterhub.readthedocs.io/en/stable/" + :status="applications.jupyter.status" + :status-reason="applications.jupyter.statusReason" + :request-status="applications.jupyter.requestStatus" + :request-reason="applications.jupyter.requestReason" + :install-application-request-params="{ hostname: applications.jupyter.hostname }" + > + <div slot="description"> + <p> + {{ s__(`ClusterIntegration|JupyterHub, a multi-user Hub, spawns, + manages, and proxies multiple instances of the single-user + Jupyter notebook server. JupyterHub can be used to serve + notebooks to a class of students, a corporate data science group, + or a scientific research group.`) }} + </p> + + <template v-if="ingressExternalIp"> + <div class="form-group"> + <label for="jupyter-hostname"> + {{ s__('ClusterIntegration|Jupyter Hostname') }} + </label> + + <div class="input-group"> + <input + type="text" + class="form-control js-hostname" + v-model="applications.jupyter.hostname" + :readonly="jupyterInstalled" + /> + <span + class="input-group-btn" + > + <clipboard-button + :text="jupyterHostname" + :title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')" + class="js-clipboard-btn" + /> + </span> + </div> + </div> + <p v-if="ingressInstalled"> + {{ s__(`ClusterIntegration|Replace this with your own hostname if you want. + If you do so, point hostname to Ingress IP Address from above.`) }} + <a + :href="ingressDnsHelpPath" + target="_blank" + rel="noopener noreferrer" + > + {{ __('More information') }} + </a> + </p> + </template> + </div> + </application-row> <!-- 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> </section> diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js index b7179f52bb3..371f71fde44 100644 --- a/app/assets/javascripts/clusters/constants.js +++ b/app/assets/javascripts/clusters/constants.js @@ -11,3 +11,4 @@ export const REQUEST_LOADING = 'request-loading'; export const REQUEST_SUCCESS = 'request-success'; export const REQUEST_FAILURE = 'request-failure'; export const INGRESS = 'ingress'; +export const JUPYTER = 'jupyter'; diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js index 13468578f4f..a7d82292ba9 100644 --- a/app/assets/javascripts/clusters/services/clusters_service.js +++ b/app/assets/javascripts/clusters/services/clusters_service.js @@ -8,6 +8,7 @@ export default class ClusterService { ingress: this.options.installIngressEndpoint, runner: this.options.installRunnerEndpoint, prometheus: this.options.installPrometheusEndpoint, + jupyter: this.options.installJupyterEndpoint, }; } @@ -15,8 +16,8 @@ export default class ClusterService { return axios.get(this.options.endpoint); } - installApplication(appId) { - return axios.post(this.appInstallEndpointMap[appId]); + installApplication(appId, params) { + return axios.post(this.appInstallEndpointMap[appId], params); } static updateCluster(endpoint, data) { diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 348bbec3b25..3a4ac09f67c 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -1,5 +1,5 @@ import { s__ } from '../../locale'; -import { INGRESS } from '../constants'; +import { INGRESS, JUPYTER } from '../constants'; export default class ClusterStore { constructor() { @@ -38,6 +38,14 @@ export default class ClusterStore { requestStatus: null, requestReason: null, }, + jupyter: { + title: s__('ClusterIntegration|JupyterHub'), + status: null, + statusReason: null, + requestStatus: null, + requestReason: null, + hostname: null, + }, }, }; } @@ -83,6 +91,12 @@ export default class ClusterStore { if (appId === INGRESS) { this.state.applications.ingress.externalIp = serverAppEntry.external_ip; + } else if (appId === JUPYTER) { + this.state.applications.jupyter.hostname = + serverAppEntry.hostname || + (this.state.applications.ingress.externalIp + ? `jupyter.${this.state.applications.ingress.externalIp}.xip.io` + : ''); } }); } diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue index 1cec84706fc..a4e06bbbe3c 100644 --- a/app/assets/javascripts/ide/components/changed_file_icon.vue +++ b/app/assets/javascripts/ide/components/changed_file_icon.vue @@ -43,7 +43,7 @@ export default { return `${this.changedIcon}-solid`; }, changedIconClass() { - return `multi-${this.changedIcon} pull-left`; + return `multi-${this.changedIcon} float-left`; }, tooltipTitle() { if (!this.showTooltip) return undefined; diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue index 81961fe3c57..e2b42ab2642 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue @@ -126,7 +126,6 @@ export default { </div> <form v-if="!isCompact" - class="form-horizontal" @submit.prevent.stop="commitChanges" ref="formEl" > @@ -144,14 +143,14 @@ export default { <loading-button :loading="submitCommitLoading" :disabled="commitButtonDisabled" - container-class="btn btn-success btn-sm pull-left" + container-class="btn btn-success btn-sm float-left" :label="__('Commit')" @click="commitChanges" /> <button v-if="!discardDraftButtonDisabled" type="button" - class="btn btn-default btn-sm pull-right" + class="btn btn-default btn-sm float-right" @click="discardDraft" > {{ __('Discard draft') }} @@ -159,7 +158,7 @@ export default { <button v-else type="button" - class="btn btn-default btn-sm pull-right" + class="btn btn-default btn-sm float-right" @click="toggleIsSmall" > {{ __('Collapse') }} diff --git a/app/assets/javascripts/ide/components/external_link.vue b/app/assets/javascripts/ide/components/external_link.vue new file mode 100644 index 00000000000..cf3316a8179 --- /dev/null +++ b/app/assets/javascripts/ide/components/external_link.vue @@ -0,0 +1,41 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + }, + props: { + file: { + type: Object, + required: true, + }, + }, + computed: { + showButtons() { + return this.file.permalink; + }, + }, +}; +</script> + +<template> + <div + v-if="showButtons" + class="pull-right ide-btn-group" + > + <a + :href="file.permalink" + target="_blank" + :title="s__('IDE|Open in file view')" + rel="noopener noreferrer" + > + <span class="vertical-align-middle">Open in file view</span> + <icon + name="external-link" + css-classes="vertical-align-middle space-right" + :size="16" + /> + </a> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 2c184ea726c..f5f832521c5 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -6,6 +6,7 @@ import RepoTabs from './repo_tabs.vue'; import IdeStatusBar from './ide_status_bar.vue'; import RepoEditor from './repo_editor.vue'; import FindFile from './file_finder/index.vue'; +import RightPane from './panes/right.vue'; const originalStopCallback = Mousetrap.stopCallback; @@ -16,6 +17,7 @@ export default { IdeStatusBar, RepoEditor, FindFile, + RightPane, }, computed: { ...mapState([ @@ -25,6 +27,7 @@ export default { 'currentMergeRequestId', 'fileFindVisible', 'emptyStateSvgPath', + 'currentProjectId', ]), ...mapGetters(['activeFile', 'hasChanges']), }, @@ -122,6 +125,9 @@ export default { </div> </template> </div> + <right-pane + v-if="currentProjectId" + /> </div> <ide-status-bar :file="activeFile"/> </article> diff --git a/app/assets/javascripts/ide/components/ide_file_buttons.vue b/app/assets/javascripts/ide/components/ide_file_buttons.vue deleted file mode 100644 index 30b00abf6ed..00000000000 --- a/app/assets/javascripts/ide/components/ide_file_buttons.vue +++ /dev/null @@ -1,84 +0,0 @@ -<script> -import { __ } from '~/locale'; -import tooltip from '~/vue_shared/directives/tooltip'; -import Icon from '~/vue_shared/components/icon.vue'; - -export default { - components: { - Icon, - }, - directives: { - tooltip, - }, - props: { - file: { - type: Object, - required: true, - }, - }, - computed: { - showButtons() { - return ( - this.file.rawPath || this.file.blamePath || this.file.commitsPath || this.file.permalink - ); - }, - rawDownloadButtonLabel() { - return this.file.binary ? __('Download') : __('Raw'); - }, - }, -}; -</script> - -<template> - <div - v-if="showButtons" - class="float-right ide-btn-group" - > - <a - v-tooltip - v-if="!file.binary" - :href="file.blamePath" - :title="__('Blame')" - class="btn btn-sm btn-transparent blame" - > - <icon - name="blame" - :size="16" - /> - </a> - <a - v-tooltip - :href="file.commitsPath" - :title="__('History')" - class="btn btn-sm btn-transparent history" - > - <icon - name="history" - :size="16" - /> - </a> - <a - v-tooltip - :href="file.permalink" - :title="__('Permalink')" - class="btn btn-sm btn-transparent permalink" - > - <icon - name="link" - :size="16" - /> - </a> - <a - v-tooltip - :href="file.rawPath" - target="_blank" - class="btn btn-sm btn-transparent prepend-left-10 raw" - rel="noopener noreferrer" - :title="rawDownloadButtonLabel"> - <icon - name="download" - :size="16" - /> - </a> - </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 6f60cfbf184..368a2995ed9 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -31,6 +31,7 @@ export default { computed: { ...mapState(['currentBranchId', 'currentProjectId']), ...mapGetters(['currentProject', 'lastCommit']), + ...mapState('pipelines', ['latestPipeline']), }, watch: { lastCommit() { @@ -51,14 +52,14 @@ export default { } }, methods: { - ...mapActions(['pipelinePoll', 'stopPipelinePolling']), + ...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']), startTimer() { this.intervalId = setInterval(() => { this.commitAgeUpdate(); }, 1000); }, initPipelinePolling() { - this.pipelinePoll(); + this.fetchLatestPipeline(); this.isPollingInitialized = true; }, commitAgeUpdate() { @@ -81,18 +82,18 @@ export default { > <span class="ide-status-pipeline" - v-if="lastCommit.pipeline && lastCommit.pipeline.details" + v-if="latestPipeline && latestPipeline.details" > <ci-icon - :status="lastCommit.pipeline.details.status" + :status="latestPipeline.details.status" v-tooltip - :title="lastCommit.pipeline.details.status.text" + :title="latestPipeline.details.status.text" /> Pipeline <a class="monospace" - :href="lastCommit.pipeline.details.status.details_path">#{{ lastCommit.pipeline.id }}</a> - {{ lastCommit.pipeline.details.status.text }} + :href="latestPipeline.details.status.details_path">#{{ latestPipeline.id }}</a> + {{ latestPipeline.details.status.text }} for </span> diff --git a/app/assets/javascripts/ide/components/jobs/item.vue b/app/assets/javascripts/ide/components/jobs/item.vue new file mode 100644 index 00000000000..c33936021d4 --- /dev/null +++ b/app/assets/javascripts/ide/components/jobs/item.vue @@ -0,0 +1,46 @@ +<script> +import Icon from '../../../vue_shared/components/icon.vue'; +import CiIcon from '../../../vue_shared/components/ci_icon.vue'; + +export default { + components: { + Icon, + CiIcon, + }, + props: { + job: { + type: Object, + required: true, + }, + }, + computed: { + jobId() { + return `#${this.job.id}`; + }, + }, +}; +</script> + +<template> + <div class="ide-job-item"> + <ci-icon + :status="job.status" + :borderless="true" + :size="24" + /> + <span class="prepend-left-8"> + {{ job.name }} + <a + :href="job.path" + target="_blank" + class="ide-external-link" + > + {{ jobId }} + <icon + name="external-link" + :size="12" + /> + </a> + </span> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue new file mode 100644 index 00000000000..bdd0364c9b9 --- /dev/null +++ b/app/assets/javascripts/ide/components/jobs/list.vue @@ -0,0 +1,44 @@ +<script> +import { mapActions } from 'vuex'; +import LoadingIcon from '../../../vue_shared/components/loading_icon.vue'; +import Stage from './stage.vue'; + +export default { + components: { + LoadingIcon, + Stage, + }, + props: { + stages: { + type: Array, + required: true, + }, + loading: { + type: Boolean, + required: true, + }, + }, + methods: { + ...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed']), + }, +}; +</script> + +<template> + <div> + <loading-icon + v-if="loading && !stages.length" + class="prepend-top-default" + size="2" + /> + <template v-else> + <stage + v-for="stage in stages" + :key="stage.id" + :stage="stage" + @fetch="fetchJobs" + @toggleCollapsed="toggleStageCollapsed" + /> + </template> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue new file mode 100644 index 00000000000..5b24bb1f5a7 --- /dev/null +++ b/app/assets/javascripts/ide/components/jobs/stage.vue @@ -0,0 +1,108 @@ +<script> +import tooltip from '../../../vue_shared/directives/tooltip'; +import Icon from '../../../vue_shared/components/icon.vue'; +import CiIcon from '../../../vue_shared/components/ci_icon.vue'; +import LoadingIcon from '../../../vue_shared/components/loading_icon.vue'; +import Item from './item.vue'; + +export default { + directives: { + tooltip, + }, + components: { + Icon, + CiIcon, + LoadingIcon, + Item, + }, + props: { + stage: { + type: Object, + required: true, + }, + }, + data() { + return { + showTooltip: false, + }; + }, + computed: { + collapseIcon() { + return this.stage.isCollapsed ? 'angle-left' : 'angle-down'; + }, + showLoadingIcon() { + return this.stage.isLoading && !this.stage.jobs.length; + }, + jobsCount() { + return this.stage.jobs.length; + }, + }, + mounted() { + const { stageTitle } = this.$refs; + + this.showTooltip = stageTitle.scrollWidth > stageTitle.offsetWidth; + + this.$emit('fetch', this.stage); + }, + methods: { + toggleCollapsed() { + this.$emit('toggleCollapsed', this.stage.id); + }, + }, +}; +</script> + +<template> + <div + class="ide-stage card prepend-top-default" + > + <div + class="card-header" + :class="{ + 'border-bottom-0': stage.isCollapsed + }" + @click="toggleCollapsed" + > + <ci-icon + :status="stage.status" + :size="24" + /> + <strong + v-tooltip="showTooltip" + :title="showTooltip ? stage.name : null" + data-container="body" + class="prepend-left-8 ide-stage-title" + ref="stageTitle" + > + {{ stage.name }} + </strong> + <div + v-if="!stage.isLoading || stage.jobs.length" + class="append-right-8 prepend-left-4" + > + <span class="badge badge-pill"> + {{ jobsCount }} + </span> + </div> + <icon + :name="collapseIcon" + css-classes="ide-stage-collapse-icon" + /> + </div> + <div + class="card-body" + v-show="!stage.isCollapsed" + > + <loading-icon + v-if="showLoadingIcon" + /> + <template v-else> + <item + v-for="job in stage.jobs" + :key="job.id" + :job="job" + /> + </template> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue new file mode 100644 index 00000000000..703c4a70cfa --- /dev/null +++ b/app/assets/javascripts/ide/components/panes/right.vue @@ -0,0 +1,65 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import tooltip from '../../../vue_shared/directives/tooltip'; +import Icon from '../../../vue_shared/components/icon.vue'; +import { rightSidebarViews } from '../../constants'; +import PipelinesList from '../pipelines/list.vue'; + +export default { + directives: { + tooltip, + }, + components: { + Icon, + PipelinesList, + }, + computed: { + ...mapState(['rightPane']), + }, + methods: { + ...mapActions(['setRightPane']), + clickTab(e, view) { + e.target.blur(); + + this.setRightPane(view); + }, + }, + rightSidebarViews, +}; +</script> + +<template> + <div + class="multi-file-commit-panel ide-right-sidebar" + > + <div + class="multi-file-commit-panel-inner" + v-if="rightPane" + > + <component :is="rightPane" /> + </div> + <nav class="ide-activity-bar"> + <ul class="list-unstyled"> + <li> + <button + v-tooltip + data-container="body" + data-placement="left" + :title="__('Pipelines')" + class="ide-sidebar-link is-right" + :class="{ + active: rightPane === $options.rightSidebarViews.pipelines + }" + type="button" + @click="clickTab($event, $options.rightSidebarViews.pipelines)" + > + <icon + :size="16" + name="pipeline" + /> + </button> + </li> + </ul> + </nav> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue new file mode 100644 index 00000000000..06455fac439 --- /dev/null +++ b/app/assets/javascripts/ide/components/pipelines/list.vue @@ -0,0 +1,146 @@ +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import _ from 'underscore'; +import { sprintf, __ } from '../../../locale'; +import LoadingIcon from '../../../vue_shared/components/loading_icon.vue'; +import Icon from '../../../vue_shared/components/icon.vue'; +import CiIcon from '../../../vue_shared/components/ci_icon.vue'; +import Tabs from '../../../vue_shared/components/tabs/tabs'; +import Tab from '../../../vue_shared/components/tabs/tab.vue'; +import EmptyState from '../../../pipelines/components/empty_state.vue'; +import JobsList from '../jobs/list.vue'; + +export default { + components: { + LoadingIcon, + Icon, + CiIcon, + Tabs, + Tab, + JobsList, + EmptyState, + }, + computed: { + ...mapState(['pipelinesEmptyStateSvgPath', 'links']), + ...mapGetters(['currentProject']), + ...mapGetters('pipelines', ['jobsCount', 'failedJobsCount', 'failedStages', 'pipelineFailed']), + ...mapState('pipelines', ['isLoadingPipeline', 'latestPipeline', 'stages', 'isLoadingJobs']), + ciLintText() { + return sprintf( + __('You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}'), + { + linkStart: `<a href="${_.escape(this.currentProject.web_url)}/-/ci/lint">`, + linkEnd: '</a>', + }, + false, + ); + }, + showLoadingIcon() { + return this.isLoadingPipeline && this.latestPipeline === null; + }, + }, + created() { + this.fetchLatestPipeline(); + }, + methods: { + ...mapActions('pipelines', ['fetchLatestPipeline']), + }, +}; +</script> + +<template> + <div class="ide-pipeline"> + <loading-icon + v-if="showLoadingIcon" + class="prepend-top-default" + size="2" + /> + <template v-else-if="latestPipeline !== null"> + <header + v-if="latestPipeline" + class="ide-tree-header ide-pipeline-header" + > + <ci-icon + :status="latestPipeline.details.status" + :size="24" + /> + <span class="prepend-left-8"> + <strong> + {{ __('Pipeline') }} + </strong> + <a + :href="latestPipeline.path" + target="_blank" + class="ide-external-link" + > + #{{ latestPipeline.id }} + <icon + name="external-link" + :size="12" + /> + </a> + </span> + </header> + <empty-state + v-if="latestPipeline === false" + :help-page-path="links.ciHelpPagePath" + :empty-state-svg-path="pipelinesEmptyStateSvgPath" + :can-set-ci="true" + /> + <div + v-else-if="latestPipeline.yamlError" + class="bs-callout bs-callout-danger" + > + <p class="append-bottom-0"> + {{ __('Found errors in your .gitlab-ci.yml:') }} + </p> + <p class="append-bottom-0"> + {{ latestPipeline.yamlError }} + </p> + <p + class="append-bottom-0" + v-html="ciLintText" + ></p> + </div> + <tabs + v-else + class="ide-pipeline-list" + > + <tab + :active="!pipelineFailed" + > + <template slot="title"> + {{ __('Jobs') }} + <span + v-if="jobsCount" + class="badge badge-pill" + > + {{ jobsCount }} + </span> + </template> + <jobs-list + :loading="isLoadingJobs" + :stages="stages" + /> + </tab> + <tab + :active="pipelineFailed" + > + <template slot="title"> + {{ __('Failed Jobs') }} + <span + v-if="failedJobsCount" + class="badge badge-pill" + > + {{ failedJobsCount }} + </span> + </template> + <jobs-list + :loading="isLoadingJobs" + :stages="failedStages" + /> + </tab> + </tabs> + </template> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index a281ecb95c8..93453989c08 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -6,12 +6,12 @@ import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer import { activityBarViews, viewerTypes } from '../constants'; import monacoLoader from '../monaco_loader'; import Editor from '../lib/editor'; -import IdeFileButtons from './ide_file_buttons.vue'; +import ExternalLink from './external_link.vue'; export default { components: { ContentViewer, - IdeFileButtons, + ExternalLink, }, props: { file: { @@ -224,7 +224,7 @@ export default { </a> </li> </ul> - <ide-file-buttons + <external-link :file="file" /> </div> diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index 442697e1c80..f56aeced806 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -169,7 +169,7 @@ export default { :show-tooltip="true" :show-staged-icon="true" :force-modified-icon="true" - class="pull-right" + class="float-right" /> </span> <new-dropdown diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 83fe22f40a4..33cd20caf52 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -20,3 +20,7 @@ export const viewerTypes = { edit: 'editor', diff: 'diff', }; + +export const rightSidebarViews = { + pipelines: 'pipelines-list', +}; diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index a21cec4e8d8..b52618f4fde 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -63,7 +63,7 @@ router.beforeEach((to, from, next) => { .then(() => { const fullProjectId = `${to.params.namespace}/${to.params.project}`; - const baseSplit = to.params[0].split('/-/'); + const baseSplit = (to.params[0] && to.params[0].split('/-/')) || ['']; const branchId = baseSplit[0].slice(-1) === '/' ? baseSplit[0].slice(0, -1) : baseSplit[0]; if (branchId) { diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index c5835cd3b06..2d74192e6b3 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import { mapActions } from 'vuex'; import Translate from '~/vue_shared/translate'; import ide from './components/ide.vue'; import store from './stores'; @@ -17,11 +18,18 @@ export function initIde(el) { ide, }, created() { - this.$store.dispatch('setEmptyStateSvgs', { + this.setEmptyStateSvgs({ emptyStateSvgPath: el.dataset.emptyStateSvgPath, noChangesStateSvgPath: el.dataset.noChangesStateSvgPath, committedStateSvgPath: el.dataset.committedStateSvgPath, + pipelinesEmptyStateSvgPath: el.dataset.pipelinesEmptyStateSvgPath, }); + this.setLinks({ + ciHelpPagePath: el.dataset.ciHelpPagePath, + }); + }, + methods: { + ...mapActions(['setEmptyStateSvgs', 'setLinks']), }, render(createElement) { return createElement('ide'); diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js index b1e43a1e38c..e5149b1f3ad 100644 --- a/app/assets/javascripts/ide/lib/common/model.js +++ b/app/assets/javascripts/ide/lib/common/model.js @@ -1,4 +1,3 @@ -/* global monaco */ import Disposable from './disposable'; import eventHub from '../../eventhub'; diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index 1a98b42761e..3dc365eaead 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -169,6 +169,12 @@ export const burstUnusedSeal = ({ state, commit }) => { } }; +export const setRightPane = ({ commit }, view) => { + commit(types.SET_RIGHT_PANE, view); +}; + +export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links); + export * from './actions/tree'; export * from './actions/file'; export * from './actions/project'; diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 13aea91d8ba..74f9c112f5a 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -84,11 +84,11 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive }); }; -export const setFileMrChange = ({ state, commit }, { file, mrChange }) => { +export const setFileMrChange = ({ commit }, { file, mrChange }) => { commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange }); }; -export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => { +export const getRawFileData = ({ state, commit }, { path, baseSha }) => { const file = state.entries[path]; return new Promise((resolve, reject) => { service @@ -156,7 +156,7 @@ export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn } }; -export const setFileViewMode = ({ state, commit }, { file, viewMode }) => { +export const setFileViewMode = ({ commit }, { file, viewMode }) => { commit(types.SET_FILE_VIEWMODE, { file, viewMode }); }; diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js index da73034fd7d..5ec9bd661bb 100644 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ b/app/assets/javascripts/ide/stores/actions/merge_request.js @@ -3,7 +3,7 @@ import service from '../../services'; import * as types from '../mutation_types'; export const getMergeRequestData = ( - { commit, state, dispatch }, + { commit, state }, { projectId, mergeRequestId, force = false } = {}, ) => new Promise((resolve, reject) => { @@ -32,7 +32,7 @@ export const getMergeRequestData = ( }); export const getMergeRequestChanges = ( - { commit, state, dispatch }, + { commit, state }, { projectId, mergeRequestId, force = false } = {}, ) => new Promise((resolve, reject) => { @@ -58,7 +58,7 @@ export const getMergeRequestChanges = ( }); export const getMergeRequestVersions = ( - { commit, state, dispatch }, + { commit, state }, { projectId, mergeRequestId, force = false } = {}, ) => new Promise((resolve, reject) => { diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index cece9154c82..46af47d2f81 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -1,16 +1,9 @@ -import Visibility from 'visibilityjs'; import flash from '~/flash'; import { __ } from '~/locale'; import service from '../../services'; import * as types from '../mutation_types'; -import Poll from '../../../lib/utils/poll'; -let eTagPoll; - -export const getProjectData = ( - { commit, state, dispatch }, - { namespace, projectId, force = false } = {}, -) => +export const getProjectData = ({ commit, state }, { namespace, projectId, force = false } = {}) => new Promise((resolve, reject) => { if (!state.projects[`${namespace}/${projectId}`] || force) { commit(types.TOGGLE_LOADING, { entry: state }); @@ -40,10 +33,7 @@ export const getProjectData = ( } }); -export const getBranchData = ( - { commit, state, dispatch }, - { projectId, branchId, force = false } = {}, -) => +export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) => new Promise((resolve, reject) => { if ( typeof state.projects[`${projectId}`] === 'undefined' || @@ -78,7 +68,7 @@ export const getBranchData = ( } }); -export const refreshLastCommitData = ({ commit, state, dispatch }, { projectId, branchId } = {}) => +export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) => service .getBranchData(projectId, branchId) .then(({ data }) => { @@ -91,61 +81,3 @@ export const refreshLastCommitData = ({ commit, state, dispatch }, { projectId, .catch(() => { flash(__('Error loading last commit.'), 'alert', document, null, false, true); }); - -export const pollSuccessCallBack = ({ commit, state, dispatch }, { data }) => { - if (data.pipelines && data.pipelines.length) { - const lastCommitHash = - state.projects[state.currentProjectId].branches[state.currentBranchId].commit.id; - const lastCommitPipeline = data.pipelines.find( - pipeline => pipeline.commit.id === lastCommitHash, - ); - commit(types.SET_LAST_COMMIT_PIPELINE, { - projectId: state.currentProjectId, - branchId: state.currentBranchId, - pipeline: lastCommitPipeline || {}, - }); - } - - return data; -}; - -export const pipelinePoll = ({ getters, dispatch }) => { - eTagPoll = new Poll({ - resource: service, - method: 'lastCommitPipelines', - data: { - getters, - }, - successCallback: ({ data }) => dispatch('pollSuccessCallBack', { data }), - errorCallback: () => { - flash( - __('Something went wrong while fetching the latest pipeline status.'), - 'alert', - document, - null, - false, - true, - ); - }, - }); - - if (!Visibility.hidden()) { - eTagPoll.makeRequest(); - } - - Visibility.change(() => { - if (!Visibility.hidden()) { - eTagPoll.restart(); - } else { - eTagPoll.stop(); - } - }); -}; - -export const stopPipelinePolling = () => { - eTagPoll.stop(); -}; - -export const restartPipelinePolling = () => { - eTagPoll.restart(); -}; diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js index 6536be04f0a..cc5116413f7 100644 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -5,7 +5,7 @@ import * as types from '../mutation_types'; import { findEntry } from '../utils'; import FilesDecoratorWorker from '../workers/files_decorator_worker'; -export const toggleTreeOpen = ({ commit, dispatch }, path) => { +export const toggleTreeOpen = ({ commit }, path) => { commit(types.TOGGLE_TREE_OPEN, path); }; @@ -23,7 +23,7 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => { } }; -export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => { +export const getLastCommitData = ({ state, commit, dispatch }, tree = state) => { if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return; service @@ -49,7 +49,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true)); }; -export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) => +export const getFiles = ({ state, commit }, { projectId, branchId } = {}) => new Promise((resolve, reject) => { if (!state.trees[`${projectId}/${branchId}`]) { const selectedProject = state.projects[projectId]; diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js index 699710055e3..f8ce8a67ec0 100644 --- a/app/assets/javascripts/ide/stores/index.js +++ b/app/assets/javascripts/ide/stores/index.js @@ -6,16 +6,21 @@ import * as getters from './getters'; import mutations from './mutations'; import commitModule from './modules/commit'; import pipelines from './modules/pipelines'; +import mergeRequests from './modules/merge_requests'; Vue.use(Vuex); -export default new Vuex.Store({ - state: state(), - actions, - mutations, - getters, - modules: { - commit: commitModule, - pipelines, - }, -}); +export const createStore = () => + new Vuex.Store({ + state: state(), + actions, + mutations, + getters, + modules: { + commit: commitModule, + pipelines, + mergeRequests, + }, + }); + +export default createStore(); diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index cd25c3060f2..0a0db4033c8 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -31,9 +31,9 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => { const currentProject = rootState.projects[rootState.currentProjectId]; const commitStats = data.stats ? sprintf(__('with %{additions} additions, %{deletions} deletions.'), { - additions: data.stats.additions, // eslint-disable-line indent - deletions: data.stats.deletions, // eslint-disable-line indent - }) // eslint-disable-line indent + additions: data.stats.additions, // eslint-disable-line indent-legacy + deletions: data.stats.deletions, // eslint-disable-line indent-legacy + }) // eslint-disable-line indent-legacy : ''; const commitMsg = sprintf( __('Your changes have been committed. Commit %{commitId} %{commitStats}'), @@ -74,10 +74,7 @@ export const checkCommitStatus = ({ rootState }) => ), ); -export const updateFilesAfterCommit = ( - { commit, dispatch, state, rootState, rootGetters }, - { data }, -) => { +export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data }) => { const selectedProject = rootState.projects[rootState.currentProjectId]; const lastCommit = { commit_path: `${selectedProject.web_url}/commit/${data.id}`, diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js new file mode 100644 index 00000000000..d3050183bd3 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js @@ -0,0 +1,25 @@ +import { __ } from '../../../../locale'; +import Api from '../../../../api'; +import flash from '../../../../flash'; +import * as types from './mutation_types'; + +export const requestMergeRequests = ({ commit }) => commit(types.REQUEST_MERGE_REQUESTS); +export const receiveMergeRequestsError = ({ commit }) => { + flash(__('Error loading merge requests.')); + commit(types.RECEIVE_MERGE_REQUESTS_ERROR); +}; +export const receiveMergeRequestsSuccess = ({ commit }, data) => + commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, data); + +export const fetchMergeRequests = ({ dispatch, state: { scope, state } }, search = '') => { + dispatch('requestMergeRequests'); + dispatch('resetMergeRequests'); + + Api.mergeRequests({ scope, state, search }) + .then(({ data }) => dispatch('receiveMergeRequestsSuccess', data)) + .catch(() => dispatch('receiveMergeRequestsError')); +}; + +export const resetMergeRequests = ({ commit }) => commit(types.RESET_MERGE_REQUESTS); + +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js b/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js new file mode 100644 index 00000000000..64b7763f257 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js @@ -0,0 +1,10 @@ +export const scopes = { + assignedToMe: 'assigned-to-me', + createdByMe: 'created-by-me', +}; + +export const states = { + opened: 'opened', + closed: 'closed', + merged: 'merged', +}; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js new file mode 100644 index 00000000000..04e7e0f08f1 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js @@ -0,0 +1,10 @@ +import state from './state'; +import * as actions from './actions'; +import mutations from './mutations'; + +export default { + namespaced: true, + state: state(), + actions, + mutations, +}; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutation_types.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutation_types.js new file mode 100644 index 00000000000..0badddcbae7 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutation_types.js @@ -0,0 +1,5 @@ +export const REQUEST_MERGE_REQUESTS = 'REQUEST_MERGE_REQUESTS'; +export const RECEIVE_MERGE_REQUESTS_ERROR = 'RECEIVE_MERGE_REQUESTS_ERROR'; +export const RECEIVE_MERGE_REQUESTS_SUCCESS = 'RECEIVE_MERGE_REQUESTS_SUCCESS'; + +export const RESET_MERGE_REQUESTS = 'RESET_MERGE_REQUESTS'; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js new file mode 100644 index 00000000000..98102a68e08 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js @@ -0,0 +1,26 @@ +/* eslint-disable no-param-reassign */ +import * as types from './mutation_types'; + +export default { + [types.REQUEST_MERGE_REQUESTS](state) { + state.isLoading = true; + }, + [types.RECEIVE_MERGE_REQUESTS_ERROR](state) { + state.isLoading = false; + }, + [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, data) { + state.isLoading = false; + state.mergeRequests = data.map(mergeRequest => ({ + id: mergeRequest.id, + iid: mergeRequest.iid, + title: mergeRequest.title, + projectId: mergeRequest.project_id, + projectPathWithNamespace: mergeRequest.web_url + .replace(`${gon.gitlab_url}/`, '') + .replace(`/merge_requests/${mergeRequest.iid}`, ''), + })); + }, + [types.RESET_MERGE_REQUESTS](state) { + state.mergeRequests = []; + }, +}; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js new file mode 100644 index 00000000000..2947b686c1c --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js @@ -0,0 +1,8 @@ +import { scopes, states } from './constants'; + +export default () => ({ + isLoading: false, + mergeRequests: [], + scope: scopes.assignedToMe, + state: states.opened, +}); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js index 07f7b201f2e..1ebe487263b 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js @@ -1,49 +1,80 @@ +import Visibility from 'visibilityjs'; +import axios from 'axios'; import { __ } from '../../../../locale'; -import Api from '../../../../api'; import flash from '../../../../flash'; +import Poll from '../../../../lib/utils/poll'; +import service from '../../../services'; import * as types from './mutation_types'; +let eTagPoll; + +export const clearEtagPoll = () => { + eTagPoll = null; +}; +export const stopPipelinePolling = () => eTagPoll && eTagPoll.stop(); +export const restartPipelinePolling = () => eTagPoll && eTagPoll.restart(); + export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE); -export const receiveLatestPipelineError = ({ commit }) => { +export const receiveLatestPipelineError = ({ commit, dispatch }) => { flash(__('There was an error loading latest pipeline')); commit(types.RECEIVE_LASTEST_PIPELINE_ERROR); + dispatch('stopPipelinePolling'); +}; +export const receiveLatestPipelineSuccess = ({ rootGetters, commit }, { pipelines }) => { + let lastCommitPipeline = false; + + if (pipelines && pipelines.length) { + const lastCommitHash = rootGetters.lastCommit && rootGetters.lastCommit.id; + lastCommitPipeline = pipelines.find(pipeline => pipeline.commit.id === lastCommitHash); + } + + commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, lastCommitPipeline); }; -export const receiveLatestPipelineSuccess = ({ commit }, pipeline) => - commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, pipeline); -export const fetchLatestPipeline = ({ dispatch, rootState }, sha) => { +export const fetchLatestPipeline = ({ dispatch, rootGetters }) => { + if (eTagPoll) return; + dispatch('requestLatestPipeline'); - return Api.pipelines(rootState.currentProjectId, { sha, per_page: '1' }) - .then(({ data }) => { - dispatch('receiveLatestPipelineSuccess', data.pop()); - }) - .catch(() => dispatch('receiveLatestPipelineError')); + eTagPoll = new Poll({ + resource: service, + method: 'lastCommitPipelines', + data: { getters: rootGetters }, + successCallback: ({ data }) => dispatch('receiveLatestPipelineSuccess', data), + errorCallback: () => dispatch('receiveLatestPipelineError'), + }); + + if (!Visibility.hidden()) { + eTagPoll.makeRequest(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + eTagPoll.restart(); + } else { + eTagPoll.stop(); + } + }); }; -export const requestJobs = ({ commit }) => commit(types.REQUEST_JOBS); -export const receiveJobsError = ({ commit }) => { +export const requestJobs = ({ commit }, id) => commit(types.REQUEST_JOBS, id); +export const receiveJobsError = ({ commit }, id) => { flash(__('There was an error loading jobs')); - commit(types.RECEIVE_JOBS_ERROR); + commit(types.RECEIVE_JOBS_ERROR, id); }; -export const receiveJobsSuccess = ({ commit }, data) => commit(types.RECEIVE_JOBS_SUCCESS, data); +export const receiveJobsSuccess = ({ commit }, { id, data }) => + commit(types.RECEIVE_JOBS_SUCCESS, { id, data }); -export const fetchJobs = ({ dispatch, state, rootState }, page = '1') => { - dispatch('requestJobs'); +export const fetchJobs = ({ dispatch }, stage) => { + dispatch('requestJobs', stage.id); - Api.pipelineJobs(rootState.currentProjectId, state.latestPipeline.id, { - page, - }) - .then(({ data, headers }) => { - const nextPage = headers && headers['x-next-page']; - - dispatch('receiveJobsSuccess', data); - - if (nextPage) { - dispatch('fetchJobs', nextPage); - } - }) - .catch(() => dispatch('receiveJobsError')); + axios + .get(stage.dropdownPath) + .then(({ data }) => dispatch('receiveJobsSuccess', { id: stage.id, data })) + .catch(() => dispatch('receiveJobsError', stage.id)); }; +export const toggleStageCollapsed = ({ commit }, stageId) => + commit(types.TOGGLE_STAGE_COLLAPSE, stageId); + export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/constants.js b/app/assets/javascripts/ide/stores/modules/pipelines/constants.js new file mode 100644 index 00000000000..f5b96327e40 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/constants.js @@ -0,0 +1,4 @@ +// eslint-disable-next-line import/prefer-default-export +export const states = { + failed: 'failed', +}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js index d6c91f5b64d..f545453806f 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js @@ -1,7 +1,22 @@ +import { states } from './constants'; + export const hasLatestPipeline = state => !state.isLoadingPipeline && !!state.latestPipeline; -export const failedJobs = state => +export const pipelineFailed = state => + state.latestPipeline && state.latestPipeline.details.status.text === states.failed; + +export const failedStages = state => + state.stages.filter(stage => stage.status.text.toLowerCase() === states.failed).map(stage => ({ + ...stage, + jobs: stage.jobs.filter(job => job.status.text.toLowerCase() === states.failed), + })); + +export const failedJobsCount = state => state.stages.reduce( - (acc, stage) => acc.concat(stage.jobs.filter(job => job.status === 'failed')), - [], + (acc, stage) => acc + stage.jobs.filter(j => j.status.text === states.failed).length, + 0, ); + +export const jobsCount = state => state.stages.reduce((acc, stage) => acc + stage.jobs.length, 0); + +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js index 6b5701670a6..3ddc8409c5b 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js @@ -5,3 +5,5 @@ export const RECEIVE_LASTEST_PIPELINE_SUCCESS = 'RECEIVE_LASTEST_PIPELINE_SUCCES export const REQUEST_JOBS = 'REQUEST_JOBS'; export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR'; export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS'; + +export const TOGGLE_STAGE_COLLAPSE = 'TOGGLE_STAGE_COLLAPSE'; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js index 2b16e57b386..745797e1ee5 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ import * as types from './mutation_types'; +import { normalizeJob } from './utils'; export default { [types.REQUEST_LATEST_PIPELINE](state) { @@ -14,40 +15,52 @@ export default { if (pipeline) { state.latestPipeline = { id: pipeline.id, - status: pipeline.status, + path: pipeline.path, + commit: pipeline.commit, + details: { + status: pipeline.details.status, + }, + yamlError: pipeline.yaml_errors, }; + state.stages = pipeline.details.stages.map((stage, i) => { + const foundStage = state.stages.find(s => s.id === i); + return { + id: i, + dropdownPath: stage.dropdown_path, + name: stage.name, + status: stage.status, + isCollapsed: foundStage ? foundStage.isCollapsed : false, + isLoading: foundStage ? foundStage.isLoading : false, + jobs: foundStage ? foundStage.jobs : [], + }; + }); + } else { + state.latestPipeline = false; } }, - [types.REQUEST_JOBS](state) { - state.isLoadingJobs = true; + [types.REQUEST_JOBS](state, id) { + state.stages = state.stages.map(stage => ({ + ...stage, + isLoading: stage.id === id ? true : stage.isLoading, + })); }, - [types.RECEIVE_JOBS_ERROR](state) { - state.isLoadingJobs = false; + [types.RECEIVE_JOBS_ERROR](state, id) { + state.stages = state.stages.map(stage => ({ + ...stage, + isLoading: stage.id === id ? false : stage.isLoading, + })); }, - [types.RECEIVE_JOBS_SUCCESS](state, jobs) { - state.isLoadingJobs = false; - - state.stages = jobs.reduce((acc, job) => { - let stage = acc.find(s => s.title === job.stage); - - if (!stage) { - stage = { - title: job.stage, - jobs: [], - }; - - acc.push(stage); - } - - stage.jobs = stage.jobs.concat({ - id: job.id, - name: job.name, - status: job.status, - stage: job.stage, - duration: job.duration, - }); - - return acc; - }, state.stages); + [types.RECEIVE_JOBS_SUCCESS](state, { id, data }) { + state.stages = state.stages.map(stage => ({ + ...stage, + isLoading: stage.id === id ? false : stage.isLoading, + jobs: stage.id === id ? data.latest_statuses.map(normalizeJob) : stage.jobs, + })); + }, + [types.TOGGLE_STAGE_COLLAPSE](state, id) { + state.stages = state.stages.map(stage => ({ + ...stage, + isCollapsed: stage.id === id ? !stage.isCollapsed : stage.isCollapsed, + })); }, }; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/state.js b/app/assets/javascripts/ide/stores/modules/pipelines/state.js index 6f22542aaea..0f83b315fff 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/state.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/state.js @@ -1,5 +1,5 @@ export default () => ({ - isLoadingPipeline: false, + isLoadingPipeline: true, isLoadingJobs: false, latestPipeline: null, stages: [], diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js new file mode 100644 index 00000000000..9f4b0d7d726 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js @@ -0,0 +1,7 @@ +// eslint-disable-next-line import/prefer-default-export +export const normalizeJob = job => ({ + id: job.id, + name: job.name, + status: job.status, + path: job.build_path, +}); diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 0a3f8d031c4..fbfb92105d6 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -6,6 +6,7 @@ export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED'; export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED'; export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS'; export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS'; +export const SET_LINKS = 'SET_LINKS'; // Project Mutation Types export const SET_PROJECT = 'SET_PROJECT'; @@ -23,7 +24,6 @@ export const SET_BRANCH = 'SET_BRANCH'; export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT'; export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE'; export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN'; -export const SET_LAST_COMMIT_PIPELINE = 'SET_LAST_COMMIT_PIPELINE'; // Tree mutation types export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA'; @@ -66,3 +66,5 @@ export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW'; export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG'; export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL'; + +export const SET_RIGHT_PANE = 'SET_RIGHT_PANE'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index a257e2ef025..eeaa7cb0ec3 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -114,12 +114,13 @@ export default { }, [types.SET_EMPTY_STATE_SVGS]( state, - { emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath }, + { emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath, pipelinesEmptyStateSvgPath }, ) { Object.assign(state, { emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath, + pipelinesEmptyStateSvgPath, }); }, [types.TOGGLE_FILE_FINDER](state, fileFindVisible) { @@ -148,6 +149,14 @@ export default { unusedSeal: false, }); }, + [types.SET_RIGHT_PANE](state, view) { + Object.assign(state, { + rightPane: state.rightPane === view ? null : view, + }); + }, + [types.SET_LINKS](state, links) { + Object.assign(state, { links }); + }, ...projectMutations, ...mergeRequestMutation, ...fileMutations, diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js index f17ec4da308..e09f88878f4 100644 --- a/app/assets/javascripts/ide/stores/mutations/branch.js +++ b/app/assets/javascripts/ide/stores/mutations/branch.js @@ -14,10 +14,6 @@ export default { treeId: `${projectPath}/${branchName}`, active: true, workingReference: '', - commit: { - ...branch.commit, - pipeline: {}, - }, }, }, }); @@ -32,9 +28,4 @@ export default { commit, }); }, - [types.SET_LAST_COMMIT_PIPELINE](state, { projectId, branchId, pipeline }) { - Object.assign(state.projects[projectId].branches[branchId].commit, { - pipeline, - }); - }, }; diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js index e7411f16a4f..4aac4696075 100644 --- a/app/assets/javascripts/ide/stores/state.js +++ b/app/assets/javascripts/ide/stores/state.js @@ -23,4 +23,6 @@ export default () => ({ currentActivityView: activityBarViews.edit, unusedSeal: true, fileFindVisible: false, + rightPane: null, + links: {}, }); diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js index 741894b5e6c..cdb75752b4e 100644 --- a/app/assets/javascripts/integrations/integration_settings_form.js +++ b/app/assets/javascripts/integrations/integration_settings_form.js @@ -101,13 +101,19 @@ export default class IntegrationSettingsForm { return axios.put(this.testEndPoint, formData) .then(({ data }) => { if (data.error) { - flash(`${data.message} ${data.service_response}`, 'alert', document, { - title: 'Save anyway', - clickHandler: (e) => { - e.preventDefault(); - this.$form.submit(); - }, - }); + let flashActions; + + if (data.test_failed) { + flashActions = { + title: 'Save anyway', + clickHandler: (e) => { + e.preventDefault(); + this.$form.submit(); + }, + }; + } + + flash(`${data.message} ${data.service_response}`, 'alert', document, flashActions); } else { this.$form.submit(); } diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 90d4e19e90b..bb8b3d91e40 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -30,7 +30,7 @@ export default class IssuableForm { } this.initAutosave(); - this.form.on('submit:success', this.handleSubmit); + this.form.on('submit', this.handleSubmit); this.form.on('click', '.btn-cancel', this.resetAutosave); this.initWip(); diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index c67bd7fb0c6..611e8200b4d 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -84,7 +84,7 @@ export default class Job { If the browser does not support position sticky, it returns the position as static. If the browser does support sticky, then we allow the browser to handle it, if not then we use a polyfill - **/ + */ if (this.$topBar.css('position') !== 'static') return; StickyFill.add(this.$topBar); diff --git a/app/assets/javascripts/jobs/job_details_mediator.js b/app/assets/javascripts/jobs/job_details_mediator.js index 5a216f8fae2..89019da9d1e 100644 --- a/app/assets/javascripts/jobs/job_details_mediator.js +++ b/app/assets/javascripts/jobs/job_details_mediator.js @@ -1,5 +1,3 @@ -/* global Build */ - import Visibility from 'visibilityjs'; import Flash from '../flash'; import Poll from '../lib/utils/poll'; @@ -50,7 +48,8 @@ export default class JobMediator { } getJob() { - return this.service.getJob() + return this.service + .getJob() .then(response => this.successCallback(response)) .catch(() => this.errorCallback()); } diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index 6af22ecc990..8c3de6e4045 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -1,7 +1,7 @@ /* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */ import $ from 'jquery'; -import Sortable from 'vendor/Sortable'; +import Sortable from 'sortablejs'; import flash from './flash'; import axios from './lib/utils/axios_utils'; diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js index 2f4328b56e1..2cc5fb10027 100644 --- a/app/assets/javascripts/locale/index.js +++ b/app/assets/javascripts/locale/index.js @@ -9,7 +9,7 @@ delete window.translations; Translates `text` @param text The text to be translated @returns {String} The translated text -**/ +*/ const gettext = locale.gettext.bind(locale); /** @@ -21,7 +21,7 @@ const gettext = locale.gettext.bind(locale); @param pluralText Plural text to translate (eg. '%d days') @param count Number to decide which translation to use (eg. 2) @returns {String} Translated text with the number replaced (eg. '2 days') -**/ +*/ const ngettext = (text, pluralText, count) => { const translated = locale.ngettext(text, pluralText, count).replace(/%d/g, count).split('|'); @@ -38,7 +38,7 @@ const ngettext = (text, pluralText, count) => { (eg. 'Context') @param key Is the dynamic variable you want to be translated @returns {String} Translated context based text -**/ +*/ const pgettext = (keyOrContext, key) => { const normalizedKey = key ? `${keyOrContext}|${key}` : keyOrContext; const translated = gettext(normalizedKey).split('|'); diff --git a/app/assets/javascripts/locale/sprintf.js b/app/assets/javascripts/locale/sprintf.js index 5f4a053f98e..599104dcfa0 100644 --- a/app/assets/javascripts/locale/sprintf.js +++ b/app/assets/javascripts/locale/sprintf.js @@ -10,7 +10,7 @@ import _ from 'underscore'; @see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf @see https://gitlab.com/gitlab-org/gitlab-ce/issues/37992 -**/ +*/ export default (input, parameters, escapeParameters = true) => { let output = input; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index bac7d966ecc..493c119dc6f 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -427,7 +427,7 @@ export default class MergeRequestTabs { If the browser does not support position sticky, it returns the position as static. If the browser does support sticky, then we allow the browser to handle it, if not then we default back to Bootstraps affix - **/ + */ if ($tabs.css('position') !== 'static') return; const $diffTabs = $('#diff-notes-app'); diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 98ce070288e..b2222476924 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -12,20 +12,13 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; let eTagPoll; -export const setNotesData = ({ commit }, data) => - commit(types.SET_NOTES_DATA, data); -export const setNoteableData = ({ commit }, data) => - commit(types.SET_NOTEABLE_DATA, data); -export const setUserData = ({ commit }, data) => - commit(types.SET_USER_DATA, data); -export const setLastFetchedAt = ({ commit }, data) => - commit(types.SET_LAST_FETCHED_AT, data); -export const setInitialNotes = ({ commit }, data) => - commit(types.SET_INITIAL_NOTES, data); -export const setTargetNoteHash = ({ commit }, data) => - commit(types.SET_TARGET_NOTE_HASH, data); -export const toggleDiscussion = ({ commit }, data) => - commit(types.TOGGLE_DISCUSSION, data); +export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data); +export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data); +export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data); +export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data); +export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data); +export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data); +export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data); export const fetchNotes = ({ commit }, path) => service @@ -69,20 +62,14 @@ export const createNewNote = ({ commit }, { endpoint, data }) => return res; }); -export const removePlaceholderNotes = ({ commit }) => - commit(types.REMOVE_PLACEHOLDER_NOTES); +export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES); -export const toggleResolveNote = ( - { commit }, - { endpoint, isResolved, discussion }, -) => +export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) => service .toggleResolveNote(endpoint, isResolved) .then(res => res.json()) .then(res => { - const mutationType = discussion - ? types.UPDATE_DISCUSSION - : types.UPDATE_NOTE; + const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE; commit(mutationType, res); }); @@ -114,7 +101,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => { export const toggleStateButtonLoading = ({ commit }, value) => commit(types.TOGGLE_STATE_BUTTON_LOADING, value); -export const emitStateChangedEvent = ({ commit, getters }, data) => { +export const emitStateChangedEvent = ({ getters }, data) => { const event = new CustomEvent('issuable_vue_app:change', { detail: { data, @@ -179,10 +166,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { loadAwardsHandler() .then(awardsHandler => { - awardsHandler.addAwardToEmojiBar( - votesBlock, - commandsChanges.emoji_award, - ); + awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award); awardsHandler.scrollToAwards(); }) .catch(() => { @@ -194,10 +178,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { }); } - if ( - commandsChanges.spend_time != null || - commandsChanges.time_estimate != null - ) { + if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) { sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res); } } @@ -218,14 +199,8 @@ const pollSuccessCallBack = (resp, commit, state, getters) => { resp.notes.forEach(note => { if (notesById[note.id]) { commit(types.UPDATE_NOTE, note); - } else if ( - note.type === constants.DISCUSSION_NOTE || - note.type === constants.DIFF_NOTE - ) { - const discussion = utils.findNoteObjectById( - state.notes, - note.discussion_id, - ); + } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) { + const discussion = utils.findNoteObjectById(state.notes, note.discussion_id); if (discussion) { commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); @@ -249,11 +224,8 @@ export const poll = ({ commit, state, getters }) => { method: 'poll', data: state, successCallback: resp => - resp - .json() - .then(data => pollSuccessCallBack(data, commit, state, getters)), - errorCallback: () => - Flash('Something went wrong while fetching latest comments.'), + resp.json().then(data => pollSuccessCallBack(data, commit, state, getters)), + errorCallback: () => Flash('Something went wrong while fetching latest comments.'), }); if (!Visibility.hidden()) { @@ -292,14 +264,11 @@ export const fetchData = ({ commit, state, getters }) => { .catch(() => Flash('Something went wrong while fetching latest comments.')); }; -export const toggleAward = ( - { commit, state, getters, dispatch }, - { awardName, noteId }, -) => { +export const toggleAward = ({ commit, getters }, { awardName, noteId }) => { commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] }); }; -export const toggleAwardRequest = ({ commit, getters, dispatch }, data) => { +export const toggleAwardRequest = ({ dispatch }, data) => { const { endpoint, awardName } = data; return service diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue index df21e2f8771..5765eed4d45 100644 --- a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue +++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue @@ -59,7 +59,7 @@ export default { ref="form" :action="deleteWikiUrl" method="post" - class="form-horizontal js-requires-input" + class="js-requires-input" > <input ref="method" diff --git a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js index 53030045292..18c7b21cf8c 100644 --- a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js +++ b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js @@ -5,7 +5,7 @@ import $ from 'jquery'; * * Toggling this checkbox adds/removes a `remember_me` parameter to the * login buttons' href, which is passed on to the omniauth callback. - **/ + */ export default class OAuthRememberMe { constructor(opts = {}) { 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 7bfe11ab8cd..e64afc94ef9 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -79,12 +79,13 @@ export default { }; </script> <template> - <div class="ci-job-dropdown-container dropdown"> + <div class="ci-job-dropdown-container dropdown dropright"> <button v-tooltip type="button" data-toggle="dropdown" data-container="body" + data-boundary="viewport" class="dropdown-menu-toggle build-content" :title="tooltipText" > diff --git a/app/assets/javascripts/raven/raven_config.js b/app/assets/javascripts/raven/raven_config.js index ae54fa5f1a9..658caeecde1 100644 --- a/app/assets/javascripts/raven/raven_config.js +++ b/app/assets/javascripts/raven/raven_config.js @@ -37,7 +37,7 @@ const IGNORE_URLS = [ /extensions\//i, /^chrome:\/\//i, // Other plugins - /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb + /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb /webappstoolbarba\.texthelp\.com\//i, /metrics\.itunes\.apple\.com\.edgesuite\.net\//i, ]; diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js index 593a43c7cc1..c0de03373d8 100644 --- a/app/assets/javascripts/registry/stores/actions.js +++ b/app/assets/javascripts/registry/stores/actions.js @@ -7,9 +7,10 @@ Vue.use(VueResource); export const fetchRepos = ({ commit, state }) => { commit(types.TOGGLE_MAIN_LOADING); - return Vue.http.get(state.endpoint) + return Vue.http + .get(state.endpoint) .then(res => res.json()) - .then((response) => { + .then(response => { commit(types.TOGGLE_MAIN_LOADING); commit(types.SET_REPOS_LIST, response); }); @@ -18,19 +19,20 @@ export const fetchRepos = ({ commit, state }) => { export const fetchList = ({ commit }, { repo, page }) => { commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); - return Vue.http.get(repo.tagsPath, { params: { page } }) - .then((response) => { - const headers = response.headers; + return Vue.http.get(repo.tagsPath, { params: { page } }).then(response => { + const headers = response.headers; - return response.json().then((resp) => { - commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); - commit(types.SET_REGISTRY_LIST, { repo, resp, headers }); - }); + return response.json().then(resp => { + commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); + commit(types.SET_REGISTRY_LIST, { repo, resp, headers }); }); + }); }; +// eslint-disable-next-line no-unused-vars export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath); +// eslint-disable-next-line no-unused-vars export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath); export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index 8c7e0664559..eb581b807a2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -168,8 +168,8 @@ <a :href="mr.mergeCommitPath" class="commit-sha js-mr-merged-commit-sha" + v-text="mr.shortMergeCommitSha" > - {{ mr.shortMergeCommitSha }} </a> <clipboard-button :title="__('Copy commit SHA to clipboard')" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue index 926a3172412..875c3323dbb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue @@ -1,15 +1,63 @@ -/* -The squash-before-merge button is EE only, but it's located right in the middle -of the readyToMerge state component template. - -If we didn't declare this component in CE, we'd need to maintain a separate copy -of the readyToMergeState template in EE, which is pretty big and likely to change. - -Instead, in CE, we declare the component, but it's hidden and is configured to do nothing. -In EE, the configuration extends this object to add a functioning squash-before-merge -button. -*/ - <script> - export default {}; +import Icon from '~/vue_shared/components/icon.vue'; +import eventHub from '~/vue_merge_request_widget/event_hub'; +import tooltip from '~/vue_shared/directives/tooltip'; + +export default { + components: { + Icon, + }, + directives: { + tooltip, + }, + props: { + mr: { + type: Object, + required: true, + }, + isMergeButtonDisabled: { + type: Boolean, + required: true, + }, + }, + data() { + return { + squashBeforeMerge: this.mr.squash, + }; + }, + methods: { + updateSquashModel() { + eventHub.$emit('MRWidgetUpdateSquash', this.squashBeforeMerge); + }, + }, +}; </script> + +<template> + <div class="accept-control inline"> + <label class="merge-param-checkbox"> + <input + type="checkbox" + name="squash" + class="qa-squash-checkbox" + :disabled="isMergeButtonDisabled" + v-model="squashBeforeMerge" + @change="updateSquashModel" + /> + {{ __('Squash commits') }} + </label> + <a + :href="mr.squashBeforeMergeHelpPath" + data-title="About this feature" + data-placement="bottom" + target="_blank" + rel="noopener noreferrer nofollow" + data-container="body" + v-tooltip + > + <icon + name="question-o" + /> + </a> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 1d1c8ebc179..3a194320bd8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -6,11 +6,13 @@ import MergeRequest from '../../../merge_request'; import Flash from '../../../flash'; import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; +import SquashBeforeMerge from './mr_widget_squash_before_merge.vue'; export default { name: 'ReadyToMerge', components: { statusIcon, + 'squash-before-merge': SquashBeforeMerge, }, props: { mr: { type: Object, required: true }, @@ -101,6 +103,12 @@ export default { return enableSquashBeforeMerge && commitsCount > 1; }, }, + created() { + eventHub.$on('MRWidgetUpdateSquash', this.handleUpdateSquash); + }, + beforeDestroy() { + eventHub.$off('MRWidgetUpdateSquash', this.handleUpdateSquash); + }, methods: { shouldShowMergeControls() { return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText; @@ -128,13 +136,9 @@ export default { commit_message: this.commitMessage, merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds, should_remove_source_branch: this.removeSourceBranch === true, + squash: this.mr.squash, }; - // Only truthy in EE extension of this component - if (this.setAdditionalParams) { - this.setAdditionalParams(options); - } - this.isMakingRequest = true; this.service.merge(options) .then(res => res.data) @@ -154,6 +158,9 @@ export default { new Flash('Something went wrong. Please try again.'); // eslint-disable-line }); }, + handleUpdateSquash(val) { + this.mr.squash = val; + }, initiateMergePolling() { simplePoll((continuePolling, stopPolling) => { this.handleMergePolling(continuePolling, stopPolling); diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 83b7b054e6f..e5b7e1f1c68 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -15,6 +15,11 @@ export default class MergeRequestStore { const currentUser = data.current_user; const pipelineStatus = data.pipeline ? data.pipeline.details.status : null; + this.squash = data.squash; + this.squashBeforeMergeHelpPath = this.squashBeforeMergeHelpPath || + data.squash_before_merge_help_path; + this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true; + this.title = data.title; this.targetBranch = data.target_branch; this.sourceBranch = data.source_branch; diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index fcab8f571dd..03f924ba99d 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -22,6 +22,8 @@ import Icon from '../../vue_shared/components/icon.vue'; * - Jobs show view header * - Jobs show view sidebar */ +const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; + export default { components: { Icon, @@ -31,17 +33,36 @@ export default { type: Object, required: true, }, + size: { + type: Number, + required: false, + default: 16, + validator(value) { + return validSizes.includes(value); + }, + }, + borderless: { + type: Boolean, + required: false, + default: false, + }, }, computed: { cssClass() { const status = this.status.group; return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`; }, + icon() { + return this.borderless ? `${this.status.icon}_borderless` : this.status.icon; + }, }, }; </script> <template> <span :class="cssClass"> - <icon :name="status.icon" /> + <icon + :name="icon" + :size="size" + /> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/tabs/tab.vue b/app/assets/javascripts/vue_shared/components/tabs/tab.vue new file mode 100644 index 00000000000..2a35d6bc151 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/tabs/tab.vue @@ -0,0 +1,42 @@ +<script> +export default { + props: { + title: { + type: String, + required: false, + default: '', + }, + active: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + // props can't be updated, so we map it to data where we can + localActive: this.active, + }; + }, + watch: { + active() { + this.localActive = this.active; + }, + }, + created() { + this.isTab = true; + }, +}; +</script> + +<template> + <div + class="tab-pane" + :class="{ + active: localActive + }" + role="tabpanel" + > + <slot></slot> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/tabs/tabs.js b/app/assets/javascripts/vue_shared/components/tabs/tabs.js new file mode 100644 index 00000000000..4362264caa5 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/tabs/tabs.js @@ -0,0 +1,64 @@ +export default { + data() { + return { + currentIndex: 0, + tabs: [], + }; + }, + mounted() { + this.updateTabs(); + }, + methods: { + updateTabs() { + this.tabs = this.$children.filter(child => child.isTab); + this.currentIndex = this.tabs.findIndex(tab => tab.localActive); + }, + setTab(index) { + this.tabs[this.currentIndex].localActive = false; + this.tabs[index].localActive = true; + + this.currentIndex = index; + }, + }, + render(h) { + const navItems = this.tabs.map((tab, i) => + h( + 'li', + { + key: i, + }, + [ + h( + 'a', + { + class: tab.localActive ? 'active' : null, + attrs: { + href: '#', + }, + on: { + click: () => this.setTab(i), + }, + }, + tab.$slots.title || tab.title, + ), + ], + ), + ); + const nav = h( + 'ul', + { + class: 'nav-links tab-links', + }, + [navItems], + ); + const content = h( + 'div', + { + class: ['tab-content'], + }, + [this.$slots.default], + ); + + return h('div', {}, [[nav], content]); + }, +}; diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js index 155e6b6698a..4f2412ce520 100644 --- a/app/assets/javascripts/vue_shared/directives/tooltip.js +++ b/app/assets/javascripts/vue_shared/directives/tooltip.js @@ -2,7 +2,9 @@ import $ from 'jquery'; export default { bind(el) { - $(el).tooltip(); + $(el).tooltip({ + trigger: 'hover', + }); }, componentUpdated(el) { diff --git a/app/assets/javascripts/vue_shared/translate.js b/app/assets/javascripts/vue_shared/translate.js index 2c7886ec308..48c63373b77 100644 --- a/app/assets/javascripts/vue_shared/translate.js +++ b/app/assets/javascripts/vue_shared/translate.js @@ -13,7 +13,7 @@ export default (Vue) => { @param text The text to be translated @returns {String} The translated text - **/ + */ __, /** Translate the text with a number @@ -24,7 +24,7 @@ export default (Vue) => { @param pluralText Plural text to translate (eg. '%d days') @param count Number to decide which translation to use (eg. 2) @returns {String} Translated text with the number replaced (eg. '2 days') - **/ + */ n__, /** Translate context based text @@ -36,7 +36,7 @@ export default (Vue) => { (eg. 'Context') @param key Is the dynamic variable you want to be translated @returns {String} Translated context based text - **/ + */ s__, sprintf, }, diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index e1a47f3d686..d8e57834f9e 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -36,6 +36,11 @@ html [type="button"], cursor: pointer; } +b, +strong { + font-weight: bold; +} + a { color: $gl-link-color; } @@ -48,6 +53,12 @@ a { } } +code { + padding: 2px 4px; + background-color: $red-100; + border-radius: 3px; +} + table { // Remove any table border lines border-spacing: 0; @@ -87,7 +98,8 @@ table { display: none; } -.dropdown-toggle::after { +.dropdown-toggle::after, +.dropright .dropdown-menu-toggle::after { // Remove bootstrap's dropdown caret display: none; } @@ -148,8 +160,14 @@ table { } } -.nav-tabs .nav-link { - border: 0; +.nav-tabs { + .nav-link { + border: 0; + } + + .nav-item { + margin-bottom: 0; + } } pre code { diff --git a/app/assets/stylesheets/errors.scss b/app/assets/stylesheets/errors.scss new file mode 100644 index 00000000000..658e0ff638e --- /dev/null +++ b/app/assets/stylesheets/errors.scss @@ -0,0 +1,120 @@ +/* + * This is a minimal stylesheet, meant to be used for error pages. + */ +@import 'framework/variables'; +@import '../../../node_modules/bootstrap/scss/functions'; +@import '../../../node_modules/bootstrap/scss/variables'; +@import '../../../node_modules/bootstrap/scss/mixins'; +@import '../../../node_modules/bootstrap/scss/reboot'; +@import '../../../node_modules/bootstrap/scss/buttons'; +@import '../../../node_modules/bootstrap/scss/forms'; + +$body-color: #666; +$header-color: #456; + +body { + color: $body-color; + text-align: center; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + margin: auto; + font-size: 14px; +} + +h1 { + font-size: 56px; + line-height: 100px; + font-weight: 400; + color: $header-color; +} + +h2 { + font-size: 24px; + color: $body-color; + line-height: 1.5em; +} + +h3 { + color: $header-color; + font-size: 20px; + font-weight: 400; + line-height: 28px; +} + +img { + max-width: 80vw; + display: block; + margin: 40px auto; +} + +a { + text-decoration: none; + color: $blue-600; +} + +.page-container { + margin: auto 20px; +} + +.container { + margin: auto; + max-width: 600px; + border-bottom: 1px solid $border-color; + padding-bottom: 1em; +} + +.action-container { + padding: 0.5em 0; +} + +.form-inline-flex { + display: flex; + flex-wrap: wrap; + + button { + display: block; + width: 100%; + } + + .field { + display: block; + width: 100%; + margin-bottom: 1em; + } + + @include media-breakpoint-up(sm) { + flex-wrap: nowrap; + + button { + width: auto; + } + + .field { + margin-bottom: 0; + margin-right: 0.5em; + } + } +} + +.error-nav { + padding: 0; + text-align: center; + + li { + display: block; + padding-bottom: 1em; + } + + @include media-breakpoint-up(sm) { + + li { + display: inline-block; + padding-bottom: 0; + + &:not(:first-child)::before { + content: '\00B7'; + display: inline-block; + padding: 0 1em; + } + } + } +} diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss index d6ae8cbb416..11e21edfc1b 100644 --- a/app/assets/stylesheets/framework/gitlab_theme.scss +++ b/app/assets/stylesheets/framework/gitlab_theme.scss @@ -169,11 +169,14 @@ color: $color-800; } - .nav-links li a.active { - border-bottom: 2px solid $color-500; + .nav-links li { + &.active a, + a.active { + border-bottom: 2px solid $color-500; - .badge.badge-pill { - font-weight: $gl-font-weight-bold; + .badge.badge-pill { + font-weight: $gl-font-weight-bold; + } } } @@ -189,6 +192,10 @@ &.active { color: $color-700; box-shadow: inset 3px 0 $color-700; + + &.is-right { + box-shadow: inset -3px 0 $color-700; + } } } } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 17f4958d535..d54490c87c6 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -2,7 +2,7 @@ * Well styled list * */ -.card-body-list { +.hover-list { position: relative; margin: 0; padding: 0; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 0ea0b65b95f..d76cf8f8182 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -19,14 +19,23 @@ width: auto; display: inline-block; overflow-x: auto; - border-left: 0; - border-right: 0; - border-bottom: 0; + border: 0; + border-color: $md-area-border; @supports(width: fit-content) { display: block; width: fit-content; } + + tr { + th { + border-bottom: solid 2px $md-area-border; + } + + td { + border-color: $md-area-border; + } + } } /* diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 667661d8b5c..a7896cc3fc3 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -1,6 +1,5 @@ .modal-header { background-color: $modal-body-bg; - padding: #{3 * $grid-size} #{2 * $grid-size}; .page-title, .modal-title { @@ -74,12 +73,6 @@ body.modal-open { } } -@include media-breakpoint-up(lg) { - .modal-full { - width: 98%; - } -} - .modal { background-color: $black-transparent; z-index: 2100; diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss index c3c64adf3da..847fc8c0792 100644 --- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss +++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss @@ -31,14 +31,15 @@ color: $black; } } + } - &.active { - color: $black; - font-weight: $gl-font-weight-bold; + &.active a, + a.active { + color: $black; + font-weight: $gl-font-weight-bold; - .badge.badge-pill { - color: $black; - } + .badge.badge-pill { + color: $black; } } } diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss index f80e9b1014b..7152ef9bcfd 100644 --- a/app/assets/stylesheets/framework/snippets.scss +++ b/app/assets/stylesheets/framework/snippets.scss @@ -49,26 +49,11 @@ margin-top: 15px; } -.snippet-embed-input { - height: 35px; -} - .embed-snippet { padding-right: 0; padding-top: $gl-padding; - .form-control { - cursor: auto; - width: 101%; - margin-left: -1px; - } - .embed-toggle-list li button { padding: 8px 40px; } - - .embed-toggle, - .snippet-clipboard-btn { - height: 35px; - } } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index ed0bfbbe08b..97b821e0cb9 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -340,10 +340,6 @@ code { } } -a > code { - color: $link-color; -} - .monospace { font-family: $monospace_font; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 89b61530ddb..946223cfff0 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -373,6 +373,7 @@ $dropdown-chevron-size: 10px; $dropdown-toggle-active-border-color: darken($border-color, 14%); $dropdown-item-hover-bg: $gray-darker; $dropdown-fade-mask-height: 32px; +$dropdown-member-form-control-width: 163px; /* * Filtered Search diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index 2b6d92016d5..3e4d123242c 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -6,7 +6,7 @@ .cluster-applications-table { // Wait for the Vue to kick-in and render the applications block - min-height: 400px; + min-height: 628px; } .clusters-dropdown-menu { diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index de2b5701e2d..9914555d309 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -36,13 +36,12 @@ } } - .form-horizontal { - margin-top: 20px; + .form-group { + margin-bottom: 0; - @include media-breakpoint-up(sm) { - display: -webkit-flex; - display: flex; - margin-top: 3px; + @include media-breakpoint-down(sm) { + display: block; + margin-left: 5px; } } @@ -62,10 +61,15 @@ } .member-form-control { - @include media-breakpoint-down(xs) { - padding-bottom: 5px; + @include media-breakpoint-down(sm) { + width: $dropdown-member-form-control-width; margin-left: 0; + padding-bottom: 5px; + } + + @include media-breakpoint-down(xs) { margin-right: 0; + width: auto; } } @@ -207,10 +211,6 @@ align-self: flex-start; } - .form-horizontal ~ .btn { - margin-right: 0; - } - @include media-breakpoint-down(xs) { display: block; @@ -220,6 +220,12 @@ display: block; } + .controls > .btn:last-child { + margin-left: 5px; + margin-right: 5px; + width: auto; + } + .form-control { width: 100%; } @@ -232,10 +238,6 @@ .member-controls { margin-top: 5px; } - - .form-horizontal { - margin-top: 10px; - } } } @@ -259,10 +261,6 @@ margin-top: 0; } - .form-horizontal { - display: block; - } - .member-form-control { margin: 5px 0; } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 06078f1d12e..5d0d59e12f2 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -405,7 +405,7 @@ table.u2f-registrations { margin-right: $gl-padding / 4; } - .label-verification-status { + .badge-verification-status { border-width: 1px; border-style: solid; diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index d5c6048037a..6bbcb15329c 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -909,6 +909,16 @@ width: 1px; background: $white-light; } + + &.is-right { + padding-right: $gl-padding; + padding-left: $gl-padding + 1px; + + &::after { + right: auto; + left: -1px; + } + } } } @@ -1121,3 +1131,112 @@ white-space: nowrap; } } + +.ide-external-link { + svg { + display: none; + } + + &:hover, + &:focus { + svg { + display: inline-block; + } + } +} + +.ide-right-sidebar { + width: auto; + min-width: 60px; + + .ide-activity-bar { + border-left: 1px solid $white-dark; + } + + .multi-file-commit-panel-inner { + width: 350px; + padding: $grid-size $gl-padding; + background-color: $white-light; + border-left: 1px solid $white-dark; + } +} + +.ide-pipeline { + display: flex; + flex-direction: column; + height: 100%; + + .empty-state { + margin-top: auto; + margin-bottom: auto; + + p { + margin: $grid-size 0; + text-align: center; + line-height: 24px; + } + + .btn, + h4 { + margin: 0; + } + } +} + +.ide-pipeline-list { + flex: 1; + overflow: auto; +} + +.ide-pipeline-header { + min-height: 50px; + padding-left: $gl-padding; + padding-right: $gl-padding; + + .ci-status-icon { + display: flex; + } +} + +.ide-job-item { + display: flex; + padding: 16px; + + &:not(:last-child) { + border-bottom: 1px solid $border-color; + } + + .ci-status-icon { + display: flex; + justify-content: center; + height: 20px; + margin-top: -2px; + overflow: hidden; + } +} + +.ide-stage { + .card-header { + display: flex; + cursor: pointer; + + .ci-status-icon { + display: flex; + align-items: center; + } + } + + .card-body { + padding: 0; + } +} + +.ide-stage-collapse-icon { + margin: auto 0 auto auto; +} + +.ide-stage-title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb index 7ed2de71028..7aba77d8129 100644 --- a/app/controllers/admin/runner_projects_controller.rb +++ b/app/controllers/admin/runner_projects_controller.rb @@ -4,9 +4,7 @@ class Admin::RunnerProjectsController < Admin::ApplicationController def create @runner = Ci::Runner.find(params[:runner_project][:runner_id]) - runner_project = @runner.assign_to(@project, current_user) - - if runner_project.persisted? + if @runner.assign_to(@project, current_user) redirect_to admin_runner_path(@runner) else redirect_to admin_runner_path(@runner), alert: 'Failed adding runner to project' diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2843d70c645..db8a8cdc0d2 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -146,14 +146,15 @@ class ApplicationController < ActionController::Base end def render_403 - head :forbidden + respond_to do |format| + format.any { head :forbidden } + format.html { render "errors/access_denied", layout: "errors", status: 403 } + end end def render_404 respond_to do |format| - format.html do - render file: Rails.root.join("public", "404"), layout: false, status: "404" - end + format.html { render "errors/not_found", layout: "errors", status: 404 } # Prevent the Rails CSRF protector from thinking a missing .js file is a JavaScript file format.js { render json: '', status: :not_found, content_type: 'application/json' } format.any { head :not_found } diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index 3b11a373368..b6eb7d292fc 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -17,10 +17,23 @@ module IssuesAction end # rubocop:enable Gitlab/ModuleWithInstanceVariables + # rubocop:disable Gitlab/ModuleWithInstanceVariables + def issues_calendar + @issues = issuables_collection + .non_archived + .with_due_date + .limit(100) + + respond_to do |format| + format.ics { response.headers['Content-Disposition'] = 'inline' } + end + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables + private def finder_type (super if defined?(super)) || - (IssuesFinder if action_name == 'issues') + (IssuesFinder if %w(issues issues_calendar).include?(action_name)) end end diff --git a/app/controllers/groups/shared_projects_controller.rb b/app/controllers/groups/shared_projects_controller.rb new file mode 100644 index 00000000000..f2f835767e0 --- /dev/null +++ b/app/controllers/groups/shared_projects_controller.rb @@ -0,0 +1,31 @@ +module Groups + class SharedProjectsController < Groups::ApplicationController + respond_to :json + before_action :group + skip_cross_project_access_check :index + + def index + shared_projects = GroupProjectsFinder.new( + group: group, + current_user: current_user, + params: finder_params, + options: { only_shared: true } + ).execute + serializer = GroupChildSerializer.new(current_user: current_user) + .with_pagination(request, response) + + render json: serializer.represent(shared_projects) + end + + private + + def finder_params + @finder_params ||= begin + # Make the `search` param consistent for the frontend, + # which will be using `filter`. + params[:search] ||= params[:filter] if params[:filter] + params.permit(:sort, :search) + end + end + end +end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 9f5ad23a20f..074db361949 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -34,12 +34,12 @@ class ProfilesController < Profiles::ApplicationController redirect_to profile_personal_access_tokens_path end - def reset_rss_token + def reset_feed_token Users::UpdateService.new(current_user, user: @user).execute! do |user| - user.reset_rss_token! + user.reset_feed_token! end - flash[:notice] = "RSS token was successfully reset" + flash[:notice] = 'Feed token was successfully reset' redirect_to profile_personal_access_tokens_path end diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb index 35885543622..4d758402850 100644 --- a/app/controllers/projects/clusters/applications_controller.rb +++ b/app/controllers/projects/clusters/applications_controller.rb @@ -5,7 +5,17 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll before_action :authorize_create_cluster!, only: [:create] def create - application = @application_class.find_or_create_by!(cluster: @cluster) + application = @application_class.find_or_initialize_by(cluster: @cluster) + + if application.has_attribute?(:hostname) + application.hostname = params[:hostname] + end + + if application.respond_to?(:oauth_application) + application.oauth_application = create_oauth_application(application) + end + + application.save! Clusters::Applications::ScheduleInstallationService.new(project, current_user).execute(application) @@ -23,4 +33,15 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll def application_class @application_class ||= Clusters::Cluster::APPLICATIONS[params[:application]] || render_404 end + + def create_oauth_application(application) + oauth_application_params = { + name: params[:application], + redirect_uri: application.callback_url, + scopes: 'api read_user openid', + owner: current_user + } + + Applications::CreateService.new(current_user, oauth_application_params).execute + end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index d69015c8665..35c36c725e2 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -10,8 +10,8 @@ class Projects::IssuesController < Projects::ApplicationController before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update] before_action :check_issues_available! - before_action :issue, except: [:index, :new, :create, :bulk_update] - before_action :set_issuables_index, only: [:index] + before_action :issue, except: [:index, :calendar, :new, :create, :bulk_update] + before_action :set_issuables_index, only: [:index, :calendar] # Allow write(create) issue before_action :authorize_create_issue!, only: [:new, :create] @@ -39,6 +39,17 @@ class Projects::IssuesController < Projects::ApplicationController end end + def calendar + @issues = @issuables + .non_archived + .with_due_date + .limit(100) + + respond_to do |format| + format.ics { response.headers['Content-Disposition'] = 'inline' } + end + end + def new params[:issue] ||= ActionController::Parameters.new( assignee_ids: "" diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb index 67d4ea2ca8f..29632bef7e5 100644 --- a/app/controllers/projects/merge_requests/application_controller.rb +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -24,6 +24,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont :source_branch, :source_project_id, :state_event, + :squash, :target_branch, :target_project_id, :task_num, diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 62b739918e6..ecea6e1b2bf 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -253,7 +253,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def merge_params_attributes - [:should_remove_source_branch, :commit_message] + [:should_remove_source_branch, :commit_message, :squash] end def merge_when_pipeline_succeeds_active? @@ -282,7 +282,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo return :sha_mismatch if params[:sha] != @merge_request.diff_head_sha - @merge_request.update(merge_error: nil) + @merge_request.update(merge_error: nil, squash: merge_params.fetch(:squash, false)) if params[:merge_when_pipeline_succeeds].present? return :failed unless @merge_request.actual_head_pipeline @@ -296,14 +296,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo elsif @merge_request.actual_head_pipeline.success? # This can be triggered when a user clicks the auto merge button while # the tests finish at about the same time - @merge_request.merge_async(current_user.id, params) + @merge_request.merge_async(current_user.id, merge_params) :success else :failed end else - @merge_request.merge_async(current_user.id, params) + @merge_request.merge_async(current_user.id, merge_params) :success end diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb index 0ec2490655f..a080724634b 100644 --- a/app/controllers/projects/runner_projects_controller.rb +++ b/app/controllers/projects/runner_projects_controller.rb @@ -9,9 +9,8 @@ class Projects::RunnerProjectsController < Projects::ApplicationController return head(403) unless can?(current_user, :assign_runner, @runner) path = project_runners_path(project) - runner_project = @runner.assign_to(project, current_user) - if runner_project.persisted? + if @runner.assign_to(project, current_user) redirect_to path else redirect_to path, alert: 'Failed adding runner to project' diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index a5ea9ff7ed7..690596b12db 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -41,13 +41,13 @@ class Projects::ServicesController < Projects::ApplicationController if outcome[:success] {} else - { error: true, message: 'Test failed.', service_response: outcome[:result].to_s } + { error: true, message: 'Test failed.', service_response: outcome[:result].to_s, test_failed: true } end else - { error: true, message: 'Validations failed.', service_response: @service.errors.full_messages.join(',') } + { error: true, message: 'Validations failed.', service_response: @service.errors.full_messages.join(','), test_failed: false } end rescue Gitlab::HTTP::BlockedUrlError => e - { error: true, message: 'Test failed.', service_response: e.message } + { error: true, message: 'Test failed.', service_response: e.message, test_failed: true } end def success_message diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 1787b4899cd..3626670d141 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -75,6 +75,8 @@ class IssuesFinder < IssuableFinder items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week) elsif filter_by_due_this_month? items = items.due_between(Date.today.beginning_of_month, Date.today.end_of_month) + elsif filter_by_due_next_month_and_previous_two_weeks? + items = items.due_between(Date.today - 2.weeks, (Date.today + 1.month).end_of_month) end end @@ -97,6 +99,10 @@ class IssuesFinder < IssuableFinder due_date? && params[:due_date] == Issue::DueThisMonth.name end + def filter_by_due_next_month_and_previous_two_weeks? + due_date? && params[:due_date] == Issue::DueNextMonthAndPreviousTwoWeeks.name + end + def due_date? params[:due_date].present? end diff --git a/app/helpers/calendar_helper.rb b/app/helpers/calendar_helper.rb new file mode 100644 index 00000000000..c54b91b0ce5 --- /dev/null +++ b/app/helpers/calendar_helper.rb @@ -0,0 +1,8 @@ +module CalendarHelper + def calendar_url_options + { format: :ics, + feed_token: current_user.try(:feed_token), + due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name, + sort: 'closest_future_date' } + end +end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index c19c5b9cc82..74251c260f0 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -97,8 +97,9 @@ module MergeRequestsHelper { merge_when_pipeline_succeeds: true, should_remove_source_branch: true, - sha: merge_request.diff_head_sha - }.merge(merge_params_ee(merge_request)) + sha: merge_request.diff_head_sha, + squash: merge_request.squash + } end def tab_link_for(merge_request, tab, options = {}, &block) @@ -149,8 +150,4 @@ module MergeRequestsHelper current_user.fork_of(project) end end - - def merge_params_ee(merge_request) - {} - end end diff --git a/app/helpers/rss_helper.rb b/app/helpers/rss_helper.rb index 9ac4df88dc3..7d4fa83a67a 100644 --- a/app/helpers/rss_helper.rb +++ b/app/helpers/rss_helper.rb @@ -1,5 +1,5 @@ module RssHelper def rss_url_options - { format: :atom, rss_token: current_user.try(:rss_token) } + { format: :atom, feed_token: current_user.try(:feed_token) } end end diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb index e12e4ba70e9..72f6b397046 100644 --- a/app/helpers/webpack_helper.rb +++ b/app/helpers/webpack_helper.rb @@ -1,5 +1,3 @@ -require 'gitlab/webpack/manifest' - module WebpackHelper def webpack_bundle_tag(bundle) javascript_include_tag(*webpack_entrypoint_paths(bundle)) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index b12f7a2c83f..3d58a14882f 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -360,17 +360,6 @@ class ApplicationSetting < ActiveRecord::Base Array(read_attribute(:repository_storages)) end - # DEPRECATED - # repository_storage is still required in the API. Remove in 9.0 - # Still used in API v3 - def repository_storage - repository_storages.first - end - - def repository_storage=(value) - self.repository_storages = [value] - end - def default_project_visibility=(level) super(Gitlab::VisibilityLevel.level_value(level)) end diff --git a/app/models/badge.rb b/app/models/badge.rb index f7e10c2ebfc..265c5d872d4 100644 --- a/app/models/badge.rb +++ b/app/models/badge.rb @@ -18,7 +18,7 @@ class Badge < ActiveRecord::Base scope :order_created_at_asc, -> { reorder(created_at: :asc) } - validates :link_url, :image_url, url_placeholder: { protocols: %w(http https), placeholder_regex: PLACEHOLDERS_REGEX } + validates :link_url, :image_url, url: { protocols: %w(http https) } validates :type, presence: true def rendered_link_url(project = nil) diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 530eacf4be0..57edd6a4956 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -12,9 +12,9 @@ module Ci FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze has_many :builds - has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :runner_projects, inverse_of: :runner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :projects, through: :runner_projects - has_many :runner_namespaces + has_many :runner_namespaces, inverse_of: :runner has_many :groups, through: :runner_namespaces has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build' @@ -56,10 +56,15 @@ module Ci end validate :tag_constraints - validate :either_projects_or_group validates :access_level, presence: true validates :runner_type, presence: true + validate :no_projects, unless: :project_type? + validate :no_groups, unless: :group_type? + validate :any_project, if: :project_type? + validate :exactly_one_group, if: :group_type? + validate :validate_is_shared + acts_as_taggable after_destroy :cleanup_runner_queue @@ -115,8 +120,15 @@ module Ci raise ArgumentError, 'Transitioning a group runner to a project runner is not supported' end - self.save - project.runner_projects.create(runner_id: self.id) + begin + transaction do + self.projects << project + self.save! + end + rescue ActiveRecord::RecordInvalid => e + self.errors.add(:assign_to, e.message) + false + end end def display_name @@ -253,13 +265,33 @@ module Ci self.class.owned_or_shared(project_id).where(id: self.id).any? end - def either_projects_or_group - if groups.many? - errors.add(:runner, 'can only be assigned to one group') + def no_projects + if projects.any? + errors.add(:runner, 'cannot have projects assigned') + end + end + + def no_groups + if groups.any? + errors.add(:runner, 'cannot have groups assigned') + end + end + + def any_project + unless projects.any? + errors.add(:runner, 'needs to be assigned to at least one project') + end + end + + def exactly_one_group + unless groups.one? + errors.add(:runner, 'needs to be assigned to exactly one group') end + end - if assigned_to_group? && assigned_to_project? - errors.add(:runner, 'can only be assigned either to projects or to a group') + def validate_is_shared + unless is_shared? == instance_type? + errors.add(:is_shared, 'is not equal to instance_type?') end end diff --git a/app/models/ci/runner_namespace.rb b/app/models/ci/runner_namespace.rb index 3269f86e8ca..29508fdd326 100644 --- a/app/models/ci/runner_namespace.rb +++ b/app/models/ci/runner_namespace.rb @@ -2,8 +2,10 @@ module Ci class RunnerNamespace < ActiveRecord::Base extend Gitlab::Ci::Model - belongs_to :runner - belongs_to :namespace, class_name: '::Namespace' + belongs_to :runner, inverse_of: :runner_namespaces, validate: true + belongs_to :namespace, inverse_of: :runner_namespaces, class_name: '::Namespace' belongs_to :group, class_name: '::Group', foreign_key: :namespace_id + + validates :runner_id, uniqueness: { scope: :namespace_id } end end diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb index 505d178ba8e..52437047300 100644 --- a/app/models/ci/runner_project.rb +++ b/app/models/ci/runner_project.rb @@ -2,8 +2,8 @@ module Ci class RunnerProject < ActiveRecord::Base extend Gitlab::Ci::Model - belongs_to :runner - belongs_to :project + belongs_to :runner, inverse_of: :runner_projects + belongs_to :project, inverse_of: :runner_projects validates :runner_id, uniqueness: { scope: :project_id } end diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb new file mode 100644 index 00000000000..975d434e1a4 --- /dev/null +++ b/app/models/clusters/applications/jupyter.rb @@ -0,0 +1,92 @@ +module Clusters + module Applications + class Jupyter < ActiveRecord::Base + VERSION = '0.0.1'.freeze + + self.table_name = 'clusters_applications_jupyter' + + include ::Clusters::Concerns::ApplicationCore + include ::Clusters::Concerns::ApplicationStatus + include ::Clusters::Concerns::ApplicationData + + belongs_to :oauth_application, class_name: 'Doorkeeper::Application' + + default_value_for :version, VERSION + + def set_initial_status + return unless not_installable? + + if cluster&.application_ingress_installed? && cluster.application_ingress.external_ip + self.status = 'installable' + end + end + + def chart + "#{name}/jupyterhub" + end + + def repository + 'https://jupyterhub.github.io/helm-chart/' + end + + def values + content_values.to_yaml + end + + def install_command + Gitlab::Kubernetes::Helm::InstallCommand.new( + name, + chart: chart, + values: values, + repository: repository + ) + end + + def callback_url + "http://#{hostname}/hub/oauth_callback" + end + + private + + def specification + { + "ingress" => { + "hosts" => [hostname] + }, + "hub" => { + "extraEnv" => { + "GITLAB_HOST" => gitlab_url + }, + "cookieSecret" => cookie_secret + }, + "proxy" => { + "secretToken" => secret_token + }, + "auth" => { + "gitlab" => { + "clientId" => oauth_application.uid, + "clientSecret" => oauth_application.secret, + "callbackUrl" => callback_url + } + } + } + end + + def gitlab_url + Gitlab.config.gitlab.url + end + + def content_values + YAML.load_file(chart_values_file).deep_merge!(specification) + end + + def secret_token + @secret_token ||= SecureRandom.hex(32) + end + + def cookie_secret + @cookie_secret ||= SecureRandom.hex(32) + end + end + end +end diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index b881b4eaf36..e6f795f3e0b 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -43,7 +43,7 @@ module Clusters def create_and_assign_runner transaction do - project.runners.create!(runner_create_params).tap do |runner| + Ci::Runner.create!(runner_create_params).tap do |runner| update!(runner_id: runner.id) end end @@ -53,7 +53,8 @@ module Clusters { name: 'kubernetes-cluster', runner_type: :project_type, - tag_list: %w(kubernetes cluster) + tag_list: %w(kubernetes cluster), + projects: [project] } end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 77947d515c1..b426b1bf8a1 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -8,7 +8,8 @@ module Clusters Applications::Helm.application_name => Applications::Helm, Applications::Ingress.application_name => Applications::Ingress, Applications::Prometheus.application_name => Applications::Prometheus, - Applications::Runner.application_name => Applications::Runner + Applications::Runner.application_name => Applications::Runner, + Applications::Jupyter.application_name => Applications::Jupyter }.freeze DEFAULT_ENVIRONMENT = '*'.freeze @@ -26,6 +27,7 @@ module Clusters has_one :application_ingress, class_name: 'Clusters::Applications::Ingress' has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus' has_one :application_runner, class_name: 'Clusters::Applications::Runner' + has_one :application_jupyter, class_name: 'Clusters::Applications::Jupyter' accepts_nested_attributes_for :provider_gcp, update_only: true accepts_nested_attributes_for :platform_kubernetes, update_only: true @@ -39,6 +41,7 @@ module Clusters delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true delegate :installed?, to: :application_helm, prefix: true, allow_nil: true + delegate :installed?, to: :application_ingress, prefix: true, allow_nil: true enum platform_type: { kubernetes: 1 @@ -74,7 +77,8 @@ module Clusters application_helm || build_application_helm, application_ingress || build_application_ingress, application_prometheus || build_application_prometheus, - application_runner || build_application_runner + application_runner || build_application_runner, + application_jupyter || build_application_jupyter ] end diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb index b32459fdabf..d58d7165969 100644 --- a/app/models/concerns/cacheable_attributes.rb +++ b/app/models/concerns/cacheable_attributes.rb @@ -6,15 +6,16 @@ module CacheableAttributes end class_methods do + def cache_key + "#{name}:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:#{Rails.version}".freeze + end + # Can be overriden def current_without_cache last end - def cache_key - "#{name}:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:json".freeze - end - + # Can be overriden def defaults {} end @@ -24,10 +25,18 @@ module CacheableAttributes end def cached - json_attributes = Rails.cache.read(cache_key) - return nil unless json_attributes.present? + if RequestStore.active? + RequestStore[:"#{name}_cached_attributes"] ||= retrieve_from_cache + else + retrieve_from_cache + end + end + + def retrieve_from_cache + record = Rails.cache.read(cache_key) + ensure_cache_setup if record.present? - build_from_defaults(JSON.parse(json_attributes)) + record end def current @@ -35,7 +44,12 @@ module CacheableAttributes return cached_record if cached_record.present? current_without_cache.tap { |current_record| current_record&.cache! } - rescue + rescue => e + if Rails.env.production? + Rails.logger.warn("Cached record for #{name} couldn't be loaded, falling back to uncached record: #{e}") + else + raise e + end # Fall back to an uncached value if there are any problems (e.g. Redis down) current_without_cache end @@ -46,9 +60,15 @@ module CacheableAttributes # Gracefully handle when Redis is not available. For example, # omnibus may fail here during gitlab:assets:compile. end + + def ensure_cache_setup + # This is a workaround for a Rails bug that causes attribute methods not + # to be loaded when read from cache: https://github.com/rails/rails/issues/27348 + define_attribute_methods + end end def cache! - Rails.cache.write(self.class.cache_key, attributes.to_json) + Rails.cache.write(self.class.cache_key, self) end end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index b45395343cc..44150b37708 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -97,8 +97,6 @@ module Issuable strip_attributes :title - after_save :ensure_metrics, unless: :imported? - # We want to use optimistic lock for cases when only title or description are involved # http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html def locking_enabled? diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index b3fec99c816..1f7d78a2efe 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -1,4 +1,4 @@ -# Makes api V3 compatible with old project features permissions methods +# Makes api V4 compatible with old project features permissions methods # # After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled # fields to a new table "project_features", support for the old fields is still needed in the API. diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb index eef9caf1c8e..be0a5b49012 100644 --- a/app/models/concerns/reactive_caching.rb +++ b/app/models/concerns/reactive_caching.rb @@ -74,6 +74,7 @@ module ReactiveCaching def clear_reactive_cache!(*args) Rails.cache.delete(full_reactive_cache_key(*args)) + Rails.cache.delete(alive_reactive_cache_key(*args)) end def exclusively_update_reactive_cache!(*args) diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb index 1caf47072bc..0fc321c52bc 100644 --- a/app/models/concerns/time_trackable.rb +++ b/app/models/concerns/time_trackable.rb @@ -30,8 +30,6 @@ module TimeTrackable return if @time_spent == 0 - touch if touchable? - if @time_spent == :reset reset_spent_time else @@ -59,10 +57,6 @@ module TimeTrackable private - def touchable? - valid? && persisted? - end - def reset_spent_time timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables end diff --git a/app/models/environment.rb b/app/models/environment.rb index fddb269af4b..8d523dae324 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -32,7 +32,7 @@ class Environment < ActiveRecord::Base validates :external_url, length: { maximum: 255 }, allow_nil: true, - addressable_url: true + url: true delegate :stop_action, :manual_actions, to: :last_deployment, allow_nil: true diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb index 532b8f4ad69..5ac8bde44cd 100644 --- a/app/models/generic_commit_status.rb +++ b/app/models/generic_commit_status.rb @@ -1,7 +1,7 @@ class GenericCommitStatus < CommitStatus before_validation :set_default_values - validates :target_url, addressable_url: true, + validates :target_url, url: true, length: { maximum: 255 }, allow_nil: true diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb index 0528266e5b3..6bef00f26ea 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -11,4 +11,9 @@ class SystemHook < WebHook default_value_for :push_events, false default_value_for :repository_update_events, true default_value_for :merge_requests_events, false + + # Allow urls pointing localhost and the local network + def allow_local_requests? + true + end end diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 27729deeac9..e353abdda9c 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -3,7 +3,9 @@ class WebHook < ActiveRecord::Base has_many :web_hook_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - validates :url, presence: true, url: true + validates :url, presence: true, public_url: { allow_localhost: lambda(&:allow_local_requests?), + allow_local_network: lambda(&:allow_local_requests?) } + validates :token, format: { without: /\n/ } def execute(data, hook_name) @@ -13,4 +15,9 @@ class WebHook < ActiveRecord::Base def async_execute(data, hook_name) WebHookService.new(self, data, hook_name).async_execute end + + # Allow urls pointing localhost and the local network + def allow_local_requests? + false + end end diff --git a/app/models/issue.rb b/app/models/issue.rb index 0332bfa9371..41a290f34b4 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -14,12 +14,13 @@ class Issue < ActiveRecord::Base ignore_column :assignee_id, :branch_name, :deleted_at - DueDateStruct = Struct.new(:title, :name).freeze - NoDueDate = DueDateStruct.new('No Due Date', '0').freeze - AnyDueDate = DueDateStruct.new('Any Due Date', '').freeze - Overdue = DueDateStruct.new('Overdue', 'overdue').freeze - DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze - DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze + DueDateStruct = Struct.new(:title, :name).freeze + NoDueDate = DueDateStruct.new('No Due Date', '0').freeze + AnyDueDate = DueDateStruct.new('Any Due Date', '').freeze + Overdue = DueDateStruct.new('Overdue', 'overdue').freeze + DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze + DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze + DueNextMonthAndPreviousTwoWeeks = DueDateStruct.new('Due Next Month And Previous Two Weeks', 'next_month_and_previous_two_weeks').freeze belongs_to :project belongs_to :moved_to, class_name: 'Issue' @@ -46,6 +47,7 @@ class Issue < ActiveRecord::Base scope :unassigned, -> { where('NOT EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') } scope :assigned_to, ->(u) { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = ? AND issue_id = issues.id)', u.id)} + scope :with_due_date, -> { where('due_date IS NOT NULL') } scope :without_due_date, -> { where(due_date: nil) } scope :due_before, ->(date) { where('issues.due_date < ?', date) } scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) } @@ -53,12 +55,14 @@ class Issue < ActiveRecord::Base scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') } scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') } + scope :order_closest_future_date, -> { reorder('CASE WHEN due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - due_date) ASC') } scope :preload_associations, -> { preload(:labels, project: :namespace) } scope :public_only, -> { where(confidential: false) } after_save :expire_etag_cache + after_save :ensure_metrics, unless: :imported? attr_spammable :title, spam_title: true attr_spammable :description, spam_description: true @@ -119,6 +123,7 @@ class Issue < ActiveRecord::Base def self.sort_by_attribute(method, excluded_labels: []) case method.to_s + when 'closest_future_date' then order_closest_future_date when 'due_date' then order_due_date_asc when 'due_date_asc' then order_due_date_asc when 'due_date_desc' then order_due_date_desc diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 9c4384a6e42..79fc155fd3c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -58,6 +58,7 @@ class MergeRequest < ActiveRecord::Base after_create :ensure_merge_request_diff, unless: :importing? after_update :clear_memoized_shas after_update :reload_diff_if_branch_changed + after_save :ensure_metrics # When this attribute is true some MR validation is ignored # It allows us to close or modify broken merge requests @@ -1140,4 +1141,11 @@ class MergeRequest < ActiveRecord::Base maintainer_push_possible? && Ability.allowed?(user, :push_code, source_project) end + + def squash_in_progress? + # The source project can be deleted + return false unless source_project + + source_project.repository.squash_in_progress?(id) + end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 3dad4277713..52fe529c016 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -21,7 +21,7 @@ class Namespace < ActiveRecord::Base has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :project_statistics - has_many :runner_namespaces, class_name: 'Ci::RunnerNamespace' + has_many :runner_namespaces, inverse_of: :namespace, class_name: 'Ci::RunnerNamespace' has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner' # This should _not_ be `inverse_of: :namespace`, because that would also set diff --git a/app/models/project.rb b/app/models/project.rb index e275ac4dc6f..32298fc7f5c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -236,7 +236,7 @@ class Project < ActiveRecord::Base has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName' has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks - has_many :runner_projects, class_name: 'Ci::RunnerProject' + has_many :runner_projects, class_name: 'Ci::RunnerProject', inverse_of: :project has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' has_many :variables, class_name: 'Ci::Variable' has_many :triggers, class_name: 'Ci::Trigger' @@ -289,8 +289,9 @@ class Project < ActiveRecord::Base validates :namespace, presence: true validates :name, uniqueness: { scope: :namespace_id } - validates :import_url, addressable_url: true, if: :external_import? - validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?] + validates :import_url, url: { protocols: %w(http https ssh git), + allow_localhost: false, + ports: VALID_IMPORT_PORTS }, if: [:external_import?, :import_url_changed?] validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? } diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 54e4b3278db..7f4c47a6d14 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -3,7 +3,7 @@ class BambooService < CiService prop_accessor :bamboo_url, :build_key, :username, :password - validates :bamboo_url, presence: true, url: true, if: :activated? + validates :bamboo_url, presence: true, public_url: true, if: :activated? validates :build_key, presence: true, if: :activated? validates :username, presence: true, diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb index 046e2809f45..e4e3a80976b 100644 --- a/app/models/project_services/bugzilla_service.rb +++ b/app/models/project_services/bugzilla_service.rb @@ -1,5 +1,5 @@ class BugzillaService < IssueTrackerService - validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? + validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index d2aaff8817a..35884c4560c 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -8,7 +8,7 @@ class BuildkiteService < CiService prop_accessor :project_url, :token boolean_accessor :enable_ssl_verification - validates :project_url, presence: true, url: true, if: :activated? + validates :project_url, presence: true, public_url: true, if: :activated? validates :token, presence: true, if: :activated? after_save :compose_service_hook, if: :activated? diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index 7591ab4f478..ae0debbd3ac 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -8,7 +8,7 @@ class ChatNotificationService < Service prop_accessor :webhook, :username, :channel boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch - validates :webhook, presence: true, url: true, if: :activated? + validates :webhook, presence: true, public_url: true, if: :activated? def initialize_properties # Custom serialized properties initialization diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index b9e3e982b64..456c7f5cee2 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -1,5 +1,5 @@ class CustomIssueTrackerService < IssueTrackerService - validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? + validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index a4bf427ac0b..ab4e46da89f 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -4,7 +4,7 @@ class DroneCiService < CiService prop_accessor :drone_url, :token boolean_accessor :enable_ssl_verification - validates :drone_url, presence: true, url: true, if: :activated? + validates :drone_url, presence: true, public_url: true, if: :activated? validates :token, presence: true, if: :activated? after_save :compose_service_hook, if: :activated? diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index 1553f169827..a4b1ef09e93 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -1,7 +1,7 @@ class ExternalWikiService < Service prop_accessor :external_wiki_url - validates :external_wiki_url, presence: true, url: true, if: :activated? + validates :external_wiki_url, presence: true, public_url: true, if: :activated? def title 'External Wiki' diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 88c428b4aae..16e32a4139e 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -1,7 +1,7 @@ class GitlabIssueTrackerService < IssueTrackerService include Gitlab::Routing - validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? + validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index ed4bbfb6cfc..eb3261c902f 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -3,8 +3,8 @@ class JiraService < IssueTrackerService include ApplicationHelper include ActionView::Helpers::AssetUrlHelper - validates :url, url: true, presence: true, if: :activated? - validates :api_url, url: true, allow_blank: true + validates :url, public_url: true, presence: true, if: :activated? + validates :api_url, public_url: true, allow_blank: true validates :username, presence: true, if: :activated? validates :password, presence: true, if: :activated? diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 20fed432e55..ddd4026019b 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -24,7 +24,7 @@ class KubernetesService < DeploymentService prop_accessor :ca_pem with_options presence: true, if: :activated? do - validates :api_url, url: true + validates :api_url, public_url: true validates :token end diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb index 2221459c90b..b89dc07a73e 100644 --- a/app/models/project_services/mock_ci_service.rb +++ b/app/models/project_services/mock_ci_service.rb @@ -3,7 +3,7 @@ class MockCiService < CiService ALLOWED_STATES = %w[failed canceled running pending success success_with_warnings skipped not_found].freeze prop_accessor :mock_service_url - validates :mock_service_url, presence: true, url: true, if: :activated? + validates :mock_service_url, presence: true, public_url: true, if: :activated? def title 'MockCI' diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index dcaeb65dc32..df4254e0523 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -6,7 +6,7 @@ class PrometheusService < MonitoringService boolean_accessor :manual_configuration with_options presence: true, if: :manual_configuration? do - validates :api_url, url: true + validates :api_url, public_url: true end before_save :synchronize_service_state diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index 6acf611eba5..3721093a6d1 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -1,5 +1,5 @@ class RedmineService < IssueTrackerService - validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? + validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 145313b8e71..802678147cf 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -3,7 +3,7 @@ class TeamcityService < CiService prop_accessor :teamcity_url, :build_type, :username, :password - validates :teamcity_url, presence: true, url: true, if: :activated? + validates :teamcity_url, presence: true, public_url: true, if: :activated? validates :build_type, presence: true, if: :activated? validates :username, presence: true, diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index aba1f2f384f..5cd222e18a4 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -17,7 +17,6 @@ class RemoteMirror < ActiveRecord::Base belongs_to :project, inverse_of: :remote_mirrors validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true } - validates :url, addressable_url: true, if: :url_changed? before_save :set_new_remote_name, if: :mirror_url_changed? diff --git a/app/models/repository.rb b/app/models/repository.rb index 0e1bf11d7c0..82cf47ba04e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -957,6 +957,22 @@ class Repository remote_branch: merge_request.target_branch) end + def blob_data_at(sha, path) + blob = blob_at(sha, path) + return unless blob + + blob.load_all_data! + blob.data + end + + def squash(user, merge_request) + raw.squash(user, merge_request.id, branch: merge_request.target_branch, + start_sha: merge_request.diff_start_sha, + end_sha: merge_request.diff_head_sha, + author: merge_request.author, + message: merge_request.title) + end + private # TODO Generice finder, later split this on finders by Ref or Oid @@ -971,14 +987,6 @@ class Repository ::Commit.new(commit, @project) if commit end - def blob_data_at(sha, path) - blob = blob_at(sha, path) - return unless blob - - blob.load_all_data! - blob.data - end - def cache @cache ||= Gitlab::RepositoryCache.new(self) end diff --git a/app/models/service.rb b/app/models/service.rb index 831c2ea1141..1d259bcfec7 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -206,10 +206,11 @@ class Service < ActiveRecord::Base args.each do |arg| class_eval %{ def #{arg}? + # '!!' is used because nil or empty string is converted to nil if Gitlab.rails5? - !ActiveModel::Type::Boolean::FALSE_VALUES.include?(#{arg}) + !!ActiveRecord::Type::Boolean.new.cast(#{arg}) else - ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg}) + !!ActiveRecord::Type::Boolean.new.type_cast_from_database(#{arg}) end end } diff --git a/app/models/timelog.rb b/app/models/timelog.rb index e166cf69703..f4c5c581a11 100644 --- a/app/models/timelog.rb +++ b/app/models/timelog.rb @@ -2,8 +2,8 @@ class Timelog < ActiveRecord::Base validates :time_spent, :user, presence: true validate :issuable_id_is_present - belongs_to :issue - belongs_to :merge_request + belongs_to :issue, touch: true + belongs_to :merge_request, touch: true belongs_to :user def issuable diff --git a/app/models/user.rb b/app/models/user.rb index 0a838d34054..e219ab800ad 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -26,7 +26,7 @@ class User < ActiveRecord::Base ignore_column :authentication_token add_authentication_token_field :incoming_email_token - add_authentication_token_field :rss_token + add_authentication_token_field :feed_token default_value_for :admin, false default_value_for(:external) { Gitlab::CurrentSettings.user_default_external } @@ -1167,11 +1167,11 @@ class User < ActiveRecord::Base save end - # each existing user needs to have an `rss_token`. + # each existing user needs to have an `feed_token`. # we do this on read since migrating all existing users is not a feasible # solution. - def rss_token - ensure_rss_token! + def feed_token + ensure_feed_token! end def sync_attribute?(attribute) diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb index c7f7aa836bd..9a7aaf4ef32 100644 --- a/app/presenters/commit_status_presenter.rb +++ b/app/presenters/commit_status_presenter.rb @@ -1,11 +1,10 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated CALLOUT_FAILURE_MESSAGES = { unknown_failure: 'There is an unknown failure, please try again', - script_failure: 'There has been a script failure. Check the job log for more information', api_failure: 'There has been an API failure, please try again', stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again', runner_system_failure: 'There has been a runner system failure, please try again', - missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information' + missing_dependency_failure: 'There has been a missing dependency failure' }.freeze presents :build diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb index b22a0b666ef..77fc3336521 100644 --- a/app/serializers/cluster_application_entity.rb +++ b/app/serializers/cluster_application_entity.rb @@ -3,4 +3,5 @@ class ClusterApplicationEntity < Grape::Entity expose :status_name, as: :status expose :status_reason expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) } + expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) } end diff --git a/app/serializers/group_child_entity.rb b/app/serializers/group_child_entity.rb index 15ec0f89bb2..ee150eefd9e 100644 --- a/app/serializers/group_child_entity.rb +++ b/app/serializers/group_child_entity.rb @@ -31,7 +31,7 @@ class GroupChildEntity < Grape::Entity end # Project only attributes - expose :star_count, + expose :star_count, :archived, if: lambda { |_instance, _options| project? } # Group only attributes diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb index 3076fed1674..960e7291ae6 100644 --- a/app/serializers/job_entity.rb +++ b/app/serializers/job_entity.rb @@ -26,7 +26,7 @@ class JobEntity < Grape::Entity expose :created_at expose :updated_at expose :detailed_status, as: :status, with: StatusEntity - expose :callout_message, if: -> (*) { failed? } + expose :callout_message, if: -> (*) { failed? && !build.script_failure? } expose :recoverable, if: -> (*) { failed? } private diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index d0165c148eb..141070aef45 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -10,6 +10,7 @@ class MergeRequestWidgetEntity < IssuableEntity expose :merge_when_pipeline_succeeds expose :source_branch expose :source_project_id + expose :squash expose :target_branch expose :target_project_id expose :allow_maintainer_to_push diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index ac70a99c2c5..5a961ac89e4 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -63,7 +63,7 @@ module Boards def without_board_labels(issues) return issues unless board_label_ids.any? - issues.where.not(issues_label_links.limit(1).arel.exists) + issues.where.not('EXISTS (?)', issues_label_links.limit(1)) end def issues_label_links @@ -71,10 +71,8 @@ module Boards end def with_list_label(issues) - issues.where( - LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id") - .where("label_links.label_id = ?", list.label_id).limit(1).arel.exists - ) + issues.where('EXISTS (?)', LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id") + .where("label_links.label_id = ?", list.label_id).limit(1)) end end end diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index 4291631913a..317d1defbba 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -89,7 +89,10 @@ module Ci end def builds_for_group_runner - hierarchy_groups = Gitlab::GroupHierarchy.new(runner.groups).base_and_descendants + # Workaround for weird Rails bug, that makes `runner.groups.to_sql` to return `runner_id = NULL` + groups = Group.joins(:runner_namespaces).merge(runner.runner_namespaces) + + hierarchy_groups = Gitlab::GroupHierarchy.new(groups).base_and_descendants projects = Project.where(namespace_id: hierarchy_groups) .with_group_runners_enabled .with_builds_enabled diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb index 4c25a09814b..7ec3a9baa6e 100644 --- a/app/services/clusters/applications/install_service.rb +++ b/app/services/clusters/applications/install_service.rb @@ -12,8 +12,8 @@ module Clusters ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) rescue Kubeclient::HttpError => ke app.make_errored!("Kubernetes error: #{ke.message}") - rescue StandardError - app.make_errored!("Can't start installation process") + rescue StandardError => e + app.make_errored!("Can't start installation process. #{e.message}") end end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 1f67e3ecf9d..683f64e82ad 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -183,7 +183,10 @@ class IssuableBaseService < BaseService old_associations = associations_before_update(issuable) label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids) - params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids) + if labels_changing?(issuable.label_ids, label_ids) + params[:label_ids] = label_ids + issuable.touch + end if issuable.changed? || params.present? issuable.assign_attributes(params.merge(updated_by: current_user)) diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 2209a60a840..126da891c78 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -34,6 +34,19 @@ module MergeRequests handle_merge_error(log_message: e.message, save_message_on_model: true) end + def source + return merge_request.diff_head_sha unless merge_request.squash + + squash_result = ::MergeRequests::SquashService.new(project, current_user, params).execute(merge_request) + + case squash_result[:status] + when :success + squash_result[:squash_sha] + when :error + raise ::MergeRequests::MergeService::MergeError, squash_result[:message] + end + end + private def error_check! @@ -116,9 +129,5 @@ module MergeRequests def merge_request_info merge_request.to_reference(full: true) end - - def source - @source ||= @merge_request.diff_head_sha - end end end diff --git a/app/services/merge_requests/squash_service.rb b/app/services/merge_requests/squash_service.rb new file mode 100644 index 00000000000..a40fb2786bd --- /dev/null +++ b/app/services/merge_requests/squash_service.rb @@ -0,0 +1,28 @@ +module MergeRequests + class SquashService < MergeRequests::WorkingCopyBaseService + def execute(merge_request) + @merge_request = merge_request + @repository = target_project.repository + + squash || error('Failed to squash. Should be done manually.') + end + + def squash + if merge_request.commits_count < 2 + return success(squash_sha: merge_request.diff_head_sha) + end + + if merge_request.squash_in_progress? + return error('Squash task canceled: another squash is already in progress.') + end + + squash_sha = repository.squash(current_user, merge_request) + + success(squash_sha: squash_sha) + rescue => e + log_error("Failed to squash merge request #{merge_request.to_reference(full: true)}:") + log_error(e.message) + false + end + end +end diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index bdd9598f85a..00080717600 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -29,7 +29,7 @@ module Projects def add_repository_to_project if project.external_import? && !unknown_url? begin - Gitlab::UrlBlocker.validate!(project.import_url, valid_ports: Project::VALID_IMPORT_PORTS) + Gitlab::UrlBlocker.validate!(project.import_url, ports: Project::VALID_IMPORT_PORTS) rescue Gitlab::UrlBlocker::BlockedUrlError => e raise Error, "Blocked import URL: #{e.message}" end diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb index eb0472c6024..21741913385 100644 --- a/app/services/projects/participants_service.rb +++ b/app/services/projects/participants_service.rb @@ -5,14 +5,16 @@ module Projects def execute(noteable) @noteable = noteable - project_members = sorted(project.team.members) participants = noteable_owner + participants_in_noteable + all_members + groups + project_members participants.uniq end + def project_members + @project_members ||= sorted(project.team.members) + end + def all_members - count = project.team.members.flatten.count - [{ username: "all", name: "All Project and Group Members", count: count }] + [{ username: "all", name: "All Project and Group Members", count: project_members.count }] end end end diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb index 3fd27d9acdc..5bdca26a584 100644 --- a/app/uploaders/object_storage.rb +++ b/app/uploaders/object_storage.rb @@ -11,6 +11,7 @@ module ObjectStorage ObjectStorageUnavailable = Class.new(StandardError) DIRECT_UPLOAD_TIMEOUT = 4.hours + DIRECT_UPLOAD_EXPIRE_OFFSET = 15.minutes TMP_UPLOAD_PATH = 'tmp/uploads'.freeze module Store @@ -174,11 +175,12 @@ module ObjectStorage id = [CarrierWave.generate_cache_id, SecureRandom.hex].join('-') upload_path = File.join(TMP_UPLOAD_PATH, id) connection = ::Fog::Storage.new(self.object_store_credentials) - expire_at = Time.now + DIRECT_UPLOAD_TIMEOUT + expire_at = Time.now + DIRECT_UPLOAD_TIMEOUT + DIRECT_UPLOAD_EXPIRE_OFFSET options = { 'Content-Type' => 'application/octet-stream' } { ID: id, + Timeout: DIRECT_UPLOAD_TIMEOUT, GetURL: connection.get_object_url(remote_store_path, upload_path, expire_at), DeleteURL: connection.delete_object_url(remote_store_path, upload_path, expire_at), StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options) diff --git a/app/validators/addressable_url_validator.rb b/app/validators/addressable_url_validator.rb deleted file mode 100644 index 94542125d43..00000000000 --- a/app/validators/addressable_url_validator.rb +++ /dev/null @@ -1,45 +0,0 @@ -# AddressableUrlValidator -# -# Custom validator for URLs. This is a stricter version of UrlValidator - it also checks -# for using the right protocol, but it actually parses the URL checking for any syntax errors. -# The regex is also different from `URI` as we use `Addressable::URI` here. -# -# By default, only URLs for http, https, ssh, and git protocols will be considered valid. -# Provide a `:protocols` option to configure accepted protocols. -# -# Example: -# -# class User < ActiveRecord::Base -# validates :personal_url, addressable_url: true -# -# validates :ftp_url, addressable_url: { protocols: %w(ftp) } -# -# validates :git_url, addressable_url: { protocols: %w(http https ssh git) } -# end -# -class AddressableUrlValidator < ActiveModel::EachValidator - DEFAULT_OPTIONS = { protocols: %w(http https ssh git) }.freeze - - def validate_each(record, attribute, value) - unless valid_url?(value) - record.errors.add(attribute, "must be a valid URL") - end - end - - private - - def valid_url?(value) - return false unless value - - valid_protocol?(value) && valid_uri?(value) - end - - def valid_uri?(value) - Gitlab::UrlSanitizer.valid?(value) - end - - def valid_protocol?(value) - options = DEFAULT_OPTIONS.merge(self.options) - value =~ /\A#{URI.regexp(options[:protocols])}\z/ - end -end diff --git a/app/validators/importable_url_validator.rb b/app/validators/importable_url_validator.rb deleted file mode 100644 index 612d3c71913..00000000000 --- a/app/validators/importable_url_validator.rb +++ /dev/null @@ -1,11 +0,0 @@ -# ImportableUrlValidator -# -# This validator blocks projects from using dangerous import_urls to help -# protect against Server-side Request Forgery (SSRF). -class ImportableUrlValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - Gitlab::UrlBlocker.validate!(value, valid_ports: Project::VALID_IMPORT_PORTS) - rescue Gitlab::UrlBlocker::BlockedUrlError => e - record.errors.add(attribute, "is blocked: #{e.message}") - end -end diff --git a/app/validators/public_url_validator.rb b/app/validators/public_url_validator.rb new file mode 100644 index 00000000000..1e8118fccbb --- /dev/null +++ b/app/validators/public_url_validator.rb @@ -0,0 +1,26 @@ +# PublicUrlValidator +# +# Custom validator for URLs. This validator works like UrlValidator but +# it blocks by default urls pointing to localhost or the local network. +# +# This validator accepts the same params UrlValidator does. +# +# Example: +# +# class User < ActiveRecord::Base +# validates :personal_url, public_url: true +# +# validates :ftp_url, public_url: { protocols: %w(ftp) } +# +# validates :git_url, public_url: { allow_localhost: true, allow_local_network: true} +# end +# +class PublicUrlValidator < UrlValidator + private + + def default_options + # By default block all urls pointing to localhost or the local network + super.merge(allow_localhost: false, + allow_local_network: false) + end +end diff --git a/app/validators/url_placeholder_validator.rb b/app/validators/url_placeholder_validator.rb deleted file mode 100644 index dd681218b6b..00000000000 --- a/app/validators/url_placeholder_validator.rb +++ /dev/null @@ -1,32 +0,0 @@ -# UrlValidator -# -# Custom validator for URLs. -# -# By default, only URLs for the HTTP(S) protocols will be considered valid. -# Provide a `:protocols` option to configure accepted protocols. -# -# Also, this validator can help you validate urls with placeholders inside. -# Usually, if you have a url like 'http://www.example.com/%{project_path}' the -# URI parser will reject that URL format. Provide a `:placeholder_regex` option -# to configure accepted placeholders. -# -# Example: -# -# class User < ActiveRecord::Base -# validates :personal_url, url: true -# -# validates :ftp_url, url: { protocols: %w(ftp) } -# -# validates :git_url, url: { protocols: %w(http https ssh git) } -# -# validates :placeholder_url, url: { placeholder_regex: /(project_path|project_id|default_branch)/ } -# end -# -class UrlPlaceholderValidator < UrlValidator - def validate_each(record, attribute, value) - placeholder_regex = self.options[:placeholder_regex] - value = value.gsub(/%{#{placeholder_regex}}/, 'foo') if placeholder_regex && value - - super(record, attribute, value) - end -end diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb index a77beb2683d..8648c4c75e3 100644 --- a/app/validators/url_validator.rb +++ b/app/validators/url_validator.rb @@ -15,25 +15,63 @@ # validates :git_url, url: { protocols: %w(http https ssh git) } # end # +# This validator can also block urls pointing to localhost or the local network to +# protect against Server-side Request Forgery (SSRF), or check for the right port. +# +# Example: +# class User < ActiveRecord::Base +# validates :personal_url, url: { allow_localhost: false, allow_local_network: false} +# +# validates :web_url, url: { ports: [80, 443] } +# end class UrlValidator < ActiveModel::EachValidator + DEFAULT_PROTOCOLS = %w(http https).freeze + + attr_reader :record + def validate_each(record, attribute, value) - unless valid_url?(value) + @record = record + + if value.present? + value.strip! + else record.errors.add(attribute, "must be a valid URL") end + + Gitlab::UrlBlocker.validate!(value, blocker_args) + rescue Gitlab::UrlBlocker::BlockedUrlError => e + record.errors.add(attribute, "is blocked: #{e.message}") end private def default_options - @default_options ||= { protocols: %w(http https) } + # By default the validator doesn't block any url based on the ip address + { + protocols: DEFAULT_PROTOCOLS, + ports: [], + allow_localhost: true, + allow_local_network: true + } end - def valid_url?(value) - return false if value.nil? + def current_options + options = self.options.map do |option, value| + [option, value.is_a?(Proc) ? value.call(record) : value] + end.to_h + + default_options.merge(options) + end - options = default_options.merge(self.options) + def blocker_args + current_options.slice(:allow_localhost, :allow_local_network, :protocols, :ports).tap do |args| + if allow_setting_local_requests? + args[:allow_localhost] = args[:allow_local_network] = true + end + end + end - value.strip! - value =~ /\A#{URI.regexp(options[:protocols])}\z/ + def allow_setting_local_requests? + ApplicationSetting.current&.allow_local_requests_from_hooks_and_services? end end diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index f40dd347eb3..07f9ea0865b 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :gravatar_enabled do - = f.check_box :gravatar_enabled + = f.check_box :gravatar_enabled, class: 'form-check-input' + = f.label :gravatar_enabled, class: 'form-check-label' do Gravatar enabled .form-group.row = f.label :default_projects_limit, class: 'col-form-label col-sm-2' @@ -25,15 +25,15 @@ = f.label :user_oauth_applications, 'User OAuth applications', class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.label :user_oauth_applications do - = f.check_box :user_oauth_applications + = f.check_box :user_oauth_applications, class: 'form-check-input' + = f.label :user_oauth_applications, class: 'form-check-label' do Allow users to register any application to use GitLab as an OAuth provider .form-group.row = f.label :user_default_external, 'New users set to external', class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.label :user_default_external do - = f.check_box :user_default_external + = f.check_box :user_default_external, class: 'form-check-input' + = f.label :user_default_external, class: 'form-check-label' do Newly registered users will by default be external = f.submit 'Save changes', class: 'btn btn-success' diff --git a/app/views/admin/application_settings/_background_jobs.html.haml b/app/views/admin/application_settings/_background_jobs.html.haml index da7248337d2..fc5df02242a 100644 --- a/app/views/admin/application_settings/_background_jobs.html.haml +++ b/app/views/admin/application_settings/_background_jobs.html.haml @@ -9,8 +9,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :sidekiq_throttling_enabled do - = f.check_box :sidekiq_throttling_enabled + = f.check_box :sidekiq_throttling_enabled, class: 'form-check-input' + = f.label :sidekiq_throttling_enabled, class: 'form-check-label' do Enable Sidekiq Job Throttling .form-text.text-muted Limit the amount of resources slow running jobs are assigned. diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml index 6a06271de2a..233821818e6 100644 --- a/app/views/admin/application_settings/_ci_cd.html.haml +++ b/app/views/admin/application_settings/_ci_cd.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :auto_devops_enabled do - = f.check_box :auto_devops_enabled + = f.check_box :auto_devops_enabled, class: 'form-check-input' + = f.label :auto_devops_enabled, class: 'form-check-label' do Enabled Auto DevOps for projects by default .form-text.text-muted It will automatically build, test, and deploy applications based on a predefined CI/CD configuration @@ -20,8 +20,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :shared_runners_enabled do - = f.check_box :shared_runners_enabled + = f.check_box :shared_runners_enabled, class: 'form-check-input' + = f.label :shared_runners_enabled, class: 'form-check-label' do Enable shared runners for new projects .form-group.row = f.label :shared_runners_text, class: 'col-form-label col-sm-2' diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml index 7443f02ad15..01be5878a60 100644 --- a/app/views/admin/application_settings/_email.html.haml +++ b/app/views/admin/application_settings/_email.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :email_author_in_body do - = f.check_box :email_author_in_body + = f.check_box :email_author_in_body, class: 'form-check-input' + = f.label :email_author_in_body, class: 'form-check-label' do Include author name in notification email body .form-text.text-muted Some email servers do not support overriding the email sender name. @@ -15,8 +15,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :html_emails_enabled do - = f.check_box :html_emails_enabled + = f.check_box :html_emails_enabled, class: 'form-check-input' + = f.label :html_emails_enabled, class: 'form-check-label' do Enable HTML emails .form-text.text-muted By default GitLab sends emails in HTML and plain text formats so mail diff --git a/app/views/admin/application_settings/_help_page.html.haml b/app/views/admin/application_settings/_help_page.html.haml index 97d397788c6..1f6c52d8b1a 100644 --- a/app/views/admin/application_settings/_help_page.html.haml +++ b/app/views/admin/application_settings/_help_page.html.haml @@ -10,8 +10,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :help_page_hide_commercial_content do - = f.check_box :help_page_hide_commercial_content + = f.check_box :help_page_hide_commercial_content, class: 'form-check-input' + = f.label :help_page_hide_commercial_content, class: 'form-check-label' do Hide marketing-related entries from help .form-group.row = f.label :help_page_support_url, 'Support page URL', class: 'col-form-label col-sm-2' diff --git a/app/views/admin/application_settings/_influx.html.haml b/app/views/admin/application_settings/_influx.html.haml index e5d699f9a2f..b40a714ed8f 100644 --- a/app/views/admin/application_settings/_influx.html.haml +++ b/app/views/admin/application_settings/_influx.html.haml @@ -11,8 +11,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :metrics_enabled do - = f.check_box :metrics_enabled + = f.check_box :metrics_enabled, class: 'form-check-input' + = f.label :metrics_enabled, class: 'form-check-label' do Enable InfluxDB Metrics .form-group.row = f.label :metrics_host, 'InfluxDB host', class: 'col-form-label col-sm-2' diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml index 539ff9b5168..320dd52ffc2 100644 --- a/app/views/admin/application_settings/_ip_limits.html.haml +++ b/app/views/admin/application_settings/_ip_limits.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :throttle_unauthenticated_enabled do - = f.check_box :throttle_unauthenticated_enabled + = f.check_box :throttle_unauthenticated_enabled, class: 'form-check-input' + = f.label :throttle_unauthenticated_enabled, class: 'form-check-label' do Enable unauthenticated request rate limit %span.form-text.text-muted Helps reduce request volume (e.g. from crawlers or abusive bots) @@ -21,8 +21,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :throttle_authenticated_api_enabled do - = f.check_box :throttle_authenticated_api_enabled + = f.check_box :throttle_authenticated_api_enabled, class: 'form-check-input' + = f.label :throttle_authenticated_api_enabled, class: 'form-check-label' do Enable authenticated API request rate limit %span.form-text.text-muted Helps reduce request volume (e.g. from crawlers or abusive bots) @@ -37,8 +37,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :throttle_authenticated_web_enabled do - = f.check_box :throttle_authenticated_web_enabled + = f.check_box :throttle_authenticated_web_enabled, class: 'form-check-input' + = f.label :throttle_authenticated_web_enabled, class: 'form-check-label' do Enable authenticated web request rate limit %span.form-text.text-muted Helps reduce request volume (e.g. from crawlers or abusive bots) diff --git a/app/views/admin/application_settings/_koding.html.haml b/app/views/admin/application_settings/_koding.html.haml index 0532f49fd5b..341c7641fcc 100644 --- a/app/views/admin/application_settings/_koding.html.haml +++ b/app/views/admin/application_settings/_koding.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :koding_enabled do - = f.check_box :koding_enabled + = f.check_box :koding_enabled, class: 'form-check-input' + = f.label :koding_enabled, class: 'form-check-label' do Enable Koding .form-text.text-muted Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again. diff --git a/app/views/admin/application_settings/_logging.html.haml b/app/views/admin/application_settings/_logging.html.haml index 4341801b045..f5c1e126c70 100644 --- a/app/views/admin/application_settings/_logging.html.haml +++ b/app/views/admin/application_settings/_logging.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :sentry_enabled do - = f.check_box :sentry_enabled + = f.check_box :sentry_enabled, class: 'form-check-input' + = f.label :sentry_enabled, class: 'form-check-label' do Enable Sentry .form-text.text-muted %p This setting requires a restart to take effect. @@ -21,8 +21,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :clientside_sentry_enabled do - = f.check_box :clientside_sentry_enabled + = f.check_box :clientside_sentry_enabled, class: 'form-check-input' + = f.label :clientside_sentry_enabled, class: 'form-check-label' do Enable Clientside Sentry .form-text.text-muted Sentry can also be used for reporting and logging clientside exceptions. diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml index 176cdfe9e1a..5dadb7b814b 100644 --- a/app/views/admin/application_settings/_outbound.html.haml +++ b/app/views/admin/application_settings/_outbound.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :allow_local_requests_from_hooks_and_services do - = f.check_box :allow_local_requests_from_hooks_and_services + = f.check_box :allow_local_requests_from_hooks_and_services, class: 'form-check-input' + = f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do Allow requests to the local network from hooks and services = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml index 0100ef038e2..f1889c3105f 100644 --- a/app/views/admin/application_settings/_pages.html.haml +++ b/app/views/admin/application_settings/_pages.html.haml @@ -10,8 +10,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :pages_domain_verification_enabled do - = f.check_box :pages_domain_verification_enabled + = f.check_box :pages_domain_verification_enabled, class: 'form-check-input' + = f.label :pages_domain_verification_enabled, class: 'form-check-label' do Require users to prove ownership of custom domains .form-text.text-muted Domain verification is an essential security measure for public GitLab diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml index 07d3a8b7cdf..57c22ce563f 100644 --- a/app/views/admin/application_settings/_performance.html.haml +++ b/app/views/admin/application_settings/_performance.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :authorized_keys_enabled do - = f.check_box :authorized_keys_enabled + = f.check_box :authorized_keys_enabled, class: 'form-check-input' + = f.label :authorized_keys_enabled, class: 'form-check-label' do Write to "authorized_keys" file .form-text.text-muted By default, we write to the "authorized_keys" file to support Git diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml index 030e8610b47..ed4de2234f7 100644 --- a/app/views/admin/application_settings/_performance_bar.html.haml +++ b/app/views/admin/application_settings/_performance_bar.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :performance_bar_enabled do - = f.check_box :performance_bar_enabled + = f.check_box :performance_bar_enabled, class: 'form-check-input' + = f.label :performance_bar_enabled, class: 'form-check-label' do Enable the Performance Bar .form-group.row = f.label :performance_bar_allowed_group_path, 'Allowed group', class: 'col-form-label col-sm-2' diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml index ee6d1d1a888..e0dc058762e 100644 --- a/app/views/admin/application_settings/_plantuml.html.haml +++ b/app/views/admin/application_settings/_plantuml.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :plantuml_enabled do - = f.check_box :plantuml_enabled + = f.check_box :plantuml_enabled, class: 'form-check-input' + = f.label :plantuml_enabled, class: 'form-check-label' do Enable PlantUML .form-group.row = f.label :plantuml_url, 'PlantUML URL', class: 'col-form-label col-sm-2' diff --git a/app/views/admin/application_settings/_prometheus.html.haml b/app/views/admin/application_settings/_prometheus.html.haml index 8c95597e787..d3c3656e96a 100644 --- a/app/views/admin/application_settings/_prometheus.html.haml +++ b/app/views/admin/application_settings/_prometheus.html.haml @@ -14,8 +14,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :prometheus_metrics_enabled do - = f.check_box :prometheus_metrics_enabled + = f.check_box :prometheus_metrics_enabled, class: 'form-check-input' + = f.label :prometheus_metrics_enabled, class: 'form-check-label' do Enable Prometheus Metrics - unless Gitlab::Metrics.metrics_folder_present? .form-text.text-muted diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml index 739ed359e16..1311f17ecda 100644 --- a/app/views/admin/application_settings/_repository_check.html.haml +++ b/app/views/admin/application_settings/_repository_check.html.haml @@ -7,8 +7,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :repository_checks_enabled do - = f.check_box :repository_checks_enabled + = f.check_box :repository_checks_enabled, class: 'form-check-input' + = f.label :repository_checks_enabled, class: 'form-check-label' do Enable Repository Checks .form-text.text-muted GitLab will periodically run @@ -25,8 +25,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :housekeeping_enabled do - = f.check_box :housekeeping_enabled + = f.check_box :housekeeping_enabled, class: 'form-check-input' + = f.label :housekeeping_enabled, class: 'form-check-label' do Enable automatic repository housekeeping (git repack, git gc) .form-text.text-muted If you keep automatic housekeeping disabled for a long time Git @@ -34,8 +34,8 @@ repositories will use more disk space. We recommend to always leave this enabled. .form-check - = f.label :housekeeping_bitmaps_enabled do - = f.check_box :housekeeping_bitmaps_enabled + = f.check_box :housekeeping_bitmaps_enabled, class: 'form-check-input' + = f.label :housekeeping_bitmaps_enabled, class: 'form-check-label' do Enable Git pack file bitmap creation .form-text.text-muted Creating pack file bitmaps makes housekeeping take a little longer but diff --git a/app/views/admin/application_settings/_repository_mirrors_form.html.haml b/app/views/admin/application_settings/_repository_mirrors_form.html.haml index edd8e5e9eb8..187c6c28bb1 100644 --- a/app/views/admin/application_settings/_repository_mirrors_form.html.haml +++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml @@ -1,13 +1,13 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path do |f| = form_errors(@application_setting) %fieldset - .form-group - = f.label :mirror_available, 'Enable mirror configuration', class: 'control-label col-sm-2' - .col-sm-10 + .form-group.row + = f.label :mirror_available, 'Enable mirror configuration', class: 'control-label col-sm-4' + .col-sm-8 .form-check - = f.label :mirror_available do - = f.check_box :mirror_available + = f.check_box :mirror_available, class: 'form-check-input' + = f.label :mirror_available, class: 'form-check-label' do Allow mirrors to be setup for projects %span.form-text.text-muted If disabled, only admins will be able to setup mirrors in projects. diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml index be85210934c..89d2c114b22 100644 --- a/app/views/admin/application_settings/_repository_storage.html.haml +++ b/app/views/admin/application_settings/_repository_storage.html.haml @@ -6,8 +6,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :hashed_storage_enabled do - = f.check_box :hashed_storage_enabled + = f.check_box :hashed_storage_enabled, class: 'form-check-input' + = f.label :hashed_storage_enabled, class: 'form-check-label' do Create new projects using hashed storage paths .form-text.text-muted Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml index 83a30504222..2ba26158162 100644 --- a/app/views/admin/application_settings/_signin.html.haml +++ b/app/views/admin/application_settings/_signin.html.haml @@ -5,16 +5,16 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :password_authentication_enabled_for_web do - = f.check_box :password_authentication_enabled_for_web + = f.check_box :password_authentication_enabled_for_web, class: 'form-check-input' + = f.label :password_authentication_enabled_for_web, class: 'form-check-label' do Password authentication enabled for web interface .form-text.text-muted When disabled, an external authentication provider must be used. .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :password_authentication_enabled_for_git do - = f.check_box :password_authentication_enabled_for_git + = f.check_box :password_authentication_enabled_for_git, class: 'form-check-input' + = f.label :password_authentication_enabled_for_git, class: 'form-check-label' do Password authentication enabled for Git over HTTP(S) .form-text.text-muted When disabled, a Personal Access Token @@ -33,8 +33,8 @@ = f.label :two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.label :require_two_factor_authentication do - = f.check_box :require_two_factor_authentication + = f.check_box :require_two_factor_authentication, class: 'form-check-input' + = f.label :require_two_factor_authentication, class: 'form-check-label' do Require all users to setup Two-factor authentication .form-group.row = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'col-form-label col-sm-2' diff --git a/app/views/admin/application_settings/_signup.html.haml b/app/views/admin/application_settings/_signup.html.haml index 50c455f8686..279f96389e9 100644 --- a/app/views/admin/application_settings/_signup.html.haml +++ b/app/views/admin/application_settings/_signup.html.haml @@ -5,14 +5,14 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :signup_enabled do - = f.check_box :signup_enabled + = f.check_box :signup_enabled, class: 'form-check-input' + = f.label :signup_enabled, class: 'form-check-label' do Sign-up enabled .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :send_user_confirmation_email do - = f.check_box :send_user_confirmation_email + = f.check_box :send_user_confirmation_email, class: 'form-check-input' + = f.label :send_user_confirmation_email, class: 'form-check-label' do Send confirmation email on sign-up .form-group.row = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'col-form-label col-sm-2' @@ -23,19 +23,19 @@ = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.label :domain_blacklist_enabled do - = f.check_box :domain_blacklist_enabled + = f.check_box :domain_blacklist_enabled, class: 'form-check-input' + = f.label :domain_blacklist_enabled, class: 'form-check-label' do Enable domain blacklist for sign ups .form-group.row .offset-sm-2.col-sm-10 .form-check - = label_tag :blacklist_type_file do - = radio_button_tag :blacklist_type, :file + = radio_button_tag :blacklist_type, :file, class: 'form-check-input' + = label_tag :blacklist_type_file, class: 'form-check-label' do .option-title Upload blacklist file .form-check - = label_tag :blacklist_type_raw do - = radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank? + = radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank?, class: 'form-check-input' + = label_tag :blacklist_type_raw, class: 'form-check-label' do .option-title Enter blacklist manually .form-group.row.blacklist-file diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml index 58543a0359a..fb38e4ae922 100644 --- a/app/views/admin/application_settings/_spam.html.haml +++ b/app/views/admin/application_settings/_spam.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :recaptcha_enabled do - = f.check_box :recaptcha_enabled + = f.check_box :recaptcha_enabled, class: 'form-check-input' + = f.label :recaptcha_enabled, class: 'form-check-label' do Enable reCAPTCHA %span.form-text.text-muted#recaptcha_help_block Helps prevent bots from creating accounts @@ -26,8 +26,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :akismet_enabled do - = f.check_box :akismet_enabled + = f.check_box :akismet_enabled, class: 'form-check-input' + = f.label :akismet_enabled, class: 'form-check-label' do Enable Akismet %span.form-text.text-muted#akismet_help_block Helps prevent bots from creating issues @@ -42,8 +42,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :unique_ips_limit_enabled do - = f.check_box :unique_ips_limit_enabled + = f.check_box :unique_ips_limit_enabled, class: 'form-check-input' + = f.label :unique_ips_limit_enabled, class: 'form-check-label' do Limit sign in from multiple ips %span.form-text.text-muted#unique_ip_help_block Helps prevent malicious users hide their activity diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml index 44bf7b65a8e..257565ce193 100644 --- a/app/views/admin/application_settings/_terms.html.haml +++ b/app/views/admin/application_settings/_terms.html.haml @@ -1,16 +1,16 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| += form_for @application_setting, url: admin_application_settings_path do |f| = form_errors(@application_setting) %fieldset - .form-group + .form-group.row .col-sm-12 .form-check - = f.label :enforce_terms do - = f.check_box :enforce_terms + = f.check_box :enforce_terms, class: 'form-check-input' + = f.label :enforce_terms, class: 'form-check-label' do = _("Require all users to accept Terms of Service when they access GitLab.") .form-text.text-muted = _("When enabled, users cannot use GitLab until the terms have been accepted.") - .form-group + .form-group.row .col-sm-12 = f.label :terms do = _("Terms of Service Agreement") diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml index 316c8b04dea..c110fd4d60d 100644 --- a/app/views/admin/application_settings/_usage.html.haml +++ b/app/views/admin/application_settings/_usage.html.haml @@ -5,8 +5,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :version_check_enabled do - = f.check_box :version_check_enabled + = f.check_box :version_check_enabled, class: 'form-check-input' + = f.label :version_check_enabled, class: 'form-check-label' do Enable version check .form-text.text-muted GitLab will inform you if a new version is available. @@ -16,8 +16,8 @@ .offset-sm-2.col-sm-10 - can_be_configured = @application_setting.usage_ping_can_be_configured? .form-check - = f.label :usage_ping_enabled do - = f.check_box :usage_ping_enabled, disabled: !can_be_configured + = f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input' + = f.label :usage_ping_enabled, class: 'form-check-label' do Enable usage ping .form-text.text-muted - if can_be_configured diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml index 0f2524047e3..2b4d3bab54d 100644 --- a/app/views/admin/application_settings/_visibility_and_access.html.haml +++ b/app/views/admin/application_settings/_visibility_and_access.html.haml @@ -46,8 +46,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = f.label :project_export_enabled do - = f.check_box :project_export_enabled + = f.check_box :project_export_enabled, class: 'form-check-input' + = f.label :project_export_enabled, class: 'form-check-label' do Project export enabled .form-group.row diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml index b5d79e1da13..7f14cddebd8 100644 --- a/app/views/admin/applications/_form.html.haml +++ b/app/views/admin/applications/_form.html.haml @@ -1,13 +1,13 @@ = form_for [:admin, @application], url: @url, html: {role: 'form'} do |f| = form_errors(application) - = content_tag :div, class: 'form-group' do + = content_tag :div, class: 'form-group row' do = f.label :name, class: 'col-sm-2 col-form-label' .col-sm-10 = f.text_field :name, class: 'form-control' = doorkeeper_errors_for application, :name - = content_tag :div, class: 'form-group' do + = content_tag :div, class: 'form-group row' do = f.label :redirect_uri, class: 'col-sm-2 col-form-label' .col-sm-10 = f.text_area :redirect_uri, class: 'form-control' @@ -20,7 +20,7 @@ %code= Doorkeeper.configuration.native_redirect_uri for local tests - = content_tag :div, class: 'form-group' do + = content_tag :div, class: 'form-group row' do = f.label :trusted, class: 'col-sm-2 col-form-label' .col-sm-10 = f.check_box :trusted diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 5ec612d0c72..6d75ccd5add 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -13,7 +13,7 @@ .card .card-header Group info: - %ul.well-list + %ul.content-list %li .avatar-container.s60 = group_icon(@group, class: "avatar s60") @@ -64,7 +64,7 @@ Projects %span.badge.badge-pill #{@group.projects.count} - %ul.well-list + %ul.content-list - @projects.each do |project| %li %strong @@ -82,7 +82,7 @@ Projects shared with #{@group.name} %span.badge.badge-pill #{@group.shared_projects.count} - %ul.well-list + %ul.content-list - @group.shared_projects.sort_by(&:name).each do |project| %li %strong @@ -118,7 +118,7 @@ %span.badge.badge-pill= @group.members.size .float-right = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@group, :members]), class: "btn btn-sm" - %ul.well-list.group-users-list.content-list.members-list + %ul.content-list.group-users-list.content-list.members-list = render partial: 'shared/members/member', collection: @members, as: :member, locals: { show_controls: false } .card-footer = paginate @members, param_name: 'members_page', theme: 'gitlab' diff --git a/app/views/admin/hooks/_form.html.haml b/app/views/admin/hooks/_form.html.haml index e54dbd20ef4..3abde755f0f 100644 --- a/app/views/admin/hooks/_form.html.haml +++ b/app/views/admin/hooks/_form.html.haml @@ -47,6 +47,6 @@ .form-group = form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox' .form-check - = form.label :enable_ssl_verification do - = form.check_box :enable_ssl_verification + = form.check_box :enable_ssl_verification, class: 'form-check-input' + = form.label :enable_ssl_verification, class: 'form-check-label' do %strong Enable SSL verification diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml index ee51b44e83e..7637471f9ae 100644 --- a/app/views/admin/labels/_form.html.haml +++ b/app/views/admin/labels/_form.html.haml @@ -19,7 +19,7 @@ .form-text.text-muted Choose any color. %br - Or you can choose one of suggested colors below + Or you can choose one of the suggested colors below .suggest-colors - suggested_colors.each do |color| diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index a6c436cd1f4..e4c0382a437 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -4,8 +4,8 @@ %div{ class: container_class } %ul.nav-links.log-tabs.nav.nav-tabs - @loggers.each do |klass| - %li{ class: active_when(klass == @loggers.first) }> - = link_to klass.file_name, "##{klass.file_name_noext}", data: { toggle: 'tab' } + %li.nav-item + = link_to klass.file_name, "##{klass.file_name_noext}", data: { toggle: 'tab' }, class: "#{active_when(klass == @loggers.first)} nav-link" .row-content-block To prevent performance issues admin logs output the last 2000 lines .tab-content diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 29ec712b6b7..0a22a142858 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -22,7 +22,7 @@ .card .card-header Project info: - %ul.well-list + %ul.content-list %li %span.light Name: %strong @@ -166,7 +166,7 @@ .float-right = link_to admin_group_path(@group), class: 'btn btn-sm' do = icon('pencil-square-o', text: 'Manage access') - %ul.well-list.content-list.members-list + %ul.content-list.members-list = render partial: 'shared/members/member', collection: @group_members, as: :member, locals: { show_controls: false } .card-footer = paginate @group_members, param_name: 'group_members_page', theme: 'gitlab' @@ -180,7 +180,7 @@ %span.badge.badge-pill= @project.users.size .float-right = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@project, :members]), class: "btn btn-sm" - %ul.well-list.project_members.content-list.members-list + %ul.content-list.project_members.members-list = render partial: 'shared/members/member', collection: @project_members, as: :member, locals: { show_controls: false } .card-footer = paginate @project_members, param_name: 'project_members_page', theme: 'gitlab' diff --git a/app/views/admin/users/_profile.html.haml b/app/views/admin/users/_profile.html.haml index af22652e07c..4fcb9aad343 100644 --- a/app/views/admin/users/_profile.html.haml +++ b/app/views/admin/users/_profile.html.haml @@ -1,7 +1,7 @@ .card .card-header Profile - %ul.well-list + %ul.content-list %li %span.light Member since %strong= user.created_at.to_s(:medium) diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml index 469a7bd9715..cf50d45f755 100644 --- a/app/views/admin/users/projects.html.haml +++ b/app/views/admin/users/projects.html.haml @@ -4,7 +4,7 @@ - if @user.groups.any? .card .card-header Group projects - %ul.card-body-list + %ul.hover-list - @user.group_members.includes(:source).each do |group_member| - group = group_member.group %li.group_member @@ -28,7 +28,7 @@ .col-md-6 .card .card-header Joined projects (#{@joined_projects.count}) - %ul.card-body-list + %ul.hover-list - @joined_projects.sort_by(&:full_name).each do |project| - member = project.team.find_member(@user.id) %li.project_member diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index a74fcea65d8..b0562226f5f 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -8,7 +8,7 @@ .card .card-header = @user.name - %ul.well-list + %ul.content-list %li = image_tag avatar_icon_for_user(@user, 60), class: "avatar s60" %li @@ -21,7 +21,7 @@ .card .card-header Account: - %ul.well-list + %ul.content-list %li %span.light Name: %strong= @user.name diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index 4bf04dadf01..86a21e24ac9 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -7,8 +7,7 @@ .top-area = render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set .nav-controls - = link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe' do - = icon('rss') + = render 'shared/issuable/feed_buttons' = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues = render 'shared/issuable/filter', type: :issues diff --git a/app/views/dashboard/issues_calendar.ics.haml b/app/views/dashboard/issues_calendar.ics.haml new file mode 100644 index 00000000000..59573e5fecf --- /dev/null +++ b/app/views/dashboard/issues_calendar.ics.haml @@ -0,0 +1 @@ += render 'issues/issues_calendar', issues: @issues diff --git a/app/views/errors/_footer.html.haml b/app/views/errors/_footer.html.haml new file mode 100644 index 00000000000..e67a3a142f6 --- /dev/null +++ b/app/views/errors/_footer.html.haml @@ -0,0 +1,11 @@ +%nav + %ul.error-nav + %li + = link_to s_('Nav|Home'), root_path + %li + - if current_user + = link_to s_('Nav|Sign out and sign in with a different account'), destroy_user_session_path + - else + = link_to s_('Nav|Sign In / Register'), new_session_path(:user, redirect_to_referer: 'yes') + %li + = link_to s_('Nav|Help'), help_path diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml index bf540439c79..227c7884915 100644 --- a/app/views/errors/access_denied.html.haml +++ b/app/views/errors/access_denied.html.haml @@ -1,15 +1,16 @@ - message = local_assigns.fetch(:message) - - content_for(:title, 'Access Denied') -%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') } - %h1 - 403 + += image_tag('illustrations/error-403.svg', alt: '403', lazy: false) .container - %h3 Access Denied - %hr + %h3 + = s_("403|You don't have the permission to access this page.") - if message %p = message - - else - %p You are not allowed to access this page. - %p Read more about project permissions #{link_to "here", help_page_path("user/permissions"), class: "vlink"} + %p + = s_('403|Please contact your GitLab administrator to get the permission.') + .action-container.js-go-back{ style: 'display: none' } + %a{ href: 'javascript:history.back()', class: 'btn btn-success' } + = s_('Go Back') += render "errors/footer" diff --git a/app/views/errors/not_found.html.haml b/app/views/errors/not_found.html.haml index a0b9a632e22..ae055f398ac 100644 --- a/app/views/errors/not_found.html.haml +++ b/app/views/errors/not_found.html.haml @@ -1,8 +1,15 @@ - content_for(:title, 'Not Found') -%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') } - %h1 - 404 += image_tag('illustrations/error-404.svg', alt: '404', lazy: false) .container - %h3 The resource you were looking for doesn't exist. - %hr - %p You may have mistyped the address or the page may have moved. + %h3 + = s_('404|Page Not Found') + %p + = s_("404|Make sure the address is correct and the page hasn't moved.") + %p + = s_('404|Please contact your GitLab administrator if you think this is a mistake.') + .action-container + = form_tag search_path, method: :get, class: 'form-inline-flex' do |f| + .field + = search_field_tag :search, '', placeholder: _('Search for projects, issues, etc.'), class: 'form-control' + = button_tag 'Search', class: 'btn btn-success', name: nil, type: 'submit' += render 'errors/footer' diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index f85f5c5be88..85f2d00bde3 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -14,7 +14,7 @@ - if event.push_with_commits? .event-body - %ul.well-list.event_commits + %ul.content-list.event_commits = render "events/commit", project: project, event: event - create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user) diff --git a/app/views/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml index 9a3ff0313b5..f950968030f 100644 --- a/app/views/groups/_create_chat_team.html.haml +++ b/app/views/groups/_create_chat_team.html.haml @@ -5,8 +5,8 @@ Mattermost .col-sm-10 .form-check.js-toggle-container - = f.label :create_chat_team do - .js-toggle-button= f.check_box(:create_chat_team, { checked: true }, true, false) + .js-toggle-button.form-check-input= f.check_box(:create_chat_team, { checked: true }, true, false) + = f.label :create_chat_team, class: 'form-check-label' do Create a Mattermost team for this group %br %small.light.js-toggle-content diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml index 3cd3fb32b9c..f7cc62c6929 100644 --- a/app/views/groups/_group_admin_settings.html.haml +++ b/app/views/groups/_group_admin_settings.html.haml @@ -2,8 +2,8 @@ = f.label :lfs_enabled, 'Large File Storage', class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.label :lfs_enabled do - = f.check_box :lfs_enabled, checked: @group.lfs_enabled? + = f.check_box :lfs_enabled, checked: @group.lfs_enabled?, class: 'form-check-input' + = f.label :lfs_enabled, class: 'form-check-label' do %strong Allow projects within this group to use Git LFS = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') @@ -14,8 +14,8 @@ = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.label :require_two_factor_authentication do - = f.check_box :require_two_factor_authentication + = f.check_box :require_two_factor_authentication, class: 'form-check-input' + = f.label :require_two_factor_authentication, class: 'form-check-label' do %strong Require all users in this group to setup Two-factor authentication = link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group') diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 662db18cf86..8037cf4b69d 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -8,10 +8,7 @@ .top-area = render 'shared/issuable/nav', type: :issues .nav-controls - = link_to safe_params.merge(rss_url_options), class: 'btn' do - = icon('rss') - %span.icon-label - Subscribe + = render 'shared/issuable/feed_buttons' = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues = render 'shared/issuable/search_bar', type: :issues diff --git a/app/views/groups/issues_calendar.ics.haml b/app/views/groups/issues_calendar.ics.haml new file mode 100644 index 00000000000..59573e5fecf --- /dev/null +++ b/app/views/groups/issues_calendar.ics.haml @@ -0,0 +1 @@ += render 'issues/issues_calendar', issues: @issues diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index cd07b95155c..ba186875a86 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -8,7 +8,7 @@ .controls = link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do New project - %ul.well-list + %ul.content-list - @projects.each do |project| %li .list-item-name diff --git a/app/views/groups/runners/_runner.html.haml b/app/views/groups/runners/_runner.html.haml index 76650a961d6..3f89b04a5fc 100644 --- a/app/views/groups/runners/_runner.html.haml +++ b/app/views/groups/runners/_runner.html.haml @@ -8,13 +8,13 @@ = link_to edit_group_runner_path(@group, runner) do = icon('edit') - .pull-right + .float-right - if runner.active? = link_to _('Pause'), pause_group_runner_path(@group, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: _("Are you sure?") } - else = link_to _('Resume'), resume_group_runner_path(@group, runner), method: :post, class: 'btn btn-success btn-sm' = link_to _('Remove Runner'), group_runner_path(@group, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm' - .pull-right + .float-right %small.light \##{runner.id} - if runner.description.present? diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index 15a5ecf791c..f1f67af1d1e 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -13,8 +13,8 @@ = s_('GroupSettings|Share with group lock') .col-sm-10 .form-check - = f.label :share_with_group_lock do - = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group) + = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'form-check-input' + = f.label :share_with_group_lock, class: 'form-check-label' do %strong - group_link = link_to @group.name, group_path(@group) = s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link } diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 9a3a03a7671..29db29235c1 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -2,11 +2,12 @@ .modal-dialog.modal-lg .modal-content .modal-header - %a.close{ href: "#", "data-dismiss" => "modal" } × - %h4 + %h4.modal-title Keyboard Shortcuts %small = link_to '(Show all)', '#', class: 'js-more-help-button' + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + %span{ "aria-hidden": true } × .modal-body .row .col-lg-4 diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index 6391a13dd25..7a66bac09cb 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -36,7 +36,7 @@ .card .card-header Quick help - %ul.well-list + %ul.content-list %li= link_to 'See our website for getting help', support_url %li %button.btn-blank.btn-link.js-trigger-search-bar{ type: 'button' } diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 94b012d39a3..de8369ed7b9 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -116,9 +116,9 @@ .lead List with hover effect - %code .well-list + %code .hover-list .example - %ul.well-list + %ul.hover-list %li One item %li @@ -131,7 +131,7 @@ .example .card .card-header Your list - %ul.well-list + %ul.content-list %li One item %li @@ -472,8 +472,8 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - %label - %input{ :type => "checkbox" }/ + %input.form-check-input{ :type => "checkbox" }/ + %label.form-check-label Remember me .form-group.row .offset-sm-2.col-sm-10 @@ -492,8 +492,8 @@ %label{ :for => "exampleInputPassword1" } Password %input#exampleInputPassword1.form-control{ :placeholder => "Password", :type => "password" }/ .form-check - %label - %input{ :type => "checkbox" }/ + %input.form-check-input{ :type => "checkbox" }/ + %label.form-check-label Remember me %button.btn.btn-default{ :type => "submit" } Sign in diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml index da9331b45dd..9f8b0acd763 100644 --- a/app/views/ide/index.html.haml +++ b/app/views/ide/index.html.haml @@ -3,7 +3,9 @@ #ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'), "no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'), - "committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg') } } + "committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'), + "pipelines-empty-state-svg-path": image_path('illustrations/pipelines_empty.svg'), + "ci-help-page-path" => help_page_path('ci/quick_start/README'), } } .text-center = icon('spinner spin 2x') %h2.clgray= _('Loading the GitLab IDE...') diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml index d5a37095365..74d686b6703 100644 --- a/app/views/import/fogbugz/new.html.haml +++ b/app/views/import/fogbugz/new.html.haml @@ -11,16 +11,16 @@ In the next steps, you'll be able to map users and select the projects you want to import. .form-group.row - = label_tag :uri, 'FogBugz URL', class: 'col-form-label col-sm-8' - .col-sm-4 + = label_tag :uri, 'FogBugz URL', class: 'col-form-label col-md-2' + .col-md-4 = text_field_tag :uri, nil, placeholder: 'https://mycompany.fogbugz.com', class: 'form-control' .form-group.row - = label_tag :email, 'FogBugz Email', class: 'col-form-label col-sm-8' - .col-sm-4 + = label_tag :email, 'FogBugz Email', class: 'col-form-label col-md-2' + .col-md-4 = text_field_tag :email, nil, class: 'form-control' .form-group.row - = label_tag :password, 'FogBugz Password', class: 'col-form-label col-sm-8' - .col-sm-4 + = label_tag :password, 'FogBugz Password', class: 'col-form-label col-md-2' + .col-md-4 = password_field_tag :password, nil, class: 'form-control' .form-actions = submit_tag 'Continue to the next step', class: 'btn btn-create' diff --git a/app/views/issues/_issues_calendar.ics.ruby b/app/views/issues/_issues_calendar.ics.ruby new file mode 100644 index 00000000000..3563635d33d --- /dev/null +++ b/app/views/issues/_issues_calendar.ics.ruby @@ -0,0 +1,15 @@ +cal = Icalendar::Calendar.new +cal.prodid = '-//GitLab//NONSGML GitLab//EN' +cal.x_wr_calname = 'GitLab Issues' + +@issues.includes(project: :namespace).each do |issue| + cal.event do |event| + event.dtstart = Icalendar::Values::Date.new(issue.due_date) + event.summary = "#{issue.title} (in #{issue.project.full_path})" + event.description = "Find out more at #{issue_url(issue)}" + event.url = issue_url(issue) + event.transp = 'TRANSPARENT' + end +end + +cal.to_ical diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index 9382ee8715e..06069a72951 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -3,57 +3,17 @@ %head %meta{ :content => "width=device-width, initial-scale=1, maximum-scale=1", :name => "viewport" } %title= yield(:title) - :css - body { - color: #666; - text-align: center; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - margin: auto; - font-size: 14px; - } + %style + = Rails.application.assets_manifest.find_sources('errors.css').first.to_s.html_safe + %body + .page-container + = yield + -# haml-lint:disable InlineJavaScript + :javascript + (function(){ + var goBackElement = document.querySelector('.js-go-back'); - h1 { - font-size: 56px; - line-height: 100px; - font-weight: 400; - color: #456; - } - - h2 { - font-size: 24px; - color: #666; - line-height: 1.5em; - } - - h3 { - color: #456; - font-size: 20px; - font-weight: 400; - line-height: 28px; - } - - hr { - max-width: 800px; - margin: 18px auto; - border: 0; - border-top: 1px solid #EEE; - border-bottom: 1px solid white; - } - - img { - max-width: 40vw; - display: block; - margin: 40px auto; - } - - .container { - margin: auto 20px; - } - - ul { - margin: auto; - text-align: left; - display:inline-block; - } -%body - = yield + if (goBackElement && history.length > 1) { + goBackElement.style.display = 'block'; + } + }()); diff --git a/app/views/notify/merge_request_unmergeable_email.html.haml b/app/views/notify/merge_request_unmergeable_email.html.haml index e84905a5fad..578fa1fbce7 100644 --- a/app/views/notify/merge_request_unmergeable_email.html.haml +++ b/app/views/notify/merge_request_unmergeable_email.html.haml @@ -1,5 +1,5 @@ %p - Merge Request #{link_to @merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request)} can no longer be merged due to the following #{pluralize(@reasons, 'reason')}: + Merge Request #{link_to @merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request)} can no longer be merged due to the following #{'reason'.pluralize(@reasons.count)}: %ul - @reasons.each do |reason| diff --git a/app/views/notify/merge_request_unmergeable_email.text.haml b/app/views/notify/merge_request_unmergeable_email.text.haml index 4f0901c761a..e4f9f1bf5e7 100644 --- a/app/views/notify/merge_request_unmergeable_email.text.haml +++ b/app/views/notify/merge_request_unmergeable_email.text.haml @@ -1,4 +1,4 @@ -Merge Request #{@merge_request.to_reference} can no longer be merged due to the following #{pluralize(@reasons, 'reason')}: +Merge Request #{@merge_request.to_reference} can no longer be merged due to the following #{'reason'.pluralize(@reasons.count)}: - @reasons.each do |reason| * #{reason} diff --git a/app/views/peek/_bar.html.haml b/app/views/peek/_bar.html.haml index cb0cccb8f8a..89d3b931f88 100644 --- a/app/views/peek/_bar.html.haml +++ b/app/views/peek/_bar.html.haml @@ -2,6 +2,6 @@ #js-peek{ data: { env: Peek.env, request_id: Peek.request_id, - peek_url: peek_routes.results_url, + peek_url: "#{peek_routes_path}/results", profile_url: url_for(safe_params.merge(lineprofiler: 'true')) }, class: Peek.env } diff --git a/app/views/profiles/_event_table.html.haml b/app/views/profiles/_event_table.html.haml index 37466f7c821..9f525547dd9 100644 --- a/app/views/profiles/_event_table.html.haml +++ b/app/views/profiles/_event_table.html.haml @@ -1,7 +1,7 @@ %h5.prepend-top-0 History of authentications -%ul.well-list +%ul.content-list - events.each do |event| %li %span.description diff --git a/app/views/profiles/active_sessions/_active_session.html.haml b/app/views/profiles/active_sessions/_active_session.html.haml index d198bfc80db..23ef31a0c85 100644 --- a/app/views/profiles/active_sessions/_active_session.html.haml +++ b/app/views/profiles/active_sessions/_active_session.html.haml @@ -1,10 +1,10 @@ - is_current_session = active_session.current?(session) %li.list-group-item - .pull-left.append-right-10{ data: { toggle: 'tooltip' }, title: active_session.human_device_type } + .float-left.append-right-10{ data: { toggle: 'tooltip' }, title: active_session.human_device_type } = active_session_device_type_icon(active_session) - .description.pull-left + .description.float-left %div %strong= active_session.ip_address - if is_current_session @@ -25,7 +25,7 @@ = l(active_session.created_at, format: :short) - unless is_current_session - .pull-right + .float-right = link_to profile_active_session_path(active_session.session_id), data: { confirm: 'Are you sure? The device will be signed out of GitLab.' }, method: :delete, class: "btn btn-danger prepend-left-10" do %span.sr-only Revoke Revoke diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 914bd4eb57c..a5db9dbe7f8 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -29,7 +29,7 @@ Your Public Email will be displayed on your public profile. %li All email addresses will be used to identify your commits. - %ul.well-list + %ul.content-list %li = render partial: 'shared/email_with_badge', locals: { email: @primary_email, verified: current_user.confirmed? } %span.float-right diff --git a/app/views/profiles/gpg_keys/_key_table.html.haml b/app/views/profiles/gpg_keys/_key_table.html.haml index cabb92c5a24..b9b60c218fd 100644 --- a/app/views/profiles/gpg_keys/_key_table.html.haml +++ b/app/views/profiles/gpg_keys/_key_table.html.haml @@ -1,7 +1,7 @@ - is_admin = local_assigns.fetch(:admin, false) - if @gpg_keys.any? - %ul.well-list + %ul.content-list = render partial: 'profiles/gpg_keys/key', collection: @gpg_keys, locals: { is_admin: is_admin } - else %p.settings-message.text-center diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index 02d5b08f7a3..2ac514d3f6f 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -4,7 +4,7 @@ .card .card-header SSH Key - %ul.well-list + %ul.content-list %li %span.light Title: %strong= @key.title diff --git a/app/views/profiles/keys/_key_table.html.haml b/app/views/profiles/keys/_key_table.html.haml index e78763bdcb2..e088140fdd2 100644 --- a/app/views/profiles/keys/_key_table.html.haml +++ b/app/views/profiles/keys/_key_table.html.haml @@ -1,7 +1,7 @@ - is_admin = local_assigns.fetch(:admin, false) - if @keys.any? - %ul.well-list + %ul.content-list = render partial: 'profiles/keys/key', collection: @keys, locals: { is_admin: is_admin } - else %p.settings-message.text-center diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index d253e8e456e..d111113c646 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -34,18 +34,18 @@ .row.prepend-top-default .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 - RSS token + Feed token %p - Your RSS token is used to authenticate you when your RSS reader loads a personalized RSS feed, and is included in your personal RSS feed URLs. + Your feed token is used to authenticate you when your RSS reader loads a personalized RSS feed or when when your calendar application loads a personalized calendar, and is included in those feed URLs. %p It cannot be used to access any other data. - .col-lg-8.rss-token-reset - = label_tag :rss_token, 'RSS token', class: "label-light" - = text_field_tag :rss_token, current_user.rss_token, class: 'form-control', readonly: true, onclick: 'this.select()' + .col-lg-8.feed-token-reset + = label_tag :feed_token, 'Feed token', class: "label-light" + = text_field_tag :feed_token, current_user.feed_token, class: 'form-control', readonly: true, onclick: 'this.select()' %p.form-text.text-muted - Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds as if they were you. + Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds or your calendar feed as if they were you. You should - = link_to 'reset it', [:reset, :rss_token, :profile], method: :put, data: { confirm: 'Are you sure? Any RSS URLs currently in use will stop working.' } + = link_to 'reset it', [:reset, :feed_token, :profile], method: :put, data: { confirm: 'Are you sure? Any RSS or calendar URLs currently in use will stop working.' } if that ever happens. - if incoming_email_token_enabled? diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index ab5565cfdaf..ce312943154 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -4,7 +4,7 @@ = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f| .col-lg-4.application-theme %h4.prepend-top-0 - GitLab navigation theme + s_('Preferences|Navigation theme') %p Customize the appearance of the application header and navigation sidebar. .col-lg-8.application-theme - Gitlab::Themes.each do |theme| diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index fbb29e7a0d9..507cd5dcc12 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -77,11 +77,10 @@ .modal-dialog .modal-content .modal-header - %button.close{ type: 'button', 'data-dismiss': 'modal' } - %span - × %h4.modal-title Position and size your new avatar + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + %span{ "aria-hidden": true } × .modal-body .profile-crop-image-container %img.modal-profile-crop-image{ alt: 'Avatar cropper' } diff --git a/app/views/projects/_bitbucket_import_modal.html.haml b/app/views/projects/_bitbucket_import_modal.html.haml index c24a496486c..c54a4ceb890 100644 --- a/app/views/projects/_bitbucket_import_modal.html.haml +++ b/app/views/projects/_bitbucket_import_modal.html.haml @@ -2,8 +2,9 @@ .modal-dialog .modal-content .modal-header - %a.close{ href: "#", "data-dismiss" => "modal" } × - %h3 Import projects from Bitbucket + %h3.modal-title Import projects from Bitbucket + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + %span{ "aria-hidden": true } × .modal-body To enable importing projects from Bitbucket, - if current_user.admin? diff --git a/app/views/projects/_gitlab_import_modal.html.haml b/app/views/projects/_gitlab_import_modal.html.haml index 00aef66e1f8..5519415cdc3 100644 --- a/app/views/projects/_gitlab_import_modal.html.haml +++ b/app/views/projects/_gitlab_import_modal.html.haml @@ -2,8 +2,9 @@ .modal-dialog .modal-content .modal-header - %a.close{ href: "#", "data-dismiss" => "modal" } × - %h3 Import projects from GitLab.com + %h3.modal-title Import projects from GitLab.com + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + %span{ "aria-hidden": true } × .modal-body To enable importing projects from GitLab.com, - if current_user.admin? diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml index 4bee6cb97eb..8f535b9d789 100644 --- a/app/views/projects/_import_project_pane.html.haml +++ b/app/views/projects/_import_project_pane.html.haml @@ -1,51 +1,49 @@ - active_tab = local_assigns.fetch(:active_tab, 'blank') - f = local_assigns.fetch(:f) -.project-import.row - .col-lg-12 - .form-group.import-btn-container.clearfix - = f.label :visibility_level, class: 'label-light' do #the label here seems wrong - Import project from - .import-buttons - - if gitlab_project_import_enabled? - .import_gitlab_project.has-tooltip{ data: { container: 'body' } } - = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do - = icon('gitlab', text: 'GitLab export') - %div - - if github_import_enabled? - = link_to new_import_github_path, class: 'btn js-import-github' do - = icon('github', text: 'GitHub') - %div - - if bitbucket_import_enabled? - = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do - = icon('bitbucket', text: 'Bitbucket') - - unless bitbucket_import_configured? - = render 'bitbucket_import_modal' - %div - - if gitlab_import_enabled? - = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do - = icon('gitlab', text: 'GitLab.com') - - unless gitlab_import_configured? - = render 'gitlab_import_modal' - %div - - if google_code_import_enabled? - = link_to new_import_google_code_path, class: 'btn import_google_code' do - = icon('google', text: 'Google Code') - %div - - if fogbugz_import_enabled? - = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do - = icon('bug', text: 'Fogbugz') - %div - - if gitea_import_enabled? - = link_to new_import_gitea_path, class: 'btn import_gitea' do - = custom_icon('go_logo') - Gitea - %div - - if git_import_enabled? - %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } } - = icon('git', text: 'Repo by URL') - .col-lg-12 - .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') } - %hr - = render "shared/import_form", f: f - = render 'new_project_fields', f: f, project_name_id: "import-url-name" +.project-import + .form-group.import-btn-container.clearfix + = f.label :visibility_level, class: 'label-light' do #the label here seems wrong + Import project from + .import-buttons + - if gitlab_project_import_enabled? + .import_gitlab_project.has-tooltip{ data: { container: 'body' } } + = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do + = icon('gitlab', text: 'GitLab export') + %div + - if github_import_enabled? + = link_to new_import_github_path, class: 'btn js-import-github' do + = icon('github', text: 'GitHub') + %div + - if bitbucket_import_enabled? + = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do + = icon('bitbucket', text: 'Bitbucket') + - unless bitbucket_import_configured? + = render 'bitbucket_import_modal' + %div + - if gitlab_import_enabled? + = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do + = icon('gitlab', text: 'GitLab.com') + - unless gitlab_import_configured? + = render 'gitlab_import_modal' + %div + - if google_code_import_enabled? + = link_to new_import_google_code_path, class: 'btn import_google_code' do + = icon('google', text: 'Google Code') + %div + - if fogbugz_import_enabled? + = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do + = icon('bug', text: 'Fogbugz') + %div + - if gitea_import_enabled? + = link_to new_import_gitea_path, class: 'btn import_gitea' do + = custom_icon('go_logo') + Gitea + %div + - if git_import_enabled? + %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } } + = icon('git', text: 'Repo by URL') + .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') } + %hr + = render "shared/import_form", f: f + = render 'new_project_fields', f: f, project_name_id: "import-url-name" diff --git a/app/views/projects/_issuable_by_email.html.haml b/app/views/projects/_issuable_by_email.html.haml index e3dc0677bd6..22adf5b4008 100644 --- a/app/views/projects/_issuable_by_email.html.haml +++ b/app/views/projects/_issuable_by_email.html.haml @@ -8,10 +8,10 @@ .modal-dialog{ role: "document" } .modal-content .modal-header - %button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } } - %span{ aria: { hidden: "true" } }= icon("times") %h4.modal-title Create new #{name} by email + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + %span{ "aria-hidden": true } × .modal-body %p You can create a new #{name} inside this project by sending an email to the following email address: diff --git a/app/views/projects/_merge_request_fast_forward_settings.html.haml b/app/views/projects/_merge_request_fast_forward_settings.html.haml deleted file mode 100644 index 2f08a28e26e..00000000000 --- a/app/views/projects/_merge_request_fast_forward_settings.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- form = local_assigns.fetch(:form) -- project = local_assigns.fetch(:project) - -.form-check - = label_tag :project_merge_method_ff do - = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff" - %strong Fast-forward merge - %br - %span.descr - No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. - %br - %span.descr - When fast-forward merge is not possible, the user is given the option to rebase. diff --git a/app/views/projects/_merge_request_merge_method_settings.html.haml b/app/views/projects/_merge_request_merge_method_settings.html.haml new file mode 100644 index 00000000000..3bb220ac6d0 --- /dev/null +++ b/app/views/projects/_merge_request_merge_method_settings.html.haml @@ -0,0 +1,36 @@ +- form = local_assigns.fetch(:form) +- project = local_assigns.fetch(:project) + +.form-group + = label_tag :merge_method_merge, class: 'label-light' do + Merge method + .form-check + = form.radio_button :merge_method, :merge, class: "js-merge-method-radio form-check-input" + = label_tag :project_merge_method_merge, class: 'form-check-label' do + %strong Merge commit + %br + %span.descr + A merge commit is created for every merge, and merging is allowed as long as there are no conflicts. + + .form-check + = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio form-check-input" + = label_tag :project_merge_method_rebase_merge, class: 'form-check-label' do + %strong Merge commit with semi-linear history + %br + %span.descr + A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible. + This way you could make sure that if this merge request would build, after merging to target branch it would also build. + %br + %span.descr + When fast-forward merge is not possible, the user is given the option to rebase. + + .form-check + = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff form-check-input" + = label_tag :project_merge_method_ff, class: 'form-check-label' do + %strong Fast-forward merge + %br + %span.descr + No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. + %br + %span.descr + When fast-forward merge is not possible, the user is given the option to rebase. diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml index 762a263656d..f178c94e008 100644 --- a/app/views/projects/_merge_request_merge_settings.html.haml +++ b/app/views/projects/_merge_request_merge_settings.html.haml @@ -2,22 +2,22 @@ .form-group .form-check.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) } - = form.label :only_allow_merge_if_pipeline_succeeds do - = form.check_box :only_allow_merge_if_pipeline_succeeds + = form.check_box :only_allow_merge_if_pipeline_succeeds, class: 'form-check-input' + = form.label :only_allow_merge_if_pipeline_succeeds, class: 'form-check-label' do %strong Only allow merge requests to be merged if the pipeline succeeds %br %span.descr Pipelines need to be configured to enable this feature. = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds'), target: '_blank' .form-check - = form.label :only_allow_merge_if_all_discussions_are_resolved do - = form.check_box :only_allow_merge_if_all_discussions_are_resolved + = form.check_box :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-input' + = form.label :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-label' do %strong Only allow merge requests to be merged if all discussions are resolved .form-check - = form.label :resolve_outdated_diff_discussions do - = form.check_box :resolve_outdated_diff_discussions + = form.check_box :resolve_outdated_diff_discussions, class: 'form-check-input' + = form.label :resolve_outdated_diff_discussions, class: 'form-check-label' do %strong Automatically resolve merge request diff discussions when they become outdated .form-check - = form.label :printing_merge_request_link_enabled do - = form.check_box :printing_merge_request_link_enabled + = form.check_box :printing_merge_request_link_enabled, class: 'form-check-input' + = form.label :printing_merge_request_link_enabled, class: 'form-check-label' do %strong Show link to create/view merge request when pushing from the command line diff --git a/app/views/projects/_merge_request_rebase_settings.html.haml b/app/views/projects/_merge_request_rebase_settings.html.haml deleted file mode 100644 index 93895a55435..00000000000 --- a/app/views/projects/_merge_request_rebase_settings.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- form = local_assigns.fetch(:form) - -.form-check - = label_tag :project_merge_method_rebase_merge do - = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio" - %strong Merge commit with semi-linear history - %br - %span.descr - A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible. - This way you could make sure that if this merge request would build, after merging to target branch it would also build. - %br - %span.descr - When fast-forward merge is not possible, the user is given the option to rebase. diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml index a9ddcd94865..c80e831dd33 100644 --- a/app/views/projects/_merge_request_settings.html.haml +++ b/app/views/projects/_merge_request_settings.html.haml @@ -1,18 +1,5 @@ - form = local_assigns.fetch(:form) -.form-group - = label_tag :merge_method_merge, class: 'label-light' do - Merge method - .form-check - = label_tag :project_merge_method_merge do - = form.radio_button :merge_method, :merge, class: "js-merge-method-radio" - %strong Merge commit - %br - %span.descr - A merge commit is created for every merge, and merging is allowed as long as there are no conflicts. - - = render 'merge_request_rebase_settings', form: form - - = render 'merge_request_fast_forward_settings', project: @project, form: form += render 'projects/merge_request_merge_method_settings', project: @project, form: form = render 'projects/merge_request_merge_settings', form: form diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index 6366a2f729a..cfbd0459e3e 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -35,11 +35,10 @@ %span (optional) = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250 -.form-group.visibility-level-setting - = f.label :visibility_level, class: 'label-light' do - Visibility Level - = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer' - = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false += f.label :visibility_level, class: 'label-light' do + Visibility Level + = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer' += render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml index e7a4e3d67cb..6f3a691518b 100644 --- a/app/views/projects/blob/_new_dir.html.haml +++ b/app/views/projects/blob/_new_dir.html.haml @@ -2,8 +2,9 @@ .modal-dialog.modal-lg .modal-content .modal-header - %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title= _('Create New Directory') + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + %span{ "aria-hidden": true } × .modal-body = form_tag project_create_dir_path(@project, @id), method: :post, remote: false, class: 'js-create-dir-form js-quick-submit js-requires-input' do .form-group.row diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml index 4628ecff3d6..f80bae5c88c 100644 --- a/app/views/projects/blob/_remove.html.haml +++ b/app/views/projects/blob/_remove.html.haml @@ -2,8 +2,9 @@ .modal-dialog .modal-content .modal-header - %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title Delete #{@blob.name} + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + %span{ "aria-hidden": true } × .modal-body = form_tag project_blob_path(@project, @id), method: :delete, class: 'js-delete-blob-form js-quick-submit js-requires-input' do diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index 60a49441ce8..0a5c73c9037 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -2,8 +2,9 @@ .modal-dialog.modal-lg .modal-content .modal-header - %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title= title + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + %span{ "aria-hidden": true } × .modal-body = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form', data: { method: method } do .dropzone diff --git a/app/views/projects/branches/_delete_protected_modal.html.haml b/app/views/projects/branches/_delete_protected_modal.html.haml index e0008e322a0..8aa79d2d464 100644 --- a/app/views/projects/branches/_delete_protected_modal.html.haml +++ b/app/views/projects/branches/_delete_protected_modal.html.haml @@ -2,11 +2,12 @@ .modal-dialog .modal-content .modal-header - %button.close{ data: { dismiss: 'modal' } } × %h3.page-title - title_branch_name = capture do %span.js-branch-name.ref-name>[branch name] = s_("Branches|Delete protected branch '%{branch_name}'?").html_safe % { branch_name: title_branch_name } + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + %span{ "aria-hidden": true } × .modal-body %p diff --git a/app/views/projects/clusters/gcp/login.html.haml b/app/views/projects/clusters/gcp/login.html.haml index f1771349a53..55a42ac4847 100644 --- a/app/views/projects/clusters/gcp/login.html.haml +++ b/app/views/projects/clusters/gcp/login.html.haml @@ -12,8 +12,7 @@ .row .col-sm-8.offset-sm-4.signin-with-google - if @authorize_url - = link_to @authorize_url do - = image_tag('auth_buttons/signin_with_google.png', width: '191px') + = link_to(image_tag('auth_buttons/signin_with_google.png', width: '191px'), @authorize_url) = _('or') = link_to('create a new Google account', 'https://accounts.google.com/SignUpWithoutGmail?service=cloudconsole&continue=https%3A%2F%2Fconsole.cloud.google.com%2Ffreetrial%3Futm_campaign%3D2018_cpanel%26utm_source%3Dgitlab%26utm_medium%3Dreferral', target: '_blank', rel: 'noopener noreferrer') - else diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index 4c510293204..08d2deff6f8 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -11,6 +11,7 @@ install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress), install_prometheus_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :prometheus), install_runner_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :runner), + install_jupyter_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :jupyter), toggle_status: @cluster.enabled? ? 'true': 'false', cluster_status: @cluster.status_name, cluster_status_reason: @cluster.status_reason, diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 430bc8f59f9..30605927fd1 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -15,8 +15,9 @@ .modal-dialog .modal-content .modal-header - %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title= title + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + %span{ "aria-hidden": true } × .modal-body - if description %p.append-bottom-20= description diff --git a/app/views/projects/deploy_tokens/_revoke_modal.html.haml b/app/views/projects/deploy_tokens/_revoke_modal.html.haml index ace3480c815..a67c3a0c841 100644 --- a/app/views/projects/deploy_tokens/_revoke_modal.html.haml +++ b/app/views/projects/deploy_tokens/_revoke_modal.html.haml @@ -2,11 +2,11 @@ .modal-dialog .modal-content .modal-header - %h4.modal-title.float-left + %h4.modal-title = s_('DeployTokens|Revoke') %b #{token.name}? - %button.close{ 'aria-label' => _('Close'), 'data-dismiss' => 'modal', type: 'button' } - %span{ 'aria-hidden' => 'true' } × + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + %span{ "aria-hidden": true } × .modal-body %p = s_('DeployTokens|You are about to revoke') diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 79b22766bc7..77ba422e87e 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -33,10 +33,15 @@ %span.light (optional) = f.text_area :description, class: "form-control", rows: 3, maxlength: 250 + = render_if_exists 'projects/classification_policy_settings', f: f + - unless @project.empty_repo? .form-group = f.label :default_branch, "Default Branch", class: 'label-light' = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) + + = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project + .form-group = f.label :tag_list, "Tags", class: 'label-light' = f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control" @@ -75,6 +80,8 @@ .js-project-permissions-form = f.submit 'Save changes', class: "btn btn-save" + = render_if_exists 'projects/issues_settings' + %section.settings.merge-requests-feature.no-animate{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] } .settings-header %h4 @@ -84,10 +91,14 @@ %p Customize your merge request restrictions. .settings-content + = render_if_exists 'shared/promotions/promote_mr_features' + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f| - = render 'merge_request_settings', form: f + = render 'projects/merge_request_settings', form: f = f.submit 'Save changes', class: "btn btn-save qa-save-merge-request-changes" + = render_if_exists 'projects/service_desk_settings' + = render 'export', project: @project %section.settings.advanced-settings.no-animate{ class: ('expanded' if expanded) } diff --git a/app/views/projects/hooks/_index.html.haml b/app/views/projects/hooks/_index.html.haml index 776681ea09a..5990582fd55 100644 --- a/app/views/projects/hooks/_index.html.haml +++ b/app/views/projects/hooks/_index.html.haml @@ -15,7 +15,7 @@ %h5.prepend-top-default Webhooks (#{@hooks.count}) - if @hooks.any? - %ul.well-list + %ul.content-list - @hooks.each do |hook| = render 'project_hook', hook: hook - else diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml index 297b928f020..0dd2d2e6c5d 100644 --- a/app/views/projects/issues/_nav_btns.html.haml +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -1,5 +1,5 @@ -= link_to safe_params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do - = icon('rss') += render 'shared/issuable/feed_buttons' + - if @can_bulk_update = button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle" - if show_new_issue_link?(@project) diff --git a/app/views/projects/issues/calendar.ics.haml b/app/views/projects/issues/calendar.ics.haml new file mode 100644 index 00000000000..59573e5fecf --- /dev/null +++ b/app/views/projects/issues/calendar.ics.haml @@ -0,0 +1 @@ += render 'issues/issues_calendar', issues: @issues diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml index 361d3c61d99..37c09f12f63 100644 --- a/app/views/projects/mattermosts/_team_selection.html.haml +++ b/app/views/projects/mattermosts/_team_selection.html.haml @@ -13,9 +13,9 @@ = f.hidden_field(:team_id, value: selected_id, required: true) if @teams.one? .form-text.text-muted - if @teams.one? - This is the only available team. + This is the only available team that you are a member of. - else - The list shows all available teams. + The list shows all available teams that you are a member of. To create a team, = link_to "#{Gitlab.config.mattermost.host}/create_team" do use Mattermost's interface diff --git a/app/views/projects/merge_requests/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml index 5353fa8a88f..62dd21ef6e0 100644 --- a/app/views/projects/merge_requests/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/_how_to_merge.html.haml @@ -2,8 +2,9 @@ .modal-dialog .modal-content .modal-header - %h3 Check out, review, and merge locally - %a.close{ href: "#", "data-dismiss" => "modal" } × + %h3.modal-title Check out, review, and merge locally + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + %span{ "aria-hidden": true } × .modal-body %p %strong Step 1. diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index aa3fb623e58..01e38ffee20 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -20,6 +20,8 @@ window.gl = window.gl || {}; window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget')} + window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}'; + #js-vue-mr-widget.mr-widget .content-block.content-block-small.emoji-list-container.js-noteable-awards diff --git a/app/views/projects/mirrors/_push.html.haml b/app/views/projects/mirrors/_push.html.haml index 4a6aefce351..c3dcd9617a6 100644 --- a/app/views/projects/mirrors/_push.html.haml +++ b/app/views/projects/mirrors/_push.html.haml @@ -30,7 +30,7 @@ #{h(@remote_mirror.last_error.strip)} = f.fields_for :remote_mirrors, @remote_mirror do |rm_form| .form-group - = rm_form.check_box :enabled, class: "pull-left" + = rm_form.check_box :enabled, class: "float-left" .prepend-left-20 = rm_form.label :enabled, "Remote mirror repository", class: "label-light append-bottom-0" %p.light.append-bottom-0 @@ -42,7 +42,7 @@ = render "projects/mirrors/instructions" .form-group - = rm_form.check_box :only_protected_branches, class: 'pull-left' + = rm_form.check_box :only_protected_branches, class: 'float-left' .prepend-left-20 = rm_form.label :only_protected_branches, class: 'label-light' = link_to icon('question-circle'), help_page_path('user/project/protected_branches') diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 75cb8245d36..2d3f9116703 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -10,8 +10,8 @@ = icon('search') .inline.prepend-left-20 .form-check.light - = label_tag :filter_ref do - = check_box_tag :filter_ref, 1, @options[:filter_ref] + = check_box_tag :filter_ref, 1, @options[:filter_ref], class: 'form-check-input' + = label_tag :filter_ref, class: 'form-check-label' do %span= _("Begin with the selected commit") - if @commit diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml index 986ca852411..e7178f9160c 100644 --- a/app/views/projects/pages/_list.html.haml +++ b/app/views/projects/pages/_list.html.haml @@ -4,7 +4,7 @@ .card .card-header Domains (#{@domains.count}) - %ul.well-list.pages-domain-list{ class: ("has-verification-status" if verification_enabled) } + %ul.content-list.pages-domain-list{ class: ("has-verification-status" if verification_enabled) } - @domains.each do |domain| %li.pages-domain-list-item.unstyled - if verification_enabled diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index d1e8e9d0d60..956f8fef6b8 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -29,7 +29,7 @@ .form-actions = f.submit s_('Pipeline|Create pipeline'), class: 'btn btn-success js-variables-save-button', tabindex: 3 - = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-default pull-right' + = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-default float-right' -# haml-lint:disable InlineJavaScript %script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml index 414df15feeb..bbabb98dafe 100644 --- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml +++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml @@ -11,22 +11,22 @@ = message.html_safe = f.fields_for :auto_devops_attributes, @auto_devops do |form| .form-check - = form.label :enabled_true do - = form.radio_button :enabled, 'true' + = form.radio_button :enabled, 'true', class: 'form-check-input' + = form.label :enabled_true, class: 'form-check-label' do %strong= s_('CICD|Enable Auto DevOps') %br = s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted } .form-check - = form.label :enabled_false do - = form.radio_button :enabled, 'false' + = form.radio_button :enabled, 'false', class: 'form-check-input' + = form.label :enabled_false, class: 'form-check-label' do %strong= s_('CICD|Disable Auto DevOps') %br = s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted } .form-check - = form.label :enabled_ do - = form.radio_button :enabled, '' + = form.radio_button :enabled, '', class: 'form-check-input' + = form.label :enabled_, class: 'form-check-label' do %strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" } %br = s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted } diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index 50175f5258c..e93b240a007 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -20,15 +20,15 @@ Choose between <code>clone</code> or <code>fetch</code> to get the recent application code = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy'), target: '_blank' .form-check - = f.label :build_allow_git_fetch_false do - = f.radio_button :build_allow_git_fetch, 'false' + = f.radio_button :build_allow_git_fetch, 'false', { class: 'form-check-input' } + = f.label :build_allow_git_fetch_false, class: 'form-check-label' do %strong git clone %br %span.descr Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job .form-check - = f.label :build_allow_git_fetch_true do - = f.radio_button :build_allow_git_fetch, 'true' + = f.radio_button :build_allow_git_fetch, 'true', { class: 'form-check-input' } + = f.label :build_allow_git_fetch_true, class: 'form-check-label' do %strong git fetch %br %span.descr @@ -53,8 +53,8 @@ %hr .form-group .form-check - = f.label :public_builds do - = f.check_box :public_builds + = f.check_box :public_builds, { class: 'form-check-input' } + = f.label :public_builds, class: 'form-check-label' do %strong Public pipelines .form-text.text-muted Allow public access to pipelines and job details, including output logs and artifacts @@ -75,8 +75,8 @@ %hr .form-group .form-check - = f.label :auto_cancel_pending_pipelines do - = f.check_box :auto_cancel_pending_pipelines, {}, 'enabled', 'disabled' + = f.check_box :auto_cancel_pending_pipelines, { class: 'form-check-input' }, 'enabled', 'disabled' + = f.label :auto_cancel_pending_pipelines, class: 'form-check-label' do %strong Auto-cancel redundant, pending pipelines .form-text.text-muted New pipelines will cancel older, pending pipelines on the same branch diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index 06a3cac12d5..38382aae67c 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -2,8 +2,9 @@ .modal-dialog .modal-content .modal-header - %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title= s_("WikiNewPageTitle|New Wiki Page") + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + %span{ "aria-hidden": true } × .modal-body %form.new-wiki-page .form-group diff --git a/app/views/shared/_allow_request_access.html.haml b/app/views/shared/_allow_request_access.html.haml index 0e698570d7d..92268e74b1e 100644 --- a/app/views/shared/_allow_request_access.html.haml +++ b/app/views/shared/_allow_request_access.html.haml @@ -1,6 +1,6 @@ .form-check - = form.label :request_access_enabled do - = form.check_box :request_access_enabled + = form.check_box :request_access_enabled, class: 'form-check-input' + = form.label :request_access_enabled, class: 'form-check-label' do %strong Allow users to request access %br %span.descr Allow users to request access if visibility is public or internal. diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml index 7c326d36d99..1dcf4369253 100644 --- a/app/views/shared/_confirm_modal.html.haml +++ b/app/views/shared/_confirm_modal.html.haml @@ -4,7 +4,8 @@ .modal-header %h3.page-title Confirmation required - %a.close{ href: "#", "data-dismiss" => "modal" } × + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + %span{ "aria-hidden": true } × .modal-body %p.text-danger.js-confirm-text diff --git a/app/views/shared/_delete_label_modal.html.haml b/app/views/shared/_delete_label_modal.html.haml index 01effefc34d..b96380923ac 100644 --- a/app/views/shared/_delete_label_modal.html.haml +++ b/app/views/shared/_delete_label_modal.html.haml @@ -2,8 +2,9 @@ .modal-dialog .modal-content .modal-header - %button.close{ data: {dismiss: 'modal' } } × %h3.page-title Delete #{render_colored_label(label, tooltip: false)} ? + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + %span{ "aria-hidden": true } × .modal-body %p diff --git a/app/views/shared/_email_with_badge.html.haml b/app/views/shared/_email_with_badge.html.haml index b7bbc109238..ad863b1967d 100644 --- a/app/views/shared/_email_with_badge.html.haml +++ b/app/views/shared/_email_with_badge.html.haml @@ -1,4 +1,4 @@ -- css_classes = %w(label label-verification-status) +- css_classes = %w(badge badge-verification-status) - css_classes << (verified ? 'verified': 'unverified') - text = verified ? 'Verified' : 'Unverified' diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml index 35673303b85..356e12cf9f8 100644 --- a/app/views/shared/_import_form.html.haml +++ b/app/views/shared/_import_form.html.haml @@ -1,19 +1,20 @@ - ci_cd_only = local_assigns.fetch(:ci_cd_only, false) -.form-group.row.import-url-data +.form-group.import-url-data = f.label :import_url, class: 'label-light' do %span = _('Git repository URL') = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', required: true - .card.prepend-top-20 - %ul - %li - = _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe - %li - = _('If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.').html_safe - %li - = import_will_timeout_message(ci_cd_only) - %li - = import_svn_message(ci_cd_only) + .info-well.prepend-top-20 + .well-segment + %ul + %li + = _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe + %li + = _('If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.').html_safe + %li + = import_will_timeout_message(ci_cd_only) + %li + = import_svn_message(ci_cd_only) diff --git a/app/views/shared/_new_merge_request_checkbox.html.haml b/app/views/shared/_new_merge_request_checkbox.html.haml index 8dbdf63f9f4..165109b6b70 100644 --- a/app/views/shared/_new_merge_request_checkbox.html.haml +++ b/app/views/shared/_new_merge_request_checkbox.html.haml @@ -1,7 +1,7 @@ .form-check - nonce = SecureRandom.hex - = label_tag "create_merge_request-#{nonce}" do - = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" + = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request form-check-input', id: "create_merge_request-#{nonce}" + = label_tag "create_merge_request-#{nonce}", class: 'form-check-label' do - translation_variables = { new_merge_request: "<strong>#{_('new merge request')}</strong>" } - translation = _('Start a %{new_merge_request} with these changes') % translation_variables #{ translation.html_safe } diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml index 38c6f560dc6..d67409ffe14 100644 --- a/app/views/shared/_visibility_level.html.haml +++ b/app/views/shared/_visibility_level.html.haml @@ -1,6 +1,6 @@ - with_label = local_assigns.fetch(:with_label, true) -.form-group.row.visibility-level-setting +.form-group.visibility-level-setting - if with_label = f.label :visibility_level, class: 'col-form-label col-sm-2' do Visibility Level diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml index bdf550cd8b6..dd6b9cce58e 100644 --- a/app/views/shared/_visibility_radios.html.haml +++ b/app/views/shared/_visibility_radios.html.haml @@ -2,9 +2,9 @@ - disallowed = disallowed_visibility_level?(form_model, level) - restricted = restricted_visibility_levels.include?(level) - disabled = disallowed || restricted - .form-check.pl-0{ class: [('disabled' if disabled), ('restricted' if restricted)] } - = form.label "#{model_method}_#{level}" do - = form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled + .form-check{ class: [('disabled' if disabled), ('restricted' if restricted)] } + = form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled, class: 'form-check-input' + = form.label "#{model_method}_#{level}", class: 'form-check-label' do = visibility_level_icon(level) .option-title = visibility_level_label(level) diff --git a/app/views/shared/dashboard/_no_filter_selected.html.haml b/app/views/shared/dashboard/_no_filter_selected.html.haml index b2e6967f6aa..32246dac4c7 100644 --- a/app/views/shared/dashboard/_no_filter_selected.html.haml +++ b/app/views/shared/dashboard/_no_filter_selected.html.haml @@ -1,8 +1,8 @@ .row.empty-state.text-center - .col-xs-12 + .col-12 .svg-130.prepend-top-default = image_tag 'illustrations/issue-dashboard_results-without-filter.svg' - .col-xs-12 + .col-12 .text-content %h4 = _("Please select at least one filter to see results") diff --git a/app/views/shared/empty_states/_wikis_layout.html.haml b/app/views/shared/empty_states/_wikis_layout.html.haml index 6fae6104ca2..a5f100e3469 100644 --- a/app/views/shared/empty_states/_wikis_layout.html.haml +++ b/app/views/shared/empty_states/_wikis_layout.html.haml @@ -1,7 +1,7 @@ .row.empty-state - .col-xs-12 + .col-12 .svg-content = image_tag image_path - .col-xs-12 + .col-12 .text-content.text-center = yield diff --git a/app/views/shared/icons/_icon_calendar.svg b/app/views/shared/icons/_icon_calendar.svg new file mode 100644 index 00000000000..4d0a703f9a0 --- /dev/null +++ b/app/views/shared/icons/_icon_calendar.svg @@ -0,0 +1 @@ +<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M15 5v7a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V5a2 2 0 0 1 2-2h1V2a1 1 0 1 1 2 0v1h4V2a1 1 0 1 1 2 0v1h1a2 2 0 0 1 2 2zM3 6v6a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V6H3zm2 2h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z" fill="#000" fill-rule="evenodd"/></svg>
\ No newline at end of file diff --git a/app/views/shared/issuable/_feed_buttons.html.haml b/app/views/shared/issuable/_feed_buttons.html.haml new file mode 100644 index 00000000000..d4834090413 --- /dev/null +++ b/app/views/shared/issuable/_feed_buttons.html.haml @@ -0,0 +1,4 @@ += link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe to RSS feed' do + = icon('rss') += link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: 'Subscribe to calendar' do + = custom_icon('icon_calendar') diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index fbc608b207a..b49e47a7266 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -23,12 +23,14 @@ .form-group.row .offset-sm-2.col-sm-10 .form-check - = form.label :confidential do - = form.check_box :confidential + = form.check_box :confidential, class: 'form-check-input' + = form.label :confidential, class: 'form-check-label' do This issue is confidential and should only be visible to team members with at least Reporter access. = render 'shared/issuable/form/metadata', issuable: issuable, form: form += render_if_exists 'shared/issuable/approvals', issuable: issuable, form: form + = render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form = render 'shared/issuable/form/merge_params', issuable: issuable @@ -77,5 +79,6 @@ %strong= link_to('contribution guidelines', guide_url) for this project. += render_if_exists 'shared/issuable/remove_approver' = form.hidden_field :lock_version diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index a57cd4b20d1..9e50e888b35 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -18,6 +18,9 @@ = render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable, is_collapsed: true .block.assignee = render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present? + + = render_if_exists 'shared/issuable/sidebar_item_epic', issuable: issuable + .block.milestone .sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } = icon('clock-o', 'aria-hidden': 'true') @@ -115,6 +118,8 @@ - if can? current_user, :admin_label, @project and @project = render partial: "shared/issuable/label_page_create" + = render_if_exists 'shared/issuable/sidebar_weight', issuable: issuable + - if issuable.has_attribute?(:confidential) -# haml-lint:disable InlineJavaScript %script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: @issue.confidential, is_editable: can_edit_issuable }.to_json.html_safe @@ -139,7 +144,7 @@ = _('Reference:') %cite{ title: project_ref } = project_ref - = clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left") + = clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left", boundary: 'viewport') - if current_user && issuable.can_move?(current_user) .block.js-sidebar-move-issue-block .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') } diff --git a/app/views/shared/issuable/form/_contribution.html.haml b/app/views/shared/issuable/form/_contribution.html.haml index f12c23cbc64..b34549240e0 100644 --- a/app/views/shared/issuable/form/_contribution.html.haml +++ b/app/views/shared/issuable/form/_contribution.html.haml @@ -12,8 +12,8 @@ = _('Contribution') .col-sm-10 .form-check - = form.label :allow_maintainer_to_push do - = form.check_box :allow_maintainer_to_push, disabled: !issuable.can_allow_maintainer_to_push?(current_user) + = form.check_box :allow_maintainer_to_push, disabled: !issuable.can_allow_maintainer_to_push?(current_user), class: 'form-check-input' + = form.label :allow_maintainer_to_push, class: 'form-check-label' do = _('Allow edits from maintainers.') = link_to 'About this feature', help_page_path('user/project/merge_requests/maintainer_access') .form-text.text-muted diff --git a/app/views/shared/issuable/form/_default_templates.html.haml b/app/views/shared/issuable/form/_default_templates.html.haml new file mode 100644 index 00000000000..49a5ce926b3 --- /dev/null +++ b/app/views/shared/issuable/form/_default_templates.html.haml @@ -0,0 +1,4 @@ +%p.form-text.text-muted + Add + = link_to 'description templates', help_page_path('user/project/description_templates'), tabindex: -1 + to help your contributors communicate effectively! diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml index 1df881e4102..1881875b7c0 100644 --- a/app/views/shared/issuable/form/_merge_params.html.haml +++ b/app/views/shared/issuable/form/_merge_params.html.haml @@ -3,15 +3,20 @@ - return unless issuable.is_a?(MergeRequest) - return if issuable.closed_without_fork? --# This check is duplicated below to avoid CE -> EE merge conflicts. --# This comment and the following line should only exist in CE. -- return unless issuable.can_remove_source_branch?(current_user) +- if issuable.can_remove_source_branch?(current_user) + .form-group.row + .col-sm-10.offset-sm-2 + .form-check + = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil + = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?, class: 'form-check-input' + = label_tag 'merge_request[force_remove_source_branch]', class: 'form-check-label' do + Remove source branch when merge request is accepted. .form-group.row .col-sm-10.offset-sm-2 - - if issuable.can_remove_source_branch?(current_user) - .form-check - = label_tag 'merge_request[force_remove_source_branch]' do - = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil - = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch? - Remove source branch when merge request is accepted. + .form-check + = hidden_field_tag 'merge_request[squash]', '0', id: nil + = check_box_tag 'merge_request[squash]', '1', issuable.squash, class: 'form-check-input' + = label_tag 'merge_request[squash]', class: 'form-check-label' do + Squash commits when merge request is accepted. + = link_to 'About this feature', help_page_path('user/project/merge_requests/squash_and_merge') diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml index c4f30f5f4d9..c35d0b3751f 100644 --- a/app/views/shared/issuable/form/_title.html.haml +++ b/app/views/shared/issuable/form/_title.html.haml @@ -30,7 +30,4 @@ merge request from being merged before it's ready. - if no_issuable_templates && can?(current_user, :push_code, issuable.project) - %p.form-text.text-muted - Add - = link_to 'description templates', help_page_path('user/project/description_templates'), tabindex: -1 - to help your contributors communicate effectively! + = render 'shared/issuable/form/default_templates' diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml index f79f66b144f..2bf5efae1e6 100644 --- a/app/views/shared/labels/_form.html.haml +++ b/app/views/shared/labels/_form.html.haml @@ -19,7 +19,7 @@ .form-text.text-muted Choose any color. %br - Or you can choose one of suggested colors below + Or you can choose one of the suggested colors below .suggest-colors - suggested_colors.each do |color| diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 42b2d27c44a..46debe1f2b9 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -61,7 +61,7 @@ title: 'Resend invite' - if user != current_user && member.can_update? - = form_for member, remote: true, html: { class: 'js-edit-member-form form-horizontal' } do |f| + = form_for member, remote: true, html: { class: 'js-edit-member-form form-group row append-right-5' } do |f| = f.hidden_field :access_level .member-form-control.dropdown.append-right-5 %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml index d8e4d2ff88c..ee6354b1c28 100644 --- a/app/views/shared/milestones/_issuables.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -11,7 +11,7 @@ = number_with_delimiter(issuables.length) - class_prefix = dom_class(issuables).pluralize - %ul{ class: "well-list milestone-#{class_prefix}-list", id: "#{class_prefix}-list-#{id}" } + %ul{ class: "content-list milestone-#{class_prefix}-list", id: "#{class_prefix}-list-#{id}" } = render partial: 'shared/milestones/issuable', collection: issuables, as: :issuable, diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index e6a65161ed6..55460acab8f 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -5,25 +5,25 @@ .fade-right= icon('angle-right') %ul.nav-links.scrolling-tabs.js-milestone-tabs.nav.nav-tabs - if issues_accessible - %li.active - = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do + %li.nav-item + = link_to '#tab-issues', class: 'nav-link active', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do Issues %span.badge.badge-pill= milestone.issues_visible_to_user(current_user).size - %li - = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do + %li.nav-item + = link_to '#tab-merge-requests', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do Merge Requests %span.badge.badge-pill= milestone.merge_requests.size - else - %li.active - = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do + %li.nav-item + = link_to '#tab-merge-requests', class: 'nav-link active', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do Merge Requests %span.badge.badge-pill= milestone.merge_requests.size - %li - = link_to '#tab-participants', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do + %li.nav-item + = link_to '#tab-participants', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do Participants %span.badge.badge-pill= milestone.participants.count - %li - = link_to '#tab-labels', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do + %li.nav-item + = link_to '#tab-labels', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do Labels %span.badge.badge-pill= milestone.labels.count diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml index d830225d169..1f6e8f98bbb 100644 --- a/app/views/shared/notifications/_custom_notifications.html.haml +++ b/app/views/shared/notifications/_custom_notifications.html.haml @@ -2,10 +2,10 @@ .modal-dialog .modal-content .modal-header - %button.close{ type: "button", "aria-label": "close", data: { dismiss: "modal" } } - %span{ "aria-hidden": "true" } × %h4#custom-notifications-title.modal-title #{ _('Custom notification events') } + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + %span{ "aria-hidden": true } × .modal-body .container-fluid @@ -23,8 +23,8 @@ - field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]" .form-group .form-check{ class: ("prepend-top-0" if index == 0) } - %label{ for: field_id } - = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.public_send(event)) + = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event form-check-input", checked: notification_setting.public_send(event)) + %label.form-check-label{ for: field_id } %strong = notification_event_name(event) = icon("spinner spin", class: "custom-notification-event-loading") diff --git a/app/views/shared/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml index 660123d8b07..0337680d79b 100644 --- a/app/views/shared/runners/_form.html.haml +++ b/app/views/shared/runners/_form.html.haml @@ -4,26 +4,26 @@ = label :active, "Active", class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.check_box :active + = f.check_box :active, { class: 'form-check-input' } %span.light Paused Runners don't accept new jobs .form-group.row = label :protected, "Protected", class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.check_box :access_level, {}, 'ref_protected', 'not_protected' + = f.check_box :access_level, { class: 'form-check-input' }, 'ref_protected', 'not_protected' %span.light This runner will only run on pipelines triggered on protected branches .form-group.row = label :run_untagged, 'Run untagged jobs', class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.check_box :run_untagged + = f.check_box :run_untagged, { class: 'form-check-input' } %span.light Indicates whether this runner can pick jobs without tags - unless runner.group_type? .form-group.row = label :locked, _('Lock to current projects'), class: 'col-form-label col-sm-2' .col-sm-10 .form-check - = f.check_box :locked + = f.check_box :locked, { class: 'form-check-input' } %span.light= _('When a runner is locked, it cannot be assigned to other projects') .form-group.row = label_tag :token, class: 'col-form-label col-sm-2' do diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index ddf54866b99..828ec870dc0 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -32,10 +32,10 @@ - if public_snippet? .embed-snippet .input-group - .input-group-btn - %button.btn.embed-toggle{ 'data-toggle': 'dropdown', type: 'button' } + .input-group-prepend + %button.btn.btn-svg.embed-toggle.input-group-text{ 'data-toggle': 'dropdown', type: 'button' } %span.js-embed-action= _("Embed") - = sprite_icon('angle-down', size: 12) + = sprite_icon('angle-down', size: 12, css_class: 'caret-down') %ul.dropdown-menu.dropdown-menu-selectable.embed-toggle-list %li %button.js-embed-btn.btn.btn-transparent.is-active{ type: 'button' } @@ -44,7 +44,6 @@ %button.js-share-btn.btn.btn-transparent{ type: 'button' } %strong.embed-toggle-list-item= _("Share") %input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed } - .input-group-btn - %button.js-clipboard-btn.snippet-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '.js-snippet-url-area' } - = sprite_icon('duplicate', size: 16) + .input-group-append + = clipboard_button(title: s_('Copy to clipboard'), class: 'js-clipboard-btn snippet-clipboard-btn btn btn-default', target: '.js-snippet-url-area') .clearfix diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index e036b21b23f..5069e2e4ca6 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -28,7 +28,7 @@ = link_to user_snippets_path(snippet.author) do = snippet.author_name - if link_project && snippet.project_id? - %span.d-none.d-sm-block + %span.d-none.d-sm-inline-block in = link_to project_path(snippet.project) do = snippet.project.full_name diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 0d1c007dd78..660769fa50d 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -10,80 +10,70 @@ Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header. .form-group = form.label :url, 'Trigger', class: 'label-light' - %ul.list-unstyled + %ul.list-unstyled.prepend-left-20 %li - = form.check_box :push_events, class: 'float-left' - .prepend-left-20 - = form.label :push_events, class: 'list-label' do - %strong Push events - %p.light - This URL will be triggered by a push to the repository + = form.check_box :push_events, class: 'form-check-input' + = form.label :push_events, class: 'list-label form-check-label ml-1' do + %strong Push events + %p.light.ml-1 + This URL will be triggered by a push to the repository %li - = form.check_box :tag_push_events, class: 'float-left' - .prepend-left-20 - = form.label :tag_push_events, class: 'list-label' do - %strong Tag push events - %p.light - This URL will be triggered when a new tag is pushed to the repository + = form.check_box :tag_push_events, class: 'form-check-input' + = form.label :tag_push_events, class: 'list-label form-check-label ml-1' do + %strong Tag push events + %p.light.ml-1 + This URL will be triggered when a new tag is pushed to the repository %li - = form.check_box :note_events, class: 'float-left' - .prepend-left-20 - = form.label :note_events, class: 'list-label' do - %strong Comments - %p.light - This URL will be triggered when someone adds a comment + = form.check_box :note_events, class: 'form-check-input' + = form.label :note_events, class: 'list-label form-check-label ml-1' do + %strong Comments + %p.light.ml-1 + This URL will be triggered when someone adds a comment %li - = form.check_box :confidential_note_events, class: 'float-left' - .prepend-left-20 - = form.label :confidential_note_events, class: 'list-label' do - %strong Confidential Comments - %p.light - This URL will be triggered when someone adds a comment on a confidential issue + = form.check_box :confidential_note_events, class: 'form-check-input' + = form.label :confidential_note_events, class: 'list-label form-check-label ml-1' do + %strong Confidential Comments + %p.light.ml-1 + This URL will be triggered when someone adds a comment on a confidential issue %li - = form.check_box :issues_events, class: 'float-left' - .prepend-left-20 - = form.label :issues_events, class: 'list-label' do - %strong Issues events - %p.light - This URL will be triggered when an issue is created/updated/merged + = form.check_box :issues_events, class: 'form-check-input' + = form.label :issues_events, class: 'list-label form-check-label ml-1' do + %strong Issues events + %p.light.ml-1 + This URL will be triggered when an issue is created/updated/merged %li - = form.check_box :confidential_issues_events, class: 'float-left' - .prepend-left-20 - = form.label :confidential_issues_events, class: 'list-label' do - %strong Confidential Issues events - %p.light - This URL will be triggered when a confidential issue is created/updated/merged + = form.check_box :confidential_issues_events, class: 'form-check-input' + = form.label :confidential_issues_events, class: 'list-label form-check-label ml-1' do + %strong Confidential Issues events + %p.light.ml-1 + This URL will be triggered when a confidential issue is created/updated/merged %li - = form.check_box :merge_requests_events, class: 'float-left' - .prepend-left-20 - = form.label :merge_requests_events, class: 'list-label' do - %strong Merge request events - %p.light - This URL will be triggered when a merge request is created/updated/merged + = form.check_box :merge_requests_events, class: 'form-check-input' + = form.label :merge_requests_events, class: 'list-label form-check-label ml-1' do + %strong Merge request events + %p.light.ml-1 + This URL will be triggered when a merge request is created/updated/merged %li - = form.check_box :job_events, class: 'float-left' - .prepend-left-20 - = form.label :job_events, class: 'list-label' do - %strong Job events - %p.light - This URL will be triggered when the job status changes + = form.check_box :job_events, class: 'form-check-input' + = form.label :job_events, class: 'list-label form-check-label ml-1' do + %strong Job events + %p.light.ml-1 + This URL will be triggered when the job status changes %li - = form.check_box :pipeline_events, class: 'float-left' - .prepend-left-20 - = form.label :pipeline_events, class: 'list-label' do - %strong Pipeline events - %p.light - This URL will be triggered when the pipeline status changes + = form.check_box :pipeline_events, class: 'form-check-input' + = form.label :pipeline_events, class: 'list-label form-check-label ml-1' do + %strong Pipeline events + %p.light.ml-1 + This URL will be triggered when the pipeline status changes %li - = form.check_box :wiki_page_events, class: 'float-left' - .prepend-left-20 - = form.label :wiki_page_events, class: 'list-label' do - %strong Wiki Page events - %p.light - This URL will be triggered when a wiki page is created/updated + = form.check_box :wiki_page_events, class: 'form-check-input' + = form.label :wiki_page_events, class: 'list-label form-check-label ml-1' do + %strong Wiki Page events + %p.light.ml-1 + This URL will be triggered when a wiki page is created/updated .form-group = form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox' .form-check - = form.label :enable_ssl_verification do - = form.check_box :enable_ssl_verification + = form.check_box :enable_ssl_verification, class: 'form-check-input' + = form.label :enable_ssl_verification, class: 'form-check-label ml-1' do %strong Enable SSL verification diff --git a/app/views/sherlock/queries/_backtrace.html.haml b/app/views/sherlock/queries/_backtrace.html.haml index 4f5146cefb9..38b4d2c6102 100644 --- a/app/views/sherlock/queries/_backtrace.html.haml +++ b/app/views/sherlock/queries/_backtrace.html.haml @@ -3,7 +3,7 @@ .card-header %strong = t('sherlock.application_backtrace') - %ul.well-list + %ul.content-list - @query.application_backtrace.each do |location| %li %strong @@ -19,7 +19,7 @@ .card-header %strong = t('sherlock.full_backtrace') - %ul.well-list + %ul.content-list - @query.backtrace.each do |location| %li - if location.application? diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml index 34c0cc4da39..37747faed62 100644 --- a/app/views/sherlock/queries/_general.html.haml +++ b/app/views/sherlock/queries/_general.html.haml @@ -3,7 +3,7 @@ .card-header %strong = t('sherlock.general') - %ul.well-list + %ul.content-list %li %span.light #{t('sherlock.time')}: @@ -32,7 +32,7 @@ = @query.formatted_query %strong = t('sherlock.query') - %ul.well-list + %ul.content-list %li .code.js-syntax-highlight.sherlock-code :preserve @@ -47,7 +47,7 @@ = @query.explain %strong = t('sherlock.query_plan') - %ul.well-list + %ul.content-list %li .code.js-syntax-highlight.sherlock-code %pre diff --git a/app/views/sherlock/transactions/_general.html.haml b/app/views/sherlock/transactions/_general.html.haml index 7ec8dde8421..9c028b5c741 100644 --- a/app/views/sherlock/transactions/_general.html.haml +++ b/app/views/sherlock/transactions/_general.html.haml @@ -3,7 +3,7 @@ .card-header %strong = t('sherlock.general') - %ul.well-list + %ul.content-list %li %span.light #{t('sherlock.id')}: diff --git a/app/views/users/terms/index.html.haml b/app/views/users/terms/index.html.haml index c5406696bdd..e0fe551cf36 100644 --- a/app/views/users/terms/index.html.haml +++ b/app/views/users/terms/index.html.haml @@ -4,10 +4,10 @@ = markdown_field(@term, :terms) .row-content-block.footer-block.clearfix - if can?(current_user, :accept_terms, @term) - .pull-right + .float-right = button_to accept_term_path(@term, redirect_params), class: 'btn btn-success prepend-left-8' do = _('Accept terms') - if can?(current_user, :decline_terms, @term) - .pull-right + .float-right = button_to decline_term_path(@term, redirect_params), class: 'btn btn-default prepend-left-8' do = _('Decline and sign out') |