summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/badges/components/badge.vue2
-rw-r--r--app/assets/javascripts/boards/index.js149
-rw-r--r--app/assets/javascripts/commons/gitlab_ui.js4
-rw-r--r--app/assets/javascripts/commons/index.js1
-rw-r--r--app/assets/javascripts/ide/components/changed_file_icon.vue20
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue3
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue10
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide_review.vue13
-rw-r--r--app/assets/javascripts/ide/components/ide_tree.vue8
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue6
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue52
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue17
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue8
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue6
-rw-r--r--app/assets/javascripts/ide/constants.js15
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js2
-rw-r--r--app/assets/javascripts/ide/services/index.js4
-rw-r--r--app/assets/javascripts/ide/stores/actions.js8
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js8
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js12
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js26
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js11
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js20
-rw-r--r--app/assets/javascripts/ide/stores/utils.js29
-rw-r--r--app/assets/javascripts/ide/utils.js12
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue19
-rw-r--r--app/assets/javascripts/vue_shared/components/bar_chart.vue391
-rw-r--r--app/assets/javascripts/vue_shared/components/bar_chart_constants.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/panel_resizer.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/reports/constants.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/reports/issue_body.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/reports/issue_status_icon.vue58
-rw-r--r--app/assets/javascripts/vue_shared/components/reports/issues_list.vue64
-rw-r--r--app/assets/javascripts/vue_shared/components/reports/report_issues.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/reports/report_section.vue31
-rw-r--r--app/assets/javascripts/vue_shared/components/svg_gradient.vue37
-rw-r--r--app/assets/stylesheets/framework/common.scss7
-rw-r--r--app/assets/stylesheets/framework/header.scss3
-rw-r--r--app/assets/stylesheets/framework/typography.scss4
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss (renamed from app/assets/stylesheets/pages/repo.scss)79
-rw-r--r--app/assets/stylesheets/pages/builds.scss1
-rw-r--r--app/assets/stylesheets/pages/diff.scss1
-rw-r--r--app/assets/stylesheets/pages/graph.scss58
-rw-r--r--app/assets/stylesheets/pages/issuable.scss22
-rw-r--r--app/assets/stylesheets/pages/login.scss2
-rw-r--r--app/assets/stylesheets/snippets.scss4
-rw-r--r--app/controllers/projects/commits_controller.rb10
-rw-r--r--app/controllers/projects/labels_controller.rb11
-rw-r--r--app/controllers/projects/milestones_controller.rb11
-rw-r--r--app/finders/admin/projects_finder.rb4
-rw-r--r--app/graphql/gitlab_schema.rb2
-rw-r--r--app/graphql/mutations/base_mutation.rb13
-rw-r--r--app/graphql/mutations/concerns/mutations/resolves_project.rb13
-rw-r--r--app/graphql/mutations/merge_requests/base.rb32
-rw-r--r--app/graphql/mutations/merge_requests/set_wip.rb35
-rw-r--r--app/graphql/types/mutation_type.rb6
-rw-r--r--app/helpers/environments_helper.rb19
-rw-r--r--app/helpers/hooks_helper.rb2
-rw-r--r--app/helpers/snippets_helper.rb2
-rw-r--r--app/mailers/notify.rb4
-rw-r--r--app/models/clusters/applications/prometheus.rb8
-rw-r--r--app/models/concerns/prometheus_adapter.rb15
-rw-r--r--app/models/concerns/reactive_caching.rb10
-rw-r--r--app/models/concerns/routable.rb44
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb13
-rw-r--r--app/models/deploy_token.rb5
-rw-r--r--app/models/email.rb4
-rw-r--r--app/models/namespace.rb1
-rw-r--r--app/models/project.rb8
-rw-r--r--app/models/project_feature.rb26
-rw-r--r--app/models/remote_mirror.rb6
-rw-r--r--app/models/repository.rb2
-rw-r--r--app/models/site_statistic.rb74
-rw-r--r--app/policies/concerns/policy_actor.rb36
-rw-r--r--app/serializers/pipeline_serializer.rb9
-rw-r--r--app/services/clusters/applications/check_installation_progress_service.rb6
-rw-r--r--app/services/projects/transfer_service.rb1
-rw-r--r--app/services/prometheus/adapter_service.rb2
-rw-r--r--app/views/admin/projects/_projects.html.haml2
-rw-r--r--app/views/ide/index.html.haml3
-rw-r--r--app/views/layouts/_head.html.haml5
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml4
-rw-r--r--app/views/projects/environments/metrics.html.haml22
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml2
-rw-r--r--app/views/shared/snippets/_embed.html.haml2
-rw-r--r--app/workers/concerns/gitlab/github_import/object_importer.rb2
-rw-r--r--app/workers/repository_fork_worker.rb4
-rw-r--r--app/workers/repository_import_worker.rb4
101 files changed, 1385 insertions, 438 deletions
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue
index b4bfaee1d85..155c348286c 100644
--- a/app/assets/javascripts/badges/components/badge.vue
+++ b/app/assets/javascripts/badges/components/badge.vue
@@ -93,7 +93,7 @@ export default {
<icon
:size="16"
class="prepend-left-8 append-right-8"
- name="doc_image"
+ name="doc-image"
aria-hidden="true"
/>
</div>
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 200d1923635..bc263cbbfea 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -1,5 +1,3 @@
-/* eslint-disable quote-props, comma-dangle */
-
import $ from 'jquery';
import _ from 'underscore';
import Vue from 'vue';
@@ -47,7 +45,7 @@ export default () => {
gl.IssueBoardsApp = new Vue({
el: $boardApp,
components: {
- 'board': gl.issueBoards.Board,
+ board: gl.issueBoards.Board,
'board-sidebar': gl.issueBoards.BoardSidebar,
BoardAddIssuesModal,
},
@@ -65,11 +63,11 @@ export default () => {
defaultAvatar: $boardApp.dataset.defaultAvatar,
},
computed: {
- detailIssueVisible () {
+ detailIssueVisible() {
return Object.keys(this.detailIssue.issue).length;
},
},
- created () {
+ created() {
gl.boardService = new BoardService({
boardsEndpoint: this.boardsEndpoint,
listsEndpoint: this.listsEndpoint,
@@ -89,15 +87,16 @@ export default () => {
eventHub.$off('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
},
- mounted () {
+ mounted() {
this.filterManager = new FilteredSearchBoards(Store.filter, true, Store.cantEdit);
this.filterManager.setup();
Store.disabled = this.disabled;
- gl.boardService.all()
+ gl.boardService
+ .all()
.then(res => res.data)
- .then((data) => {
- data.forEach((board) => {
+ .then(data => {
+ data.forEach(board => {
const list = Store.addList(board, this.defaultAvatar);
if (list.type === 'closed') {
@@ -126,7 +125,7 @@ export default () => {
newIssue.setFetchingState('subscriptions', true);
BoardService.getIssueInfo(sidebarInfoEndpoint)
.then(res => res.data)
- .then((data) => {
+ .then(data => {
newIssue.setFetchingState('subscriptions', false);
newIssue.updateData({
subscribed: data.subscribed,
@@ -159,7 +158,7 @@ export default () => {
Flash(__('An error occurred when toggling the notification subscription'));
});
}
- }
+ },
},
});
@@ -168,77 +167,81 @@ export default () => {
data: {
filters: Store.state.filters,
},
- mounted () {
+ mounted() {
gl.issueBoards.newListDropdownInit();
},
});
- gl.IssueBoardsModalAddBtn = new Vue({
- el: document.getElementById('js-add-issues-btn'),
- mixins: [modalMixin],
- data() {
- return {
- modal: ModalStore.store,
- store: Store.state,
- canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
- };
- },
- computed: {
- disabled() {
- if (!this.store) {
- return true;
- }
- return !this.store.lists.filter(list => !list.preset).length;
+ const issueBoardsModal = document.getElementById('js-add-issues-btn');
+
+ if (issueBoardsModal) {
+ gl.IssueBoardsModalAddBtn = new Vue({
+ el: issueBoardsModal,
+ mixins: [modalMixin],
+ data() {
+ return {
+ modal: ModalStore.store,
+ store: Store.state,
+ canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
+ };
},
- tooltipTitle() {
- if (this.disabled) {
- return 'Please add a list to your board first';
- }
+ computed: {
+ disabled() {
+ if (!this.store) {
+ return true;
+ }
+ return !this.store.lists.filter(list => !list.preset).length;
+ },
+ tooltipTitle() {
+ if (this.disabled) {
+ return 'Please add a list to your board first';
+ }
- return '';
+ return '';
+ },
},
- },
- watch: {
- disabled() {
+ watch: {
+ disabled() {
+ this.updateTooltip();
+ },
+ },
+ mounted() {
this.updateTooltip();
},
- },
- mounted() {
- this.updateTooltip();
- },
- methods: {
- updateTooltip() {
- const $tooltip = $(this.$refs.addIssuesButton);
-
- this.$nextTick(() => {
- if (this.disabled) {
- $tooltip.tooltip();
- } else {
- $tooltip.tooltip('dispose');
+ methods: {
+ updateTooltip() {
+ const $tooltip = $(this.$refs.addIssuesButton);
+
+ this.$nextTick(() => {
+ if (this.disabled) {
+ $tooltip.tooltip();
+ } else {
+ $tooltip.tooltip('dispose');
+ }
+ });
+ },
+ openModal() {
+ if (!this.disabled) {
+ this.toggleModal(true);
}
- });
- },
- openModal() {
- if (!this.disabled) {
- this.toggleModal(true);
- }
+ },
},
- },
- template: `
- <div class="board-extra-actions">
- <button
- class="btn btn-create prepend-left-10"
- type="button"
- data-placement="bottom"
- ref="addIssuesButton"
- :class="{ 'disabled': disabled }"
- :title="tooltipTitle"
- :aria-disabled="disabled"
- v-if="canAdminList"
- @click="openModal">
- Add issues
- </button>
- </div>
- `,
- });
+ template: `
+ <div class="board-extra-actions">
+ <button
+ class="btn btn-create prepend-left-10"
+ type="button"
+ data-placement="bottom"
+ ref="addIssuesButton"
+ :class="{ 'disabled': disabled }"
+ :title="tooltipTitle"
+ :aria-disabled="disabled"
+ v-if="canAdminList"
+ @click="openModal">
+ Add issues
+ </button>
+ </div>
+ `,
+ });
+ }
};
diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js
new file mode 100644
index 00000000000..923c036f5a4
--- /dev/null
+++ b/app/assets/javascripts/commons/gitlab_ui.js
@@ -0,0 +1,4 @@
+import Vue from 'vue';
+import progressBar from '@gitlab-org/gitlab-ui/dist/base/progress_bar';
+
+Vue.component('gl-progress-bar', progressBar);
diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js
index 0d2fe2925d8..ea945cd3fa5 100644
--- a/app/assets/javascripts/commons/index.js
+++ b/app/assets/javascripts/commons/index.js
@@ -3,4 +3,5 @@ import './polyfills';
import './jquery';
import './bootstrap';
import './vue';
+import './gitlab_ui';
import '../lib/utils/axios_utils';
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue
index a4e06bbbe3c..720ae11aaa6 100644
--- a/app/assets/javascripts/ide/components/changed_file_icon.vue
+++ b/app/assets/javascripts/ide/components/changed_file_icon.vue
@@ -3,6 +3,7 @@ import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
+import { getCommitIconMap } from '../utils';
export default {
components: {
@@ -34,16 +35,14 @@ export default {
},
computed: {
changedIcon() {
- const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : '';
- return this.file.tempFile && !this.forceModifiedIcon
- ? `file-addition${suffix}`
- : `file-modified${suffix}`;
- },
- stagedIcon() {
- return `${this.changedIcon}-solid`;
+ const suffix = !this.file.changed && this.file.staged && !this.showStagedIcon ? '-solid' : '';
+
+ if (this.forceModifiedIcon) return `file-modified${suffix}`;
+
+ return `${getCommitIconMap(this.file).icon}${suffix}`;
},
changedIconClass() {
- return `multi-${this.changedIcon} float-left`;
+ return `ide-${this.changedIcon} float-left`;
},
tooltipTitle() {
if (!this.showTooltip) return undefined;
@@ -66,6 +65,9 @@ export default {
return undefined;
},
+ showIcon() {
+ return this.file.changed || this.file.tempFile || this.file.staged || this.file.deleted;
+ },
},
};
</script>
@@ -79,7 +81,7 @@ export default {
class="ide-file-changed-icon"
>
<icon
- v-if="file.changed || file.tempFile || file.staged"
+ v-if="showIcon"
:name="changedIcon"
:size="12"
:css-classes="changedIconClass"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index eb7cb9745ec..a8b5c7a16d0 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -1,4 +1,5 @@
<script>
+import _ from 'underscore';
import { mapActions, mapState, mapGetters } from 'vuex';
import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants';
@@ -14,7 +15,7 @@ export default {
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
- { branchName: `<strong class="monospace">${this.currentBranchId}</strong>` },
+ { branchName: `<strong class="monospace">${_.escape(this.currentBranchId)}</strong>` },
false,
);
},
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index ee21eeda3cd..391004dcd3c 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue';
import { viewerTypes } from '../../constants';
+import { getCommitIconMap } from '../../utils';
export default {
components: {
@@ -42,11 +43,12 @@ export default {
},
computed: {
iconName() {
- const prefix = this.stagedList ? '-solid' : '';
- return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`;
+ const suffix = this.stagedList ? '-solid' : '';
+
+ return `${getCommitIconMap(this.file).icon}${suffix}`;
},
iconClass() {
- return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
+ return `${getCommitIconMap(this.file).class} append-right-8`;
},
fullKey() {
return `${this.keyPrefix}-${this.file.key}`;
@@ -67,6 +69,8 @@ export default {
'stageChange',
]),
openFileInEditor() {
+ if (this.file.type === 'tree') return null;
+
return this.openPendingTab({
file: this.file,
keyPrefix: this.keyPrefix,
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
index 7014b9f605e..e6044401c9f 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
@@ -56,7 +56,7 @@ export default {
>
<icon
:size="12"
- name="more"
+ name="ellipsis_h"
/>
</button>
<div class="dropdown-menu dropdown-menu-right">
diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue
index f9978762c45..d09c99050fe 100644
--- a/app/assets/javascripts/ide/components/ide_review.vue
+++ b/app/assets/javascripts/ide/components/ide_review.vue
@@ -10,7 +10,7 @@ export default {
EditorModeDropdown,
},
computed: {
- ...mapGetters(['currentMergeRequest']),
+ ...mapGetters(['currentMergeRequest', 'activeFile']),
...mapState(['viewer', 'currentMergeRequestId']),
showLatestChangesText() {
return !this.currentMergeRequestId || this.viewer === viewerTypes.diff;
@@ -23,12 +23,20 @@ export default {
},
},
mounted() {
+ if (this.activeFile && this.activeFile.pending && !this.activeFile.deleted) {
+ this.$router.push(`/project${this.activeFile.url}`, () => {
+ this.updateViewer('editor');
+ });
+ } else if (this.activeFile && this.activeFile.deleted) {
+ this.resetOpenFiles();
+ }
+
this.$nextTick(() => {
this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff);
});
},
methods: {
- ...mapActions(['updateViewer']),
+ ...mapActions(['updateViewer', 'resetOpenFiles']),
},
};
</script>
@@ -36,7 +44,6 @@ export default {
<template>
<ide-tree-list
:viewer-type="viewer"
- :disable-action-dropdown="true"
header-class="ide-review-header"
>
<template
diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue
index 0a95c0bb30d..e996dd9059e 100644
--- a/app/assets/javascripts/ide/components/ide_tree.vue
+++ b/app/assets/javascripts/ide/components/ide_tree.vue
@@ -17,14 +17,18 @@ export default {
...mapGetters(['currentProject', 'currentTree', 'activeFile']),
},
mounted() {
- if (this.activeFile && this.activeFile.pending) {
+ if (!this.activeFile) return;
+
+ if (this.activeFile.pending && !this.activeFile.deleted) {
this.$router.push(`/project${this.activeFile.url}`, () => {
this.updateViewer('editor');
});
+ } else if (this.activeFile.deleted) {
+ this.resetOpenFiles();
}
},
methods: {
- ...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry']),
+ ...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry', 'resetOpenFiles']),
},
};
</script>
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index 0df99798d21..2e7226b727c 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -22,11 +22,6 @@ export default {
required: false,
default: null,
},
- disableActionDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
},
computed: {
...mapState(['currentBranchId']),
@@ -69,7 +64,6 @@ export default {
:key="file.key"
:file="file"
:level="0"
- :disable-action-dropdown="disableActionDropdown"
/>
</template>
</div>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index c29e49ba766..440e480d596 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -13,7 +13,7 @@ export default {
ItemButton,
},
props: {
- branch: {
+ type: {
type: String,
required: true,
},
@@ -45,7 +45,7 @@ export default {
},
},
methods: {
- ...mapActions(['createTempEntry', 'openNewEntryModal']),
+ ...mapActions(['createTempEntry', 'openNewEntryModal', 'deleteEntry']),
createNewItem(type) {
this.openNewEntryModal({ type, path: this.path });
this.dropdownOpen = false;
@@ -82,28 +82,40 @@ export default {
ref="dropdownMenu"
class="dropdown-menu dropdown-menu-right"
>
+ <template v-if="type === 'tree'">
+ <li>
+ <item-button
+ :label="__('New file')"
+ class="d-flex"
+ icon="doc-new"
+ icon-classes="mr-2"
+ @click="createNewItem('blob')"
+ />
+ </li>
+ <li>
+ <upload
+ :path="path"
+ @create="createTempEntry"
+ />
+ </li>
+ <li>
+ <item-button
+ :label="__('New directory')"
+ class="d-flex"
+ icon="folder-new"
+ icon-classes="mr-2"
+ @click="createNewItem('tree')"
+ />
+ </li>
+ <li class="divider"></li>
+ </template>
<li>
<item-button
- :label="__('New file')"
+ :label="__('Delete')"
class="d-flex"
- icon="doc-new"
+ icon="remove"
icon-classes="mr-2"
- @click="createNewItem('blob')"
- />
- </li>
- <li>
- <upload
- :path="path"
- @create="createTempEntry"
- />
- </li>
- <li>
- <item-button
- :label="__('New directory')"
- class="d-flex"
- icon="folder-new"
- icon-classes="mr-2"
- @click="createNewItem('tree')"
+ @click="deleteEntry(path)"
/>
</li>
</ul>
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 50ab242ba2a..6f1a941fbc4 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -44,7 +44,7 @@ export default {
},
},
mounted() {
- if (this.lastOpenedFile) {
+ if (this.lastOpenedFile && this.lastOpenedFile.type !== 'tree') {
this.openPendingTab({
file: this.lastOpenedFile,
keyPrefix: this.lastOpenedFile.changed ? stageKeys.unstaged : stageKeys.staged,
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 08ee12fd98f..f9badb01535 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -87,7 +87,9 @@ export default {
this.editor.updateDimensions();
},
viewer() {
- this.createEditorInstance();
+ if (!this.file.pending) {
+ this.createEditorInstance();
+ }
},
panelResizing() {
if (!this.panelResizing) {
@@ -109,6 +111,7 @@ export default {
},
methods: {
...mapActions([
+ 'getFileData',
'getRawFileData',
'changeFileContent',
'setFileLanguage',
@@ -123,10 +126,16 @@ export default {
this.editor.clearEditor();
- this.getRawFileData({
+ this.getFileData({
path: this.file.path,
- baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
+ makeFileActive: false,
})
+ .then(() =>
+ this.getRawFileData({
+ path: this.file.path,
+ baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
+ }),
+ )
.then(() => {
this.createEditorInstance();
})
@@ -246,6 +255,8 @@ export default {
ref="editor"
:class="{
'is-readonly': isCommitModeActive,
+ 'is-deleted': file.deleted,
+ 'is-added': file.tempFile
}"
class="multi-file-editor-holder"
>
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 3b4dd5ae9aa..eb4a927fe0d 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -34,11 +34,6 @@ export default {
type: Number,
required: true,
},
- disableActionDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
},
data() {
return {
@@ -212,8 +207,7 @@ export default {
/>
</span>
<new-dropdown
- v-if="isTree && !disableActionDropdown"
- :project-id="file.projectId"
+ :type="file.type"
:branch="file.branchId"
:path="file.path"
:mouse-over="mouseOver"
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index 03772ae4a4c..db47b75ec5c 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -37,7 +37,7 @@ export default {
return this.fileHasChanged ? !this.tabMouseOver : false;
},
fileHasChanged() {
- return this.tab.changed || this.tab.tempFile || this.tab.staged;
+ return this.tab.changed || this.tab.tempFile || this.tab.staged || this.tab.deleted;
},
},
@@ -71,7 +71,8 @@ export default {
<template>
<li
:class="{
- active: tab.active
+ active: tab.active,
+ disabled: tab.pending
}"
@click="clickFile(tab)"
@mouseover="mouseOverTab"
@@ -105,7 +106,6 @@ export default {
<changed-file-icon
v-else
:file="tab"
- :force-modified-icon="true"
/>
</button>
</li>
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 45d36f6f42c..0b514f31467 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -38,3 +38,18 @@ export const stageKeys = {
unstaged: 'unstaged',
staged: 'staged',
};
+
+export const commitItemIconMap = {
+ addition: {
+ icon: 'file-addition',
+ class: 'ide-file-addition',
+ },
+ modified: {
+ icon: 'file-modified',
+ class: 'ide-file-modified',
+ },
+ deleted: {
+ icon: 'file-deletion',
+ class: 'ide-file-deletion',
+ },
+};
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index 78e6f632728..60bddb34977 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -7,7 +7,7 @@ export default class Model {
this.disposable = new Disposable();
this.file = file;
this.head = head;
- this.content = file.content !== '' ? file.content : file.raw;
+ this.content = file.content !== '' || file.deleted ? file.content : file.raw;
this.disposable.add(
(this.originalModel = monacoEditor.createModel(
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 49a481f25d5..cb93fba1665 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -18,7 +18,7 @@ export default {
return axios
.get(file.rawPath, {
- params: { format: 'json' },
+ transformResponse: [f => f],
})
.then(({ data }) => data);
},
@@ -33,7 +33,7 @@ export default {
return axios
.get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), {
- params: { format: 'json' },
+ transformResponse: [f => f],
})
.then(({ data }) => data);
},
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index b5bd6f5a6bb..2765acada48 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -185,6 +185,14 @@ export const openNewEntryModal = ({ commit }, { type, path = '' }) => {
$('#ide-new-entry').modal('show');
};
+export const deleteEntry = ({ commit, dispatch, state }, path) => {
+ dispatch('burstUnusedSeal');
+ dispatch('closeFile', state.entries[path]);
+ commit(types.DELETE_ENTRY, path);
+};
+
+export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
+
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 6c0887e11ee..b343750f789 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -61,7 +61,11 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => {
const file = state.entries[path];
+
+ if (file.raw || file.tempFile) return Promise.resolve();
+
commit(types.TOGGLE_LOADING, { entry: file });
+
return service
.getFileData(
`${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`,
@@ -71,7 +75,7 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE']));
commit(types.SET_FILE_DATA, { data, file });
- commit(types.TOGGLE_FILE_OPEN, path);
+ if (makeFileActive) commit(types.TOGGLE_FILE_OPEN, path);
if (makeFileActive) dispatch('setFileActive', path);
commit(types.TOGGLE_LOADING, { entry: file });
})
@@ -97,7 +101,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) =
service
.getRawFileData(file)
.then(raw => {
- commit(types.SET_FILE_RAW_DATA, { file, raw });
+ if (!file.tempFile) commit(types.SET_FILE_RAW_DATA, { file, raw });
if (file.mrChange && file.mrChange.new_file === false) {
service
.getBaseRawFileData(file, baseSha)
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index ffaaaabff17..acb6ef5e6d4 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -21,14 +21,12 @@ export const showTreeEntry = ({ commit, dispatch, state }, path) => {
export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
if (row.type === 'tree') {
dispatch('toggleTreeOpen', row.path);
- } else if (row.type === 'blob' && (row.opened || row.changed)) {
- if (row.changed && !row.opened) {
+ } else if (row.type === 'blob') {
+ if (!row.opened) {
commit(types.TOGGLE_FILE_OPEN, row.path);
}
dispatch('setFileActive', row.path);
- } else {
- dispatch('getFileData', { path: row.path });
}
dispatch('showTreeEntry', row.path);
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 7828c31f20e..462ca45db9b 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -174,11 +174,13 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
dispatch('updateActivityBarView', activityBarViews.edit, { root: true });
dispatch('updateViewer', 'editor', { root: true });
- router.push(
- `/project/${rootState.currentProjectId}/blob/${getters.branchName}/-/${
- rootGetters.activeFile.path
- }`,
- );
+ if (rootGetters.activeFile) {
+ router.push(
+ `/project/${rootState.currentProjectId}/blob/${getters.branchName}/-/${
+ rootGetters.activeFile.path
+ }`,
+ );
+ }
}
})
.then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH))
diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js
index 3db4b2f903e..03777e6c10b 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js
@@ -1,7 +1,15 @@
-import { sprintf, n__ } from '../../../../locale';
+import { sprintf, n__, __ } from '../../../../locale';
import * as consts from './constants';
const BRANCH_SUFFIX_COUNT = 5;
+const createTranslatedTextForFiles = (files, text) => {
+ if (!files.length) return null;
+
+ return sprintf(n__('%{text} %{files}', '%{text} %{files} files', files.length), {
+ files: files.reduce((acc, val) => acc.concat(val.path), []).join(', '),
+ text,
+ });
+};
export const discardDraftButtonDisabled = state =>
state.commitMessage === '' || state.submitCommitLoading;
@@ -29,14 +37,16 @@ export const branchName = (state, getters, rootState) => {
export const preBuiltCommitMessage = (state, _, rootState) => {
if (state.commitMessage) return state.commitMessage;
- const files = (rootState.stagedFiles.length
- ? rootState.stagedFiles
- : rootState.changedFiles
- ).reduce((acc, val) => acc.concat(val.path), []);
+ const files = rootState.stagedFiles.length ? rootState.stagedFiles : rootState.changedFiles;
+ const modifiedFiles = files.filter(f => !f.deleted);
+ const deletedFiles = files.filter(f => f.deleted);
- return sprintf(n__('Update %{files}', 'Update %{files} files', files.length), {
- files: files.join(', '),
- });
+ return [
+ createTranslatedTextForFiles(modifiedFiles, __('Update')),
+ createTranslatedTextForFiles(deletedFiles, __('Deleted')),
+ ]
+ .filter(t => t)
+ .join('\n');
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index 8d6f9ccaf34..dae60f4d65a 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -76,3 +76,4 @@ export const RESET_OPEN_FILES = 'RESET_OPEN_FILES';
export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE';
export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL';
+export const DELETE_ENTRY = 'DELETE_ENTRY';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index f8091f5b5e0..799c2f51e8d 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,3 +1,4 @@
+/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
import projectMutations from './mutations/project';
import mergeRequestMutation from './mutations/merge_request';
@@ -171,6 +172,16 @@ export default {
newEntryModal: { type, path },
});
},
+ [types.DELETE_ENTRY](state, path) {
+ const entry = state.entries[path];
+ const parent = entry.parentPath
+ ? state.entries[entry.parentPath]
+ : state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
+
+ entry.deleted = true;
+ state.changedFiles = state.changedFiles.concat(entry);
+ parent.tree = parent.tree.filter(f => f.path !== entry.path);
+ },
...projectMutations,
...mergeRequestMutation,
...fileMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 46547820425..9a87d50d6d5 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
import * as types from '../mutation_types';
+import { sortTree } from '../utils';
import { diffModes } from '../../constants';
export default {
@@ -51,9 +52,17 @@ export default {
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
+ const openPendingFile = state.openFiles.find(
+ f => f.path === file.path && f.pending && !f.tempFile,
+ );
+
Object.assign(state.entries[file.path], {
raw,
});
+
+ if (openPendingFile) {
+ openPendingFile.raw = raw;
+ }
},
[types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
Object.assign(state.entries[file.path], {
@@ -109,11 +118,22 @@ export default {
},
[types.DISCARD_FILE_CHANGES](state, path) {
const stagedFile = state.stagedFiles.find(f => f.path === path);
+ const entry = state.entries[path];
+ const { deleted } = entry;
Object.assign(state.entries[path], {
content: stagedFile ? stagedFile.content : state.entries[path].raw,
changed: false,
+ deleted: false,
});
+
+ if (deleted) {
+ const parent = entry.parentPath
+ ? state.entries[entry.parentPath]
+ : state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
+
+ parent.tree = sortTree(parent.tree.concat(entry));
+ }
},
[types.ADD_FILE_TO_CHANGED](state, path) {
Object.assign(state, {
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 9e6b86dd844..bf7ab93ff5e 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -46,6 +46,7 @@ export const dataStructure = () => ({
parentPath: null,
lastOpenedAt: 0,
mrChange: null,
+ deleted: false,
});
export const decorateData = entity => {
@@ -105,15 +106,37 @@ export const setPageTitle = title => {
document.title = title;
};
+export const commitActionForFile = file => {
+ if (file.deleted) {
+ return 'delete';
+ } else if (file.tempFile) {
+ return 'create';
+ }
+
+ return 'update';
+};
+
+export const getCommitFiles = (stagedFiles, deleteTree = false) =>
+ stagedFiles.reduce((acc, file) => {
+ if ((file.deleted || deleteTree) && file.type === 'tree') {
+ return acc.concat(getCommitFiles(file.tree, true));
+ }
+
+ return acc.concat({
+ ...file,
+ deleted: deleteTree || file.deleted,
+ });
+ }, []);
+
export const createCommitPayload = ({ branch, getters, newBranch, state, rootState }) => ({
branch,
commit_message: state.commitMessage || getters.preBuiltCommitMessage,
- actions: rootState.stagedFiles.map(f => ({
- action: f.tempFile ? 'create' : 'update',
+ actions: getCommitFiles(rootState.stagedFiles).map(f => ({
+ action: commitActionForFile(f),
file_path: f.path,
content: f.content,
encoding: f.base64 ? 'base64' : 'text',
- last_commit_id: newBranch ? undefined : f.lastCommitSha,
+ last_commit_id: newBranch || f.deleted ? undefined : f.lastCommitSha,
})),
start_branch: newBranch ? rootState.currentBranchId : undefined,
});
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
new file mode 100644
index 00000000000..92b15cf232d
--- /dev/null
+++ b/app/assets/javascripts/ide/utils.js
@@ -0,0 +1,12 @@
+import { commitItemIconMap } from './constants';
+
+// eslint-disable-next-line import/prefer-default-export
+export const getCommitIconMap = file => {
+ if (file.deleted) {
+ return commitItemIconMap.deleted;
+ } else if (file.tempFile) {
+ return commitItemIconMap.addition;
+ }
+
+ return commitItemIconMap.modified;
+};
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 c32dc83da8e..14518f86dc7 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -1,5 +1,6 @@
<script>
import $ from 'jquery';
+import _ from 'underscore';
import JobNameComponent from './job_name_component.vue';
import JobComponent from './job_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
@@ -46,7 +47,7 @@ export default {
computed: {
tooltipText() {
- return `${this.job.name} - ${this.job.status.label}`;
+ return _.escape(`${this.job.name} - ${this.job.status.label}`);
},
},
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 4ec67f6c01b..1952dd453f4 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -1,4 +1,5 @@
<script>
+import _ from 'underscore';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import StageColumnComponent from './stage_column_component.vue';
@@ -26,7 +27,8 @@ export default {
methods: {
capitalizeStageName(name) {
- return name.charAt(0).toUpperCase() + name.slice(1);
+ const escapedName = _.escape(name);
+ return escapedName.charAt(0).toUpperCase() + escapedName.slice(1);
},
isFirstColumn(index) {
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index 8af984ef91a..84a3d58b770 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -1,4 +1,5 @@
<script>
+import _ from 'underscore';
import ActionComponent from './action_component.vue';
import JobNameComponent from './job_name_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
@@ -61,7 +62,7 @@ export default {
const textBuilder = [];
if (this.job.name) {
- textBuilder.push(this.job.name);
+ textBuilder.push(_.escape(this.job.name));
}
if (this.job.name && this.status.tooltip) {
@@ -69,7 +70,7 @@ export default {
}
if (this.status.tooltip) {
- textBuilder.push(`${this.job.status.tooltip}`);
+ textBuilder.push(this.job.status.tooltip);
}
return textBuilder.join(' ');
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index 2c728582b7c..e7b2de52f76 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -1,4 +1,5 @@
<script>
+import _ from 'underscore';
import JobComponent from './job_component.vue';
import DropdownJobComponent from './dropdown_job_component.vue';
@@ -37,7 +38,7 @@ export default {
},
jobId(job) {
- return `ci-badge-${job.name}`;
+ return `ci-badge-${_.escape(job.name)}`;
},
buildConnnectorClass(index) {
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
index d335c3f55af..dc599e1b9fc 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
@@ -42,11 +42,14 @@ export default {
return this.timeEstimate - this.timeSpent;
},
timeRemainingPercent() {
- return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`;
+ return Math.floor((this.timeSpent / this.timeEstimate) * 100);
},
timeRemainingStatusClass() {
return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate';
},
+ progressBarVariant() {
+ return this.timeRemainingPercent > 100 ? 'danger' : 'primary';
+ },
},
};
</script>
@@ -62,16 +65,10 @@ export default {
data-placement="top"
role="timeRemainingDisplay"
>
- <div
- :aria-valuenow="timeRemainingPercent"
- class="meter-container"
- >
- <div
- :style="{ width: timeRemainingPercent }"
- class="meter-fill"
- >
- </div>
- </div>
+ <gl-progress-bar
+ :value="timeRemainingPercent"
+ :variant="progressBarVariant"
+ />
<div class="compare-display-container">
<div class="compare-display float-left">
<span class="compare-label">
diff --git a/app/assets/javascripts/vue_shared/components/bar_chart.vue b/app/assets/javascripts/vue_shared/components/bar_chart.vue
new file mode 100644
index 00000000000..3ced4eb691a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/bar_chart.vue
@@ -0,0 +1,391 @@
+<script>
+import * as d3 from 'd3';
+import tooltip from '../directives/tooltip';
+import Icon from './icon.vue';
+import SvgGradient from './svg_gradient.vue';
+import {
+ GRADIENT_COLORS,
+ GRADIENT_OPACITY,
+ INVERSE_GRADIENT_COLORS,
+ INVERSE_GRADIENT_OPACITY,
+} from './bar_chart_constants';
+
+/**
+ * Renders a bar chart that can be dragged(scrolled) when the number
+ * of elements to renders surpasses that of the available viewport space
+ * while keeping even padding and a width of 24px (customizable)
+ *
+ * It can render data with the following format:
+ * graphData: [{
+ * name: 'element' // x domain data
+ * value: 1 // y domain data
+ * }]
+ *
+ * Used in:
+ * - Contribution analytics - all of the rows describing pushes, merge requests and issues
+ */
+
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ SvgGradient,
+ },
+ props: {
+ graphData: {
+ type: Array,
+ required: true,
+ },
+ barWidth: {
+ type: Number,
+ required: false,
+ default: 24,
+ },
+ yAxisLabel: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ minX: -40,
+ minY: 0,
+ vbWidth: 0,
+ vbHeight: 0,
+ vpWidth: 0,
+ vpHeight: 350,
+ preserveAspectRatioType: 'xMidYMid meet',
+ containerMargin: {
+ leftRight: 30,
+ },
+ viewBoxMargin: {
+ topBottom: 150,
+ },
+ panX: 0,
+ xScale: {},
+ yScale: {},
+ zoom: {},
+ bars: {},
+ xGraphRange: 0,
+ isLoading: true,
+ paddingThreshold: 50,
+ showScrollIndicator: false,
+ showLeftScrollIndicator: false,
+ isGrabbed: false,
+ isPanAvailable: false,
+ gradientColors: GRADIENT_COLORS,
+ gradientOpacity: GRADIENT_OPACITY,
+ inverseGradientColors: INVERSE_GRADIENT_COLORS,
+ inverseGradientOpacity: INVERSE_GRADIENT_OPACITY,
+ maxTextWidth: 72,
+ rectYAxisLabelDims: {},
+ xAxisTextElements: {},
+ yAxisRectTransformPadding: 20,
+ yAxisTextTransformPadding: 10,
+ yAxisTextRotation: 90,
+ };
+ },
+ computed: {
+ svgViewBox() {
+ return `${this.minX} ${this.minY} ${this.vbWidth} ${this.vbHeight}`;
+ },
+ xAxisLocation() {
+ return `translate(${this.panX}, ${this.vbHeight})`;
+ },
+ barTranslationTransform() {
+ return `translate(${this.panX}, 0)`;
+ },
+ scrollIndicatorTransform() {
+ return `translate(${this.vbWidth - 80}, 0)`;
+ },
+ activateGrabCursor() {
+ return {
+ 'svg-graph-container-with-grab': this.isPanAvailable,
+ 'svg-graph-container-grabbed': this.isPanAvailable && this.isGrabbed,
+ };
+ },
+ yAxisLabelRectTransform() {
+ const rectWidth =
+ this.rectYAxisLabelDims.height != null ? this.rectYAxisLabelDims.height / 2 : 0;
+ const yCoord = this.vbHeight / 2 - rectWidth;
+
+ return `translate(${this.minX - this.yAxisRectTransformPadding}, ${yCoord})`;
+ },
+ yAxisLabelTextTransform() {
+ const rectWidth =
+ this.rectYAxisLabelDims.height != null ? this.rectYAxisLabelDims.height / 2 : 0;
+ const yCoord = this.vbHeight / 2 + rectWidth - 5;
+
+ return `translate(${this.minX + this.yAxisTextTransformPadding}, ${yCoord}) rotate(-${this.yAxisTextRotation})`;
+ },
+ },
+ mounted() {
+ if (!this.allValuesEmpty) {
+ this.draw();
+ }
+ },
+ methods: {
+ draw() {
+ // update viewport
+ this.vpWidth = this.$refs.svgContainer.clientWidth - this.containerMargin.leftRight;
+ // update viewbox
+ this.vbWidth = this.vpWidth;
+ this.vbHeight = this.vpHeight - this.viewBoxMargin.topBottom;
+ let padding = 0;
+ if (this.graphData.length * this.barWidth > this.vbWidth) {
+ this.xGraphRange = this.graphData.length * this.barWidth;
+ padding = this.calculatePadding(this.barWidth);
+ this.showScrollIndicator = true;
+ this.isPanAvailable = true;
+ } else {
+ this.xGraphRange = this.vbWidth - Math.abs(this.minX);
+ }
+
+ this.xScale = d3
+ .scaleBand()
+ .range([0, this.xGraphRange])
+ .round(true)
+ .paddingInner(padding);
+ this.yScale = d3.scaleLinear().rangeRound([this.vbHeight, 0]);
+
+ this.xScale.domain(this.graphData.map(d => d.name));
+ this.yScale.domain([0, d3.max(this.graphData.map(d => d.value))]);
+
+ // Zoom/Panning Function
+ this.zoom = d3
+ .zoom()
+ .translateExtent([[0, 0], [this.xGraphRange, this.vbHeight]])
+ .on('zoom', this.panGraph)
+ .on('end', this.removeGrabStyling);
+
+ const xAxis = d3.axisBottom().scale(this.xScale);
+
+ const yAxis = d3
+ .axisLeft()
+ .scale(this.yScale)
+ .ticks(4);
+
+ const renderedXAxis = d3
+ .select(this.$refs.baseSvg)
+ .select('.x-axis')
+ .call(xAxis);
+
+ this.xAxisTextElements = this.$refs.xAxis.querySelectorAll('text');
+
+ renderedXAxis.select('.domain').remove();
+
+ renderedXAxis
+ .selectAll('text')
+ .style('text-anchor', 'end')
+ .attr('dx', '-.3em')
+ .attr('dy', '-.95em')
+ .attr('class', 'tick-text')
+ .attr('transform', 'rotate(-90)');
+
+ renderedXAxis.selectAll('line').remove();
+
+ const { maxTextWidth } = this;
+ renderedXAxis.selectAll('text').each(function formatText() {
+ const axisText = d3.select(this);
+ let textLength = axisText.node().getComputedTextLength();
+ let textContent = axisText.text();
+ while (textLength > maxTextWidth && textContent.length > 0) {
+ textContent = textContent.slice(0, -1);
+ axisText.text(`${textContent}...`);
+ textLength = axisText.node().getComputedTextLength();
+ }
+ });
+
+ const width = this.vbWidth;
+
+ const renderedYAxis = d3
+ .select(this.$refs.baseSvg)
+ .select('.y-axis')
+ .call(yAxis);
+
+ renderedYAxis.selectAll('.tick').each(function createTickLines(d, i) {
+ if (i > 0) {
+ d3
+ .select(this)
+ .select('line')
+ .attr('x2', width)
+ .attr('class', 'axis-tick');
+ }
+ });
+
+ // Add the panning capabilities
+ if (this.isPanAvailable) {
+ d3
+ .select(this.$refs.baseSvg)
+ .call(this.zoom)
+ .on('wheel.zoom', null); // This disables the pan of the graph with the scroll of the mouse wheel
+ }
+
+ this.isLoading = false;
+ // Update the yAxisLabel coordinates
+ const labelDims = this.$refs.yAxisLabel.getBBox();
+ this.rectYAxisLabelDims = {
+ height: labelDims.width + 10,
+ };
+ },
+ panGraph() {
+ const allowedRightScroll = this.xGraphRange - this.vbWidth - this.paddingThreshold;
+ const graphMaxPan = Math.abs(d3.event.transform.x) < allowedRightScroll;
+ this.isGrabbed = true;
+ this.panX = d3.event.transform.x;
+
+ if (d3.event.transform.x === 0) {
+ this.showLeftScrollIndicator = false;
+ } else {
+ this.showLeftScrollIndicator = true;
+ this.showScrollIndicator = true;
+ }
+
+ if (!graphMaxPan) {
+ this.panX = -1 * (this.xGraphRange - this.vbWidth + this.paddingThreshold);
+ this.showScrollIndicator = false;
+ }
+ },
+ setTooltipTitle(data) {
+ return data !== null ? `${data.name}: ${data.value}` : '';
+ },
+ calculatePadding(desiredBarWidth) {
+ const widthWithMargin = this.vbWidth - Math.abs(this.minX);
+ const dividend = widthWithMargin - this.graphData.length * desiredBarWidth;
+ const divisor = widthWithMargin - desiredBarWidth;
+
+ return dividend / divisor;
+ },
+ removeGrabStyling() {
+ this.isGrabbed = false;
+ },
+ barHoveredIn(index) {
+ this.xAxisTextElements[index].classList.add('x-axis-text');
+ },
+ barHoveredOut(index) {
+ this.xAxisTextElements[index].classList.remove('x-axis-text');
+ },
+ },
+};
+</script>
+<template>
+ <div
+ ref="svgContainer"
+ :class="activateGrabCursor"
+ class="svg-graph-container"
+ >
+ <svg
+ ref="baseSvg"
+ :width="vpWidth"
+ :height="vpHeight"
+ :viewBox="svgViewBox"
+ :preserveAspectRatio="preserveAspectRatioType">
+ <g
+ ref="xAxis"
+ :transform="xAxisLocation"
+ class="x-axis"
+ />
+ <g v-if="!isLoading">
+ <template
+ v-for="(data, index) in graphData">
+ <rect
+ v-tooltip
+ :key="index"
+ :width="xScale.bandwidth()"
+ :x="xScale(data.name)"
+ :y="yScale(data.value)"
+ :height="vbHeight - yScale(data.value)"
+ :transform="barTranslationTransform"
+ :title="setTooltipTitle(data)"
+ class="bar-rect"
+ data-placement="top"
+ @mouseover="barHoveredIn(index)"
+ @mouseout="barHoveredOut(index)"
+ />
+ </template>
+ </g>
+ <rect
+ :height="vbHeight + 100"
+ transform="translate(-100, -5)"
+ width="100"
+ fill="#fff"
+ />
+ <g class="y-axis-label">
+ <line
+ :x1="0"
+ :x2="0"
+ :y1="0"
+ :y2="vbHeight"
+ transform="translate(-35, 0)"
+ stroke="black"
+ />
+ <!--Get text length and change the height of this rect accordingly-->
+ <rect
+ :height="rectYAxisLabelDims.height"
+ :transform="yAxisLabelRectTransform"
+ :width="30"
+ fill="#fff"
+ />
+ <text
+ ref="yAxisLabel"
+ :transform="yAxisLabelTextTransform"
+ >
+ {{ yAxisLabel }}
+ </text>
+ </g>
+ <g
+ class="y-axis"
+ />
+ <g v-if="showScrollIndicator">
+ <rect
+ :height="vbHeight + 100"
+ :transform="`translate(${vpWidth - 60}, -5)`"
+ width="40"
+ fill="#fff"
+ />
+ <icon
+ :x="vpWidth - 50"
+ :y="vbHeight / 2"
+ :width="14"
+ :height="14"
+ name="chevron-right"
+ class="animate-flicker"
+ />
+ </g>
+ <!--The line that shows up when the data elements surpass the available width -->
+ <g
+ v-if="showScrollIndicator"
+ :transform="scrollIndicatorTransform">
+ <rect
+ :height="vbHeight"
+ x="0"
+ y="0"
+ width="20"
+ fill="url(#shadow-gradient)"
+ />
+ </g>
+ <!--Left scroll indicator-->
+ <g
+ v-if="showLeftScrollIndicator"
+ transform="translate(0, 0)">
+ <rect
+ :height="vbHeight"
+ x="0"
+ y="0"
+ width="20"
+ fill="url(#left-shadow-gradient)"
+ />
+ </g>
+ <svg-gradient
+ :colors="gradientColors"
+ :opacity="gradientOpacity"
+ identifier-name="shadow-gradient"/>
+ <svg-gradient
+ :colors="inverseGradientColors"
+ :opacity="inverseGradientOpacity"
+ identifier-name="left-shadow-gradient"/>
+ </svg>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/bar_chart_constants.js b/app/assets/javascripts/vue_shared/components/bar_chart_constants.js
new file mode 100644
index 00000000000..6957b112da6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/bar_chart_constants.js
@@ -0,0 +1,4 @@
+export const GRADIENT_COLORS = ['#000', '#a7a7a7'];
+export const GRADIENT_OPACITY = ['0', '0.4'];
+export const INVERSE_GRADIENT_COLORS = ['#a7a7a7', '#000'];
+export const INVERSE_GRADIENT_OPACITY = ['0.4', '0'];
diff --git a/app/assets/javascripts/vue_shared/components/panel_resizer.vue b/app/assets/javascripts/vue_shared/components/panel_resizer.vue
index 8c2dcc2d902..7947ae1e4da 100644
--- a/app/assets/javascripts/vue_shared/components/panel_resizer.vue
+++ b/app/assets/javascripts/vue_shared/components/panel_resizer.vue
@@ -32,7 +32,7 @@
},
computed: {
className() {
- return `drag${this.side}`;
+ return `drag-${this.side}`;
},
cursorStyle() {
if (this.enabled) {
@@ -44,8 +44,15 @@
methods: {
resetSize(e) {
e.preventDefault();
+ this.$emit('resize-start', this.size);
+
this.size = this.startSize;
this.$emit('update:size', this.size);
+
+ // End resizing on next tick so that listeners can react to DOM changes
+ this.$nextTick(() => {
+ this.$emit('resize-end', this.size);
+ });
},
startDrag(e) {
if (this.enabled) {
@@ -84,7 +91,7 @@
<div
:class="className"
:style="cursorStyle"
- class="dragHandle"
+ class="drag-handle"
@mousedown="startDrag"
@dblclick="resetSize"
></div>
diff --git a/app/assets/javascripts/vue_shared/components/reports/constants.js b/app/assets/javascripts/vue_shared/components/reports/constants.js
new file mode 100644
index 00000000000..dbde648bfdb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/reports/constants.js
@@ -0,0 +1,3 @@
+export const STATUS_FAILED = 'failed';
+export const STATUS_SUCCESS = 'success';
+export const STATUS_NEUTRAL = 'neutral';
diff --git a/app/assets/javascripts/vue_shared/components/reports/issue_body.js b/app/assets/javascripts/vue_shared/components/reports/issue_body.js
new file mode 100644
index 00000000000..f2141e519da
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/reports/issue_body.js
@@ -0,0 +1,3 @@
+export const components = {};
+
+export const componentNames = {};
diff --git a/app/assets/javascripts/vue_shared/components/reports/issue_status_icon.vue b/app/assets/javascripts/vue_shared/components/reports/issue_status_icon.vue
new file mode 100644
index 00000000000..f8189117ac3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/reports/issue_status_icon.vue
@@ -0,0 +1,58 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+
+import {
+ STATUS_FAILED,
+ STATUS_NEUTRAL,
+ STATUS_SUCCESS,
+} from '~/vue_shared/components/reports/constants';
+
+export default {
+ name: 'IssueStatusIcon',
+ components: {
+ Icon,
+ },
+ props: {
+ // failed || success
+ status: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ iconName() {
+ if (this.isStatusFailed) {
+ return 'status_failed_borderless';
+ } else if (this.isStatusSuccess) {
+ return 'status_success_borderless';
+ }
+
+ return 'status_created_borderless';
+ },
+ isStatusFailed() {
+ return this.status === STATUS_FAILED;
+ },
+ isStatusSuccess() {
+ return this.status === STATUS_SUCCESS;
+ },
+ isStatusNeutral() {
+ return this.status === STATUS_NEUTRAL;
+ },
+ },
+};
+</script>
+<template>
+ <div
+ :class="{
+ failed: isStatusFailed,
+ success: isStatusSuccess,
+ neutral: isStatusNeutral,
+ }"
+ class="report-block-list-icon"
+ >
+ <icon
+ :name="iconName"
+ :size="32"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/reports/issues_list.vue b/app/assets/javascripts/vue_shared/components/reports/issues_list.vue
index e1e03e39ee0..c01f77c2509 100644
--- a/app/assets/javascripts/vue_shared/components/reports/issues_list.vue
+++ b/app/assets/javascripts/vue_shared/components/reports/issues_list.vue
@@ -1,5 +1,10 @@
<script>
-import IssuesBlock from './report_issues.vue';
+import IssuesBlock from '~/vue_shared/components/reports/report_issues.vue';
+import {
+ STATUS_SUCCESS,
+ STATUS_FAILED,
+ STATUS_NEUTRAL,
+} from '~/vue_shared/components/reports/constants';
/**
* Renders block of issues
@@ -9,6 +14,9 @@ export default {
components: {
IssuesBlock,
},
+ success: STATUS_SUCCESS,
+ failed: STATUS_FAILED,
+ neutral: STATUS_NEUTRAL,
props: {
unresolvedIssues: {
type: Array,
@@ -25,29 +33,10 @@ export default {
required: false,
default: () => [],
},
- allIssues: {
- type: Array,
- required: false,
- default: () => [],
- },
- type: {
+ component: {
type: String,
- required: true,
- },
- },
- data() {
- return {
- isFullReportVisible: false,
- };
- },
- computed: {
- unresolvedIssuesStatus() {
- return this.type === 'license' ? 'neutral' : 'failed';
- },
- },
- methods: {
- openFullReport() {
- this.isFullReportVisible = true;
+ required: false,
+ default: '',
},
},
};
@@ -57,43 +46,26 @@ export default {
<issues-block
v-if="unresolvedIssues.length"
- :type="type"
- :status="unresolvedIssuesStatus"
+ :component="component"
:issues="unresolvedIssues"
+ :status="$options.failed"
class="js-mr-code-new-issues"
/>
<issues-block
- v-if="isFullReportVisible"
- :type="type"
- :issues="allIssues"
- class="js-mr-code-all-issues"
- status="failed"
- />
-
- <issues-block
v-if="neutralIssues.length"
- :type="type"
+ :component="component"
:issues="neutralIssues"
+ :status="$options.neutral"
class="js-mr-code-non-issues"
- status="neutral"
/>
<issues-block
v-if="resolvedIssues.length"
- :type="type"
+ :component="component"
:issues="resolvedIssues"
+ :status="$options.success"
class="js-mr-code-resolved-issues"
- status="success"
/>
-
- <button
- v-if="allIssues.length && !isFullReportVisible"
- type="button"
- class="btn-link btn-blank prepend-left-10 js-expand-full-list break-link"
- @click="openFullReport"
- >
- {{ s__("ciReport|Show complete code vulnerabilities report") }}
- </button>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue b/app/assets/javascripts/vue_shared/components/reports/report_issues.vue
index ecffb02a3a0..2d1f3d82234 100644
--- a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue
+++ b/app/assets/javascripts/vue_shared/components/reports/report_issues.vue
@@ -1,19 +1,23 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
+import IssueStatusIcon from '~/vue_shared/components/reports/issue_status_icon.vue';
+import { components, componentNames } from '~/vue_shared/components/reports/issue_body';
export default {
name: 'ReportIssues',
components: {
- Icon,
+ IssueStatusIcon,
+ ...components,
},
props: {
issues: {
type: Array,
required: true,
},
- type: {
+ component: {
type: String,
- required: true,
+ required: false,
+ default: '',
+ validator: value => value === '' || Object.values(componentNames).includes(value),
},
// failed || success
status: {
@@ -21,26 +25,6 @@ export default {
required: true,
},
},
- computed: {
- iconName() {
- if (this.isStatusFailed) {
- return 'status_failed_borderless';
- } else if (this.isStatusSuccess) {
- return 'status_success_borderless';
- }
-
- return 'status_created_borderless';
- },
- isStatusFailed() {
- return this.status === 'failed';
- },
- isStatusSuccess() {
- return this.status === 'success';
- },
- isStatusNeutral() {
- return this.status === 'neutral';
- },
- },
};
</script>
<template>
@@ -52,20 +36,17 @@ export default {
:key="index"
class="report-block-list-issue"
>
- <div
- :class="{
- failed: isStatusFailed,
- success: isStatusSuccess,
- neutral: isStatusNeutral,
- }"
- class="report-block-list-icon append-right-5"
- >
- <icon
- :name="iconName"
- :size="32"
- />
- </div>
+ <issue-status-icon
+ :status="issue.status || status"
+ class="append-right-5"
+ />
+ <component
+ v-if="component"
+ :is="component"
+ :issue="issue"
+ :status="issue.status || status"
+ />
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/reports/report_section.vue b/app/assets/javascripts/vue_shared/components/reports/report_section.vue
index d383ed99a0c..0124d8b5bcc 100644
--- a/app/assets/javascripts/vue_shared/components/reports/report_section.vue
+++ b/app/assets/javascripts/vue_shared/components/reports/report_section.vue
@@ -21,7 +21,7 @@ export default {
required: false,
default: false,
},
- type: {
+ component: {
type: String,
required: false,
default: '',
@@ -59,11 +59,6 @@ export default {
required: false,
default: () => [],
},
- allIssues: {
- type: Array,
- required: false,
- default: () => [],
- },
infoText: {
type: [String, Boolean],
required: false,
@@ -142,18 +137,10 @@ export default {
</script>
<template>
<section class="media-section">
- <div
- class="media"
- >
- <status-icon
- :status="statusIconName"
- />
- <div
- class="media-body space-children d-flex"
- >
- <span
- class="js-code-text code-text"
- >
+ <div class="media">
+ <status-icon :status="statusIconName" />
+ <div class="media-body space-children d-flex flex-align-self-center">
+ <span class="js-code-text code-text">
{{ headerText }}
<popover
@@ -163,10 +150,12 @@ export default {
/>
</span>
+ <slot name="actionButtons"></slot>
+
<button
v-if="isCollapsible"
type="button"
- class="js-collapse-btn btn bt-default float-right btn-sm"
+ class="js-collapse-btn btn float-right btn-sm"
@click="toggleCollapsed"
>
{{ collapseText }}
@@ -183,8 +172,8 @@ export default {
<issues-list
:unresolved-issues="unresolvedIssues"
:resolved-issues="resolvedIssues"
- :all-issues="allIssues"
- :type="type"
+ :neutral-issues="neutralIssues"
+ :component="component"
/>
</slot>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/svg_gradient.vue b/app/assets/javascripts/vue_shared/components/svg_gradient.vue
new file mode 100644
index 00000000000..b61a1befcd6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/svg_gradient.vue
@@ -0,0 +1,37 @@
+<script>
+export default {
+ props: {
+ colors: {
+ type: Array,
+ required: true,
+ },
+ opacity: {
+ type: Array,
+ required: true,
+ },
+ identifierName: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <svg
+ height="0"
+ width="0">
+ <defs>
+ <linearGradient
+ :id="identifierName">
+ <stop
+ :stop-color="colors[0]"
+ :stop-opacity="opacity[0]"
+ offset="0%" />
+ <stop
+ :stop-color="colors[1]"
+ :stop-opacity="opacity[1]"
+ offset="100%" />
+ </linearGradient>
+ </defs>
+ </svg>
+</template>
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 637587de597..2d6dba52801 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -370,11 +370,14 @@ img.emoji {
margin-right: 10px;
}
-.alert,
-.progress {
+.alert {
margin-bottom: $gl-padding;
}
+.progress {
+ height: 4px;
+}
+
.project-item-select-holder {
display: inline-block;
position: relative;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 2097bcebf69..e7e13d35d8e 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -567,9 +567,6 @@
border-bottom: 1px solid $white-normal;
.mx-auto {
- margin: 8px 0;
- text-align: center;
-
.tanuki-logo,
img {
height: 36px;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 56307777a72..a2789021ab4 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -179,6 +179,10 @@
font-weight: inherit;
}
+ a > code {
+ color: $gl-link-color;
+ }
+
dd {
margin-left: $gl-padding;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index efc54196b75..08755b4b545 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -835,3 +835,5 @@ $font-family-monospace: $monospace-font;
$input-line-height: 20px;
$btn-line-height: 20px;
$table-accent-bg: $gray-light;
+$card-border-color: $border-color;
+$card-cap-bg: $gray-light;
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 2d76f0ce004..442aef124d3 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -1,3 +1,6 @@
+@import 'framework/variables';
+@import 'framework/mixins';
+
.project-refs-form,
.project-refs-target-form {
display: inline-block;
@@ -74,6 +77,7 @@
.ide-file-icon-holder {
display: flex;
align-items: center;
+ color: $theme-gray-700;
}
.ide-file-changed-icon {
@@ -161,12 +165,23 @@
background-color: $white-light;
border-bottom-color: $white-light;
}
+
+ &:not(.disabled) {
+ .multi-file-tab {
+ cursor: pointer;
+ }
+ }
+
+ &.disabled {
+ .multi-file-tab-close {
+ cursor: default;
+ }
+ }
}
}
.multi-file-tab {
@include str-truncated(141px);
- cursor: pointer;
svg {
vertical-align: middle;
@@ -241,6 +256,38 @@
}
}
+ .is-deleted {
+ .editor.modified {
+ .margin-view-overlays,
+ .lines-content,
+ .decorationsOverviewRuler {
+ // !important to override monaco inline styles
+ display: none !important;
+ }
+ }
+
+ .diffOverviewRuler.modified {
+ // !important to override monaco inline styles
+ display: none !important;
+ }
+ }
+
+ .is-added {
+ .editor.original {
+ .margin-view-overlays,
+ .lines-content,
+ .decorationsOverviewRuler {
+ // !important to override monaco inline styles
+ display: none !important;
+ }
+ }
+
+ .diffOverviewRuler.original {
+ // !important to override monaco inline styles
+ display: none !important;
+ }
+ }
+
.monaco-diff-editor.vs {
.editor.modified {
box-shadow: none;
@@ -557,16 +604,21 @@
}
}
-.multi-file-addition,
-.multi-file-addition-solid {
+.ide-file-addition,
+.ide-file-addition-solid {
color: $green-500;
}
-.multi-file-modified,
-.multi-file-modified-solid {
+.ide-file-modified,
+.ide-file-modified-solid {
color: $orange-500;
}
+.ide-file-deletion,
+.ide-file-deletion-solid {
+ color: $red-500;
+}
+
.multi-file-commit-list-collapsed {
display: flex;
flex-direction: column;
@@ -781,18 +833,21 @@
}
}
-.dragHandle {
+.drag-handle {
position: absolute;
top: 0;
bottom: 0;
- width: 1px;
- background-color: $white-dark;
+ width: 4px;
+
+ &:hover {
+ background-color: $white-normal;
+ }
- &.dragright {
+ &.drag-right {
right: 0;
}
- &.dragleft {
+ &.drag-left {
left: 0;
}
}
@@ -1014,6 +1069,10 @@
.ide-new-btn {
margin-left: auto;
}
+
+ button {
+ color: $gl-text-color;
+ }
}
.ide-sidebar-branch-title {
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index f030189af06..e5c38a20bf0 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -270,6 +270,7 @@
.block {
width: 100%;
+ word-break: break-word;
&:last-child {
border-bottom: 1px solid $border-gray-normal;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index b616357bb8d..591e21243ed 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -16,6 +16,7 @@
svg {
vertical-align: middle;
+ top: -1px;
}
}
diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss
index 84da9180f93..49d8a5d959b 100644
--- a/app/assets/stylesheets/pages/graph.scss
+++ b/app/assets/stylesheets/pages/graph.scss
@@ -31,3 +31,61 @@
color: $gl-text-red;
}
}
+
+.svg-graph-container {
+ width: 100%;
+
+ .axis-tick {
+ opacity: 0.4;
+ }
+
+ .tick-text {
+ fill: $gl-text-color-secondary;
+ }
+
+ .x-axis-text {
+ fill: $theme-gray-900;
+ }
+
+ .bar-rect {
+ fill: rgba($blue-500, 0.1);
+ stroke: $blue-500;
+ }
+
+ .bar-rect:hover {
+ fill: rgba($blue-700, 0.3);
+ }
+
+ .y-axis-label {
+ line {
+ stroke: $stat-graph-axis-fill;
+ }
+
+ text {
+ font-weight: bold;
+ font-size: 12px;
+ fill: $theme-gray-800;
+ }
+ }
+}
+
+.svg-graph-container-with-grab {
+ cursor: grab;
+ cursor: -webkit-grab;
+}
+
+.svg-graph-container-grabbed {
+ cursor: grabbing;
+ cursor: -webkit-grabbing;
+}
+
+@keyframes flickerAnimation {
+ 0% { opacity: 1; }
+ 50% { opacity: 0; }
+ 100% { opacity: 1; }
+}
+
+.animate-flicker {
+ animation: flickerAnimation 1.5s infinite;
+ fill: $theme-gray-500;
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 797b106de23..d5ae2b673d9 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -834,17 +834,7 @@
}
.compare-meter {
- &.within_estimate {
- .meter-fill {
- background: $gl-primary;
- }
- }
-
&.over_estimate {
- .meter-fill {
- background: $red-500;
- }
-
.time-remaining,
.compare-value.spent {
color: $red-500;
@@ -852,18 +842,6 @@
}
}
- .meter-container {
- background: $border-gray-light;
- border-radius: 3px;
-
- .meter-fill {
- max-width: 100%;
- height: 5px;
- border-radius: 3px;
- background: $gl-primary;
- }
- }
-
.compare-display-container {
display: flex;
justify-content: space-between;
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index c1b1d2e028d..8a4a2caa6c9 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -237,7 +237,7 @@
}
.login-page-broadcast {
- margin-top: 50px;
+ margin-top: 40px;
}
.navless-container {
diff --git a/app/assets/stylesheets/snippets.scss b/app/assets/stylesheets/snippets.scss
index 64110f9c3a0..bd777c66b56 100644
--- a/app/assets/stylesheets/snippets.scss
+++ b/app/assets/stylesheets/snippets.scss
@@ -22,8 +22,8 @@
height: 16px;
background-size: cover;
- &.gl-snippet-icon-doc_code { background-position: 0 0; }
- &.gl-snippet-icon-doc_text { background-position: 0 -16px; }
+ &.gl-snippet-icon-doc-code { background-position: 0 0; }
+ &.gl-snippet-icon-doc-text { background-position: 0 -16px; }
&.gl-snippet-icon-download { background-position: 0 -32px; }
}
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 9e495061f4e..36faea8056e 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -4,13 +4,17 @@ class Projects::CommitsController < Projects::ApplicationController
include ExtractsPath
include RendersCommits
- before_action :whitelist_query_limiting
+ before_action :whitelist_query_limiting, except: :commits_root
before_action :require_non_empty_project
- before_action :assign_ref_vars
+ before_action :assign_ref_vars, except: :commits_root
before_action :authorize_download_code!
- before_action :set_commits
+ before_action :set_commits, except: :commits_root
before_action :set_request_format, only: :show
+ def commits_root
+ redirect_to project_commits_path(@project, @project.default_branch)
+ end
+
def show
@merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
.find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref)
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 21d3c918581..ce03b2d8d1d 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -112,7 +112,7 @@ class Projects::LabelsController < Projects::ApplicationController
begin
return render_404 unless promote_service.execute(@label)
- flash[:notice] = "#{@label.title} promoted to <a href=\"#{group_labels_path(@project.group)}\">group label</a>.".html_safe
+ flash[:notice] = flash_notice_for(@label, @project.group)
respond_to do |format|
format.html do
redirect_to(project_labels_path(@project), status: :see_other)
@@ -135,6 +135,15 @@ class Projects::LabelsController < Projects::ApplicationController
end
end
+ def flash_notice_for(label, group)
+ notice = ''.html_safe
+ notice << label.title
+ notice << ' promoted to '
+ notice << view_context.link_to('<u>group label</u>'.html_safe, group_labels_path(group))
+ notice << '.'
+ notice
+ end
+
protected
def label_params
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 5e86ec93f34..b9b3dcd5a85 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -76,8 +76,8 @@ class Projects::MilestonesController < Projects::ApplicationController
def promote
promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone)
+ flash[:notice] = flash_notice_for(promoted_milestone, project.group)
- flash[:notice] = "#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, promoted_milestone.iid)}\"><u>group milestone</u></a>.".html_safe
respond_to do |format|
format.html do
redirect_to project_milestones_path(project)
@@ -90,6 +90,15 @@ class Projects::MilestonesController < Projects::ApplicationController
redirect_to milestone, alert: error.message
end
+ def flash_notice_for(milestone, group)
+ notice = ''.html_safe
+ notice << milestone.title
+ notice << ' promoted to '
+ notice << view_context.link_to('<u>group milestone</u>'.html_safe, group_milestone_path(group, milestone.iid))
+ notice << '.'
+ notice
+ end
+
def destroy
return access_denied! unless can?(current_user, :admin_milestone, @project)
diff --git a/app/finders/admin/projects_finder.rb b/app/finders/admin/projects_finder.rb
index 53b77f5fed9..543bf1a1415 100644
--- a/app/finders/admin/projects_finder.rb
+++ b/app/finders/admin/projects_finder.rb
@@ -7,7 +7,7 @@ class Admin::ProjectsFinder
end
def execute
- items = Project.without_deleted.with_statistics
+ items = Project.without_deleted.with_statistics.with_route
items = by_namespace_id(items)
items = by_visibilty_level(items)
items = by_with_push(items)
@@ -16,7 +16,7 @@ class Admin::ProjectsFinder
items = by_archived(items)
items = by_personal(items)
items = by_name(items)
- items = items.includes(namespace: [:owner])
+ items = items.includes(namespace: [:owner, :route])
sort(items).page(params[:page])
end
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index d9f9129d08a..8755a1a62e7 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -7,5 +7,5 @@ class GitlabSchema < GraphQL::Schema
query(Types::QueryType)
default_max_page_size 100
- # mutation(Types::MutationType)
+ mutation(Types::MutationType)
end
diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb
new file mode 100644
index 00000000000..eb03dfe1624
--- /dev/null
+++ b/app/graphql/mutations/base_mutation.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Mutations
+ class BaseMutation < GraphQL::Schema::RelayClassicMutation
+ field :errors, [GraphQL::STRING_TYPE],
+ null: false,
+ description: "Reasons why the mutation failed."
+
+ def current_user
+ context[:current_user]
+ end
+ end
+end
diff --git a/app/graphql/mutations/concerns/mutations/resolves_project.rb b/app/graphql/mutations/concerns/mutations/resolves_project.rb
new file mode 100644
index 00000000000..0dd1f264a52
--- /dev/null
+++ b/app/graphql/mutations/concerns/mutations/resolves_project.rb
@@ -0,0 +1,13 @@
+module Mutations
+ module ResolvesProject
+ extend ActiveSupport::Concern
+
+ def resolve_project(full_path:)
+ resolver.resolve(full_path: full_path)
+ end
+
+ def resolver
+ Resolvers::ProjectResolver.new(object: nil, context: context)
+ end
+ end
+end
diff --git a/app/graphql/mutations/merge_requests/base.rb b/app/graphql/mutations/merge_requests/base.rb
new file mode 100644
index 00000000000..2149e72e2df
--- /dev/null
+++ b/app/graphql/mutations/merge_requests/base.rb
@@ -0,0 +1,32 @@
+module Mutations
+ module MergeRequests
+ class Base < BaseMutation
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+ include Mutations::ResolvesProject
+
+ argument :project_path, GraphQL::ID_TYPE,
+ required: true,
+ description: "The project the merge request to mutate is in"
+
+ argument :iid, GraphQL::ID_TYPE,
+ required: true,
+ description: "The iid of the merge request to mutate"
+
+ field :merge_request,
+ Types::MergeRequestType,
+ null: true,
+ description: "The merge request after mutation"
+
+ authorize :update_merge_request
+
+ private
+
+ def find_object(project_path:, iid:)
+ project = resolve_project(full_path: project_path)
+ resolver = Resolvers::MergeRequestResolver.new(object: project, context: context)
+
+ resolver.resolve(iid: iid)
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/merge_requests/set_wip.rb b/app/graphql/mutations/merge_requests/set_wip.rb
new file mode 100644
index 00000000000..a2aa0c84ee4
--- /dev/null
+++ b/app/graphql/mutations/merge_requests/set_wip.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Mutations
+ module MergeRequests
+ class SetWip < Base
+ graphql_name 'MergeRequestSetWip'
+
+ argument :wip,
+ GraphQL::BOOLEAN_TYPE,
+ required: true,
+ description: <<~DESC
+ Whether or not to set the merge request as a WIP.
+ DESC
+
+ def resolve(project_path:, iid:, wip: nil)
+ merge_request = authorized_find!(project_path: project_path, iid: iid)
+ project = merge_request.project
+
+ ::MergeRequests::UpdateService.new(project, current_user, wip_event: wip_event(merge_request, wip))
+ .execute(merge_request)
+
+ {
+ merge_request: merge_request,
+ errors: merge_request.errors.full_messages
+ }
+ end
+
+ private
+
+ def wip_event(merge_request, wip)
+ wip ? 'wip' : 'unwip'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 06ed91c1658..2b4ef299296 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -1,7 +1,11 @@
+# frozen_string_literal: true
+
module Types
class MutationType < BaseObject
+ include Gitlab::Graphql::MountMutation
+
graphql_name "Mutation"
- # TODO: Add Mutations as fields
+ mount_mutation Mutations::MergeRequests::SetWip
end
end
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 4ce89f89fa9..c005ecbb56b 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -4,4 +4,23 @@ module EnvironmentsHelper
endpoint: project_environments_path(@project, format: :json)
}
end
+
+ def metrics_data(project, environment)
+ {
+ "settings-path" => edit_project_service_path(project, 'prometheus'),
+ "clusters-path" => project_clusters_path(project),
+ "current-environment-name": environment.name,
+ "documentation-path" => help_page_path('administration/monitoring/prometheus/index.md'),
+ "empty-getting-started-svg-path" => image_path('illustrations/monitoring/getting_started.svg'),
+ "empty-loading-svg-path" => image_path('illustrations/monitoring/loading.svg'),
+ "empty-no-data-svg-path" => image_path('illustrations/monitoring/no_data.svg'),
+ "empty-unable-to-connect-svg-path" => image_path('illustrations/monitoring/unable_to_connect.svg'),
+ "metrics-endpoint" => additional_metrics_project_environment_path(project, environment, format: :json),
+ "deployment-endpoint" => project_environment_deployments_path(project, environment, format: :json),
+ "environments-endpoint": project_environments_path(project, format: :json),
+ "project-path" => project_path(project),
+ "tags-path" => project_tags_path(project),
+ "has-metrics" => "#{environment.has_metrics?}"
+ }
+ end
end
diff --git a/app/helpers/hooks_helper.rb b/app/helpers/hooks_helper.rb
index 551b9cca6b1..0a356ba55d2 100644
--- a/app/helpers/hooks_helper.rb
+++ b/app/helpers/hooks_helper.rb
@@ -10,7 +10,7 @@ module HooksHelper
trigger_human_name = trigger.to_s.tr('_', ' ').camelize
- link_to path, rel: 'nofollow' do
+ link_to path, rel: 'nofollow', method: :post do
content_tag(:span, trigger_human_name)
end
end
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 733832c1bbb..a05640773ad 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -116,7 +116,7 @@ module SnippetsHelper
raw_project_snippet_url(@snippet.project, @snippet)
end
- link_to external_snippet_icon('doc_code'), snippet_raw_url, class: 'btn', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw'
+ link_to external_snippet_icon('doc-code'), snippet_raw_url, class: 'btn', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw'
end
def embedded_snippet_download_button
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 1db1482d6b7..0e1e39501f5 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -124,7 +124,7 @@ class Notify < BaseMailer
fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze
headers['References'] ||= []
- headers['References'] << fallback_reply_message_id
+ headers['References'].unshift(fallback_reply_message_id)
@reply_by_email = true
end
@@ -158,7 +158,7 @@ class Notify < BaseMailer
def mail_answer_thread(model, headers = {})
headers['Message-ID'] = "<#{SecureRandom.hex}@#{Gitlab.config.gitlab.host}>"
headers['In-Reply-To'] = message_id(model)
- headers['References'] = message_id(model)
+ headers['References'] = [message_id(model)]
headers[:subject]&.prepend('Re: ')
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index 48137c2ed68..ea6ec4d6b03 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -21,6 +21,14 @@ module Clusters
end
end
+ def ready_status
+ [:installed]
+ end
+
+ def ready?
+ ready_status.include?(status_name)
+ end
+
def chart
'stable/prometheus'
end
diff --git a/app/models/concerns/prometheus_adapter.rb b/app/models/concerns/prometheus_adapter.rb
index 18cbbd871a1..9c36f633395 100644
--- a/app/models/concerns/prometheus_adapter.rb
+++ b/app/models/concerns/prometheus_adapter.rb
@@ -24,11 +24,10 @@ module PrometheusAdapter
def query(query_name, *args)
return unless can_query?
- query_class = Gitlab::Prometheus::Queries.const_get("#{query_name.to_s.classify}Query")
+ query_class = query_klass_for(query_name)
+ query_args = build_query_args(*args)
- args.map!(&:id)
-
- with_reactive_cache(query_class.name, *args, &query_class.method(:transform_reactive_result))
+ with_reactive_cache(query_class.name, *query_args, &query_class.method(:transform_reactive_result))
end
# Cache metrics for specific environment
@@ -44,5 +43,13 @@ module PrometheusAdapter
rescue Gitlab::PrometheusClient::Error => err
{ success: false, result: err.message }
end
+
+ def query_klass_for(query_name)
+ Gitlab::Prometheus::Queries.const_get("#{query_name.to_s.classify}Query")
+ end
+
+ def build_query_args(*args)
+ args.map(&:id)
+ end
end
end
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
index be0a5b49012..9155d82d567 100644
--- a/app/models/concerns/reactive_caching.rb
+++ b/app/models/concerns/reactive_caching.rb
@@ -59,6 +59,9 @@ module ReactiveCaching
raise NotImplementedError
end
+ def reactive_cache_updated(*args)
+ end
+
def with_reactive_cache(*args, &blk)
bootstrap = !within_reactive_cache_lifetime?(*args)
Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime)
@@ -81,8 +84,11 @@ module ReactiveCaching
locking_reactive_cache(*args) do
if within_reactive_cache_lifetime?(*args)
enqueuing_update(*args) do
- value = calculate_reactive_cache(*args)
- Rails.cache.write(full_reactive_cache_key(*args), value)
+ key = full_reactive_cache_key(*args)
+ new_value = calculate_reactive_cache(*args)
+ old_value = Rails.cache.read(key)
+ Rails.cache.write(key, new_value)
+ reactive_cache_updated(*args) if new_value != old_value
end
end
end
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 0176a12a131..cb91f8fbac8 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -90,34 +90,17 @@ module Routable
end
def full_name
- if route && route.name.present?
- @full_name ||= route.name # rubocop:disable Gitlab/ModuleWithInstanceVariables
- else
- update_route if persisted?
-
- build_full_name
- end
+ route&.name || build_full_name
end
- # Every time `project.namespace.becomes(Namespace)` is called for polymorphic_path,
- # a new instance is instantiated, and we end up duplicating the same query to retrieve
- # the route. Caching this per request ensures that even if we have multiple instances,
- # we will not have to duplicate work, avoiding N+1 queries in some cases.
def full_path
- return uncached_full_path unless RequestStore.active? && persisted?
-
- RequestStore[full_path_key] ||= uncached_full_path
+ route&.path || build_full_path
end
def full_path_components
full_path.split('/')
end
- def expires_full_path_cache
- RequestStore.delete(full_path_key) if RequestStore.active?
- @full_path = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables
- end
-
def build_full_path
if parent && path
parent.full_path + '/' + path
@@ -138,16 +121,6 @@ module Routable
self.errors[:path].concat(route_path_errors) if route_path_errors
end
- def uncached_full_path
- if route && route.path.present?
- @full_path ||= route.path # rubocop:disable Gitlab/ModuleWithInstanceVariables
- else
- update_route if persisted?
-
- build_full_path
- end
- end
-
def full_name_changed?
name_changed? || parent_changed?
end
@@ -156,10 +129,6 @@ module Routable
path_changed? || parent_changed?
end
- def full_path_key
- @full_path_key ||= "routable/full_path/#{self.class.name}/#{self.id}"
- end
-
def build_full_name
if parent && name
parent.human_name + ' / ' + name
@@ -168,18 +137,9 @@ module Routable
end
end
- def update_route
- return if Gitlab::Database.read_only?
-
- prepare_route
- route.save
- end
-
def prepare_route
route || build_route(source: self)
route.path = build_full_path
route.name = build_full_name
- @full_path = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables
- @full_name = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index f66bdd529f1..f5225cd81ed 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -11,8 +11,6 @@ module Storage
Namespace.find(parent_id_was) # raise NotFound early if needed
end
- expires_full_path_cache
-
move_repositories
if parent_changed?
@@ -34,13 +32,12 @@ module Storage
begin
send_update_instructions
write_projects_repository_config
-
- true
- rescue
- # Returning false does not rollback after_* transaction but gives
- # us information about failing some of tasks
- false
+ rescue => e
+ # Raise if development/test environment, else just notify Sentry
+ Gitlab::Sentry.track_exception(e, extra: { full_path_was: full_path_was, full_path: full_path, action: 'move_dir' })
end
+
+ true # false would cancel later callbacks but not rollback
end
# Hooks
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index 7ab647abe93..fdbe95059e5 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -1,6 +1,7 @@
class DeployToken < ActiveRecord::Base
include Expirable
include TokenAuthenticatable
+ include PolicyActor
add_authentication_token_field :token
AVAILABLE_SCOPES = %i(read_repository read_registry).freeze
@@ -58,10 +59,6 @@ class DeployToken < ActiveRecord::Base
write_attribute(:expires_at, value.presence || Forever.date)
end
- def admin?
- false
- end
-
private
def ensure_at_least_one_scope
diff --git a/app/models/email.rb b/app/models/email.rb
index d6516761f0a..15bdedeac33 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -25,6 +25,10 @@ class Email < ActiveRecord::Base
self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email)
end
+ def accept_pending_invitations!
+ user.accept_pending_invitations!
+ end
+
# once email is confirmed, update the gpg signatures
def update_invalid_gpg_signatures
user.update_invalid_gpg_signatures if confirmed?
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 7034c633268..c1dc2f55346 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -304,7 +304,6 @@ class Namespace < ActiveRecord::Base
def write_projects_repository_config
all_projects.find_each do |project|
- project.expires_full_path_cache # we need to clear cache to validate renames correctly
project.write_repository_config
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 325dbd0197f..a452ec5fcdf 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -31,6 +31,7 @@ class Project < ActiveRecord::Base
BoardLimitExceeded = Class.new(StandardError)
+ STATISTICS_ATTRIBUTE = 'repositories_count'.freeze
NUMBER_OF_PERMITTED_BOARDS = 1
UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze
# Hashed Storage versions handle rolling out new storage to project and dependents models:
@@ -79,6 +80,10 @@ class Project < ActiveRecord::Base
after_create :create_project_feature, unless: :project_feature
+ after_create -> { SiteStatistic.track(STATISTICS_ATTRIBUTE) }
+ before_destroy ->(project) { project.project_feature.untrack_statistics_for_deletion! }
+ after_destroy -> { SiteStatistic.untrack(STATISTICS_ATTRIBUTE) }
+
after_create :create_ci_cd_settings,
unless: :ci_cd_settings,
if: proc { ProjectCiCdSetting.available? }
@@ -1235,8 +1240,6 @@ class Project < ActiveRecord::Base
return true if skip_disk_validation
return false unless repository_storage
- expires_full_path_cache # we need to clear cache to validate renames correctly
-
# Check if repository with same path already exists on disk we can
# skip this for the hashed storage because the path does not change
if legacy_storage? && repository_with_same_path_already_exists?
@@ -1615,7 +1618,6 @@ class Project < ActiveRecord::Base
# When we import a project overwriting the original project, there
# is a move operation. In that case we don't want to send the instructions.
send_move_instructions(full_path_was) unless import_started?
- expires_full_path_cache
self.old_path_with_namespace = full_path_was
SystemHooksService.new.execute_hooks_for(self, :rename)
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index bfb8d703ec9..9c768b13f78 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -19,6 +19,7 @@ class ProjectFeature < ActiveRecord::Base
ENABLED = 20
FEATURES = %i(issues merge_requests wiki snippets builds repository).freeze
+ STATISTICS_ATTRIBUTE = 'wikis_count'.freeze
class << self
def access_level_attribute(feature)
@@ -52,6 +53,9 @@ class ProjectFeature < ActiveRecord::Base
default_value_for :wiki_access_level, value: ENABLED, allows_nil: false
default_value_for :repository_access_level, value: ENABLED, allows_nil: false
+ after_create ->(model) { SiteStatistic.track(STATISTICS_ATTRIBUTE) if model.wiki_enabled? }
+ after_update :update_site_statistics
+
def feature_available?(feature, user)
get_permission(user, access_level(feature))
end
@@ -76,8 +80,30 @@ class ProjectFeature < ActiveRecord::Base
issues_access_level > DISABLED
end
+ # This is a workaround for the removal hooks not been triggered when removing a Project.
+ #
+ # ProjectFeature is removed using database cascade index rule.
+ # This method is called by Project model when deletion starts.
+ def untrack_statistics_for_deletion!
+ return unless wiki_enabled?
+
+ SiteStatistic.untrack(STATISTICS_ATTRIBUTE)
+ end
+
private
+ def update_site_statistics
+ return unless wiki_access_level_changed?
+
+ if self.wiki_access_level_was == DISABLED
+ # possible new states are PRIVATE / ENABLED, both should be tracked
+ SiteStatistic.track(STATISTICS_ATTRIBUTE)
+ elsif self.wiki_access_level == DISABLED
+ # old state was either PRIVATE / ENABLED, only untrack if new state is DISABLED
+ SiteStatistic.untrack(STATISTICS_ATTRIBUTE)
+ end
+ end
+
# Validates builds and merge requests access level
# which cannot be higher than repository access level
def repository_children_level
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index 976b501e297..6172bb38881 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -48,13 +48,13 @@ class RemoteMirror < ActiveRecord::Base
state :failed
after_transition any => :started do |remote_mirror, _|
- Gitlab::Metrics.add_event(:remote_mirrors_running, path: remote_mirror.project.full_path)
+ Gitlab::Metrics.add_event(:remote_mirrors_running)
remote_mirror.update(last_update_started_at: Time.now)
end
after_transition started: :finished do |remote_mirror, _|
- Gitlab::Metrics.add_event(:remote_mirrors_finished, path: remote_mirror.project.full_path)
+ Gitlab::Metrics.add_event(:remote_mirrors_finished)
timestamp = Time.now
remote_mirror.update!(
@@ -63,7 +63,7 @@ class RemoteMirror < ActiveRecord::Base
end
after_transition started: :failed do |remote_mirror, _|
- Gitlab::Metrics.add_event(:remote_mirrors_failed, path: remote_mirror.project.full_path)
+ Gitlab::Metrics.add_event(:remote_mirrors_failed)
remote_mirror.update(last_update_at: Time.now)
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index e248f94cbd8..9873d9a6327 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1029,7 +1029,7 @@ class Repository
end
def repository_event(event, tags = {})
- Gitlab::Metrics.add_event(event, { path: full_path }.merge(tags))
+ Gitlab::Metrics.add_event(event, tags)
end
def initialize_raw_repository
diff --git a/app/models/site_statistic.rb b/app/models/site_statistic.rb
new file mode 100644
index 00000000000..9c9c3172fe6
--- /dev/null
+++ b/app/models/site_statistic.rb
@@ -0,0 +1,74 @@
+class SiteStatistic < ActiveRecord::Base
+ # prevents the creation of multiple rows
+ default_value_for :id, 1
+
+ COUNTER_ATTRIBUTES = %w(repositories_count wikis_count).freeze
+ REQUIRED_SCHEMA_VERSION = 20180629153018
+
+ # Tracks specific attribute
+ #
+ # @param [String] raw_attribute must be one of the values listed in COUNTER_ATTRIBUTES
+ def self.track(raw_attribute)
+ with_statistics_available(raw_attribute) do |attribute|
+ SiteStatistic.update_all(["#{attribute} = #{attribute}+1"])
+ end
+ end
+
+ # Untracks specific attribute
+ #
+ # @param [String] raw_attribute must be one of the values listed in COUNTER_ATTRIBUTES
+ def self.untrack(raw_attribute)
+ with_statistics_available(raw_attribute) do |attribute|
+ SiteStatistic.update_all(["#{attribute} = #{attribute}-1 WHERE #{attribute} > 0"])
+ end
+ end
+
+ # Wrapper for track/untrack operations with basic validations and enforced requirements
+ #
+ # @param [String] raw_attribute must be one of the values listed in COUNTER_ATTRIBUTES
+ # @yield [String] attribute quoted to be used inside SQL / Arel query
+ def self.with_statistics_available(raw_attribute)
+ unless raw_attribute.in?(COUNTER_ATTRIBUTES)
+ raise ArgumentError, "Invalid attribute: '#{raw_attribute}' to '#{caller_locations(1, 1)[0].label}' method. " \
+ "Valid attributes are: #{COUNTER_ATTRIBUTES.join(', ')}"
+ end
+
+ return unless available?
+
+ self.fetch # make sure record exists
+
+ attribute = self.connection.quote_column_name(raw_attribute)
+
+ # will be running on its own transaction context
+ yield(attribute)
+ end
+
+ # Returns a site statistic record with tracked information
+ #
+ # @return [SiteStatistic] record with tracked information
+ def self.fetch
+ SiteStatistic.transaction(requires_new: true) do
+ SiteStatistic.first_or_create!
+ end
+ rescue ActiveRecord::RecordNotUnique
+ retry
+ end
+
+ # Return whether required schema change is available
+ #
+ # This is needed in order to degrade gracefully when testing schema migrations
+ #
+ # @return [Boolean] whether schema is available
+ def self.available?
+ @available_flag ||= ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION
+ end
+
+ # Resets cached column information
+ #
+ # This is called during schema migration specs, in order to reset internal cache state
+ def self.reset_column_information
+ @available_flag = nil
+
+ super
+ end
+end
diff --git a/app/policies/concerns/policy_actor.rb b/app/policies/concerns/policy_actor.rb
new file mode 100644
index 00000000000..069d065280e
--- /dev/null
+++ b/app/policies/concerns/policy_actor.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+# Include this module if we want to pass something else than the user to
+# check policies. This defines several methods which the policy checker
+# would call and check.
+module PolicyActor
+ extend ActiveSupport::Concern
+
+ def blocked?
+ false
+ end
+
+ def admin?
+ false
+ end
+
+ def external?
+ false
+ end
+
+ def internal?
+ false
+ end
+
+ def access_locked?
+ false
+ end
+
+ def required_terms_not_accepted?
+ false
+ end
+
+ def can_create_group
+ false
+ end
+end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index 4a33160afa1..3205578b83e 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -11,10 +11,15 @@ class PipelineSerializer < BaseSerializer
:retryable_builds,
:cancelable_statuses,
:trigger_requests,
- :project,
:manual_actions,
:artifacts,
- { pending_builds: :project }
+ {
+ pending_builds: :project,
+ project: [:route, { namespace: :route }],
+ artifacts: {
+ project: [:route, { namespace: :route }]
+ }
+ }
])
end
diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb
index 4640c5a2d4b..a1165b0ab28 100644
--- a/app/services/clusters/applications/check_installation_progress_service.rb
+++ b/app/services/clusters/applications/check_installation_progress_service.rb
@@ -50,17 +50,17 @@ module Clusters
end
def remove_installation_pod
- helm_api.delete_installation_pod!(install_command.pod_name)
+ helm_api.delete_pod!(install_command.pod_name)
rescue
# no-op
end
def installation_phase
- helm_api.installation_status(install_command.pod_name)
+ helm_api.status(install_command.pod_name)
end
def installation_errors
- helm_api.installation_log(install_command.pod_name)
+ helm_api.log(install_command.pod_name)
end
end
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index a4a66330546..c2a0c5fa7f3 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -77,7 +77,6 @@ module Projects
Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
project.old_path_with_namespace = @old_path
- project.expires_full_path_cache
write_repository_config(@new_path)
diff --git a/app/services/prometheus/adapter_service.rb b/app/services/prometheus/adapter_service.rb
index cbba79690c5..a791845ba20 100644
--- a/app/services/prometheus/adapter_service.rb
+++ b/app/services/prometheus/adapter_service.rb
@@ -30,7 +30,7 @@ module Prometheus
return unless deployment_platform.respond_to?(:cluster)
cluster = deployment_platform.cluster
- return unless cluster.application_prometheus&.installed?
+ return unless cluster.application_prometheus&.ready?
cluster.application_prometheus
end
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
index 00933d726d9..fdaacc098e0 100644
--- a/app/views/admin/projects/_projects.html.haml
+++ b/app/views/admin/projects/_projects.html.haml
@@ -17,7 +17,7 @@
- if project.archived
%span.badge.badge-warning archived
.title
- = link_to [:admin, project.namespace.becomes(Namespace), project] do
+ = link_to(admin_namespace_project_path(project.namespace, project)) do
.dash-project-avatar
.avatar-container.s40
= project_icon(project, alt: '', class: 'avatar project-avatar s40')
diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml
index 9f8b0acd763..d29dda43c89 100644
--- a/app/views/ide/index.html.haml
+++ b/app/views/ide/index.html.haml
@@ -1,6 +1,9 @@
- @body_class = 'ide'
- page_title 'IDE'
+- content_for :page_specific_javascripts do
+ = stylesheet_link_tag 'page_bundles/ide'
+
#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'),
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 9253a0652da..ac5916d129c 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -3,6 +3,11 @@
- site_name = "GitLab"
%head{ prefix: "og: http://ogp.me/ns#" }
%meta{ charset: "utf-8" }
+
+ - if Feature.enabled?('asset_host_prefetch') && ActionController::Base.asset_host
+ %link{ rel: 'dns-prefetch', href: ActionController::Base.asset_host }
+ %link{ rel: 'preconnnect', href: ActionController::Base.asset_host, crossorigin: '' }
+
%meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' }
-# Open Graph - http://ogp.me/
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 97c04dda8cb..e8d31992149 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -67,5 +67,5 @@
%button.navbar-toggler.d-block.d-sm-none{ type: 'button' }
%span.sr-only= _("Toggle navigation")
- = sprite_icon('more', size: 12, css_class: 'more-icon js-navbar-toggle-right')
+ = sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right')
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 0a3b5ec7eea..d471dd84550 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -15,7 +15,7 @@
= nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups', 'analytics#show'], html_options: { class: 'home' }) do
= link_to group_path(@group) do
.nav-icon-container
- = sprite_icon('project')
+ = sprite_icon('home')
%span.nav-item-name
= _('Overview')
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index 94863a3460d..d65f153b451 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -110,7 +110,7 @@
= nav_link(controller: :gpg_keys) do
= link_to profile_gpg_keys_path do
.nav-icon-container
- = sprite_icon('key-2')
+ = sprite_icon('key-modern')
%span.nav-item-name
= _('GPG Keys')
%ul.sidebar-sub-level-items.is-fly-out-only
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 0ec61df1f0a..2c262a2b7dd 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -11,7 +11,7 @@
= nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do
= link_to project_path(@project), class: 'shortcuts-project' do
.nav-icon-container
- = sprite_icon('project')
+ = sprite_icon('home')
%span.nav-item-name
= _('Project')
@@ -40,7 +40,7 @@
= nav_link(controller: sidebar_repository_paths) do
= link_to project_tree_path(@project), class: 'shortcuts-tree' do
.nav-icon-container
- = sprite_icon('doc_text')
+ = sprite_icon('doc-text')
%span.nav-item-name
= _('Repository')
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index 290970a1045..af86b8e8e67 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -2,17 +2,11 @@
- page_title "Metrics for environment", @environment.name
.prometheus-container{ class: container_class }
- #prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'),
- "clusters-path": project_clusters_path(@project),
- "current-environment-name": @environment.name,
- "documentation-path": help_page_path('administration/monitoring/prometheus/index.md'),
- "empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'),
- "empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'),
- "empty-no-data-svg-path": image_path('illustrations/monitoring/no_data.svg'),
- "empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'),
- "metrics-endpoint": additional_metrics_project_environment_path(@project, @environment, format: :json),
- "deployment-endpoint": project_environment_deployments_path(@project, @environment, format: :json),
- "environments-endpoint": project_environments_path(@project, format: :json),
- "project-path": project_path(@project),
- "tags-path": project_tags_path(@project),
- "has-metrics": "#{@environment.has_metrics?}" } }
+ .top-area
+ .row
+ .col-sm-6
+ %h3
+ Environment:
+ = link_to @environment.name, environment_path(@environment)
+
+ #prometheus-graphs{ data: metrics_data(@project, @environment) }
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index b88fe47726d..759efd4e9d4 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -86,7 +86,7 @@
- HasStatus::ORDERED_STATUSES.each do |build_status|
- builds.select{|build| build.status == build_status}.each do |build|
.build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
- - tooltip = build.tooltip_message
+ - tooltip = sanitize(build.tooltip_message.dup)
= link_to(project_job_path(@project, build), data: { toggle: 'tooltip', html: 'true', title: tooltip, container: 'body' }) do
= sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right')
%span{ class: "ci-status-icon-#{build.status}" }
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index ca0f7d6098f..afa7eb06cb4 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -27,7 +27,7 @@
= dropdown_filter(_("Search branches"))
= dropdown_content
= dropdown_loading
- .panel-footer
+ .card-footer
.text-center= icon('spinner spin', class: 'js-source-loading')
%ul.list-unstyled.mr_source_commit
diff --git a/app/views/shared/snippets/_embed.html.haml b/app/views/shared/snippets/_embed.html.haml
index 36f56fbad1a..c7f0511d1de 100644
--- a/app/views/shared/snippets/_embed.html.haml
+++ b/app/views/shared/snippets/_embed.html.haml
@@ -2,7 +2,7 @@
.gitlab-embed-snippets
.js-file-title.file-title-flex-parent
.file-header-content
- = external_snippet_icon('doc_text')
+ = external_snippet_icon('doc-text')
%strong.file-title-name
%a.gitlab-embedded-snippets-title{ href: url_for(only_path: false, overwrite_params: nil) }
diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb
index 100d86e38c8..eeeff6e93a0 100644
--- a/app/workers/concerns/gitlab/github_import/object_importer.rb
+++ b/app/workers/concerns/gitlab/github_import/object_importer.rb
@@ -22,7 +22,7 @@ module Gitlab
importer_class.new(object, project, client).execute
- counter.increment(project: project.full_path)
+ counter.increment
end
def counter
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 5ef9b744db3..68ec66e8499 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -23,9 +23,7 @@ class RepositoryForkWorker
def fork_repository(target_project, source_repository_storage_name, source_disk_path)
return unless start_fork(target_project)
- Gitlab::Metrics.add_event(:fork_repository,
- source_path: source_disk_path,
- target_path: target_project.disk_path)
+ Gitlab::Metrics.add_event(:fork_repository)
result = gitlab_shell.fork_repository(source_repository_storage_name, source_disk_path,
target_project.repository_storage, target_project.disk_path)
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 25fec542ac7..8c64c513c74 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -11,9 +11,7 @@ class RepositoryImportWorker
return unless start_import(project)
- Gitlab::Metrics.add_event(:import_repository,
- import_url: project.import_url,
- path: project.full_path)
+ Gitlab::Metrics.add_event(:import_repository)
service = Projects::ImportService.new(project, project.creator)
result = service.execute