Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml8
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml3
-rw-r--r--app/assets/javascripts/boards/constants.js6
-rw-r--r--app/assets/javascripts/boards/index.js110
-rw-r--r--app/assets/javascripts/boards/models/assignee.js2
-rw-r--r--app/assets/javascripts/boards/models/issue.js2
-rw-r--r--app/assets/javascripts/boards/models/list.js12
-rw-r--r--app/assets/javascripts/boards/queries/board_list.fragment.graphql5
-rw-r--r--app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql15
-rw-r--r--app/assets/javascripts/boards/queries/group_board.query.graphql13
-rw-r--r--app/assets/javascripts/boards/queries/project_board.query.graphql13
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js10
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue33
-rw-r--r--app/assets/javascripts/environments/mixins/environments_app_mixin.js32
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js11
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue20
-rw-r--r--app/assets/stylesheets/framework/common.scss1
-rw-r--r--app/controllers/help_controller.rb4
-rw-r--r--app/controllers/projects/environments_controller.rb11
-rw-r--r--app/controllers/projects_controller.rb1
-rw-r--r--app/helpers/boards_helper.rb3
-rw-r--r--app/helpers/projects_helper.rb3
-rw-r--r--app/models/project_feature.rb2
-rw-r--r--app/policies/project_policy.rb25
-rw-r--r--app/services/metrics/dashboard/base_service.rb2
-rw-r--r--changelogs/unreleased/rc-add_metrics_dashboard_policy.yml5
-rw-r--r--doc/administration/gitaly/praefect.md33
-rw-r--r--doc/ci/review_apps/index.md2
-rw-r--r--doc/user/group/roadmap/img/roadmap_view_v12_10.pngbin46736 -> 0 bytes
-rw-r--r--doc/user/group/roadmap/img/roadmap_view_v13_0.pngbin0 -> 55012 bytes
-rw-r--r--doc/user/group/roadmap/index.md2
-rw-r--r--doc/user/project/settings/index.md5
-rw-r--r--lib/gitlab/jira_import/labels_importer.rb2
-rw-r--r--locale/gitlab.pot13
-rw-r--r--spec/controllers/help_controller_spec.rb7
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb36
-rw-r--r--spec/features/dashboard/help_spec.rb21
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js32
-rw-r--r--spec/helpers/boards_helper_spec.rb4
-rw-r--r--spec/lib/gitlab/jira_import/labels_importer_spec.rb22
-rw-r--r--spec/policies/project_policy_spec.rb187
-rw-r--r--spec/views/help/index.html.haml_spec.rb12
-rw-r--r--spec/views/help/show.html.haml_spec.rb18
44 files changed, 593 insertions, 168 deletions
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index 3e2ccb6fdfe..56aa16e73e0 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -33,10 +33,10 @@
paths:
- webpack-report/
- assets-compile.log
- # We consume these files in GitLab UI for integration tests:
- # https://gitlab.com/gitlab-org/gitlab-ui/-/blob/e88493b3c855aea30bf60baee692a64606b0eb1e/.storybook/preview-head.pug#L1
- - public/assets/application-*.css
- - public/assets/application-*.css.gz
+ # These assets are used in multiple locations:
+ # - in `build-assets-image` job to create assets image for packaging systems
+ # - GitLab UI for integration tests: https://gitlab.com/gitlab-org/gitlab-ui/-/blob/e88493b3c855aea30bf60baee692a64606b0eb1e/.storybook/preview-head.pug#L1
+ - public/assets
when: always
script:
- node --version
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index e0ec20cfd9d..3e4f5da007b 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -84,7 +84,6 @@
- "{,ee/}rubocop/**/*"
- "{,ee/}spec/**/*"
- "doc/README.md" # Some RSpec test rely on this file
- - "doc/administration/raketasks/maintenance.md" # Some RSpec test rely on this file
.code-patterns: &code-patterns
- "{package.json,yarn.lock}"
@@ -128,7 +127,6 @@
- "{,ee/}rubocop/**/*"
- "{,ee/}spec/**/*"
- "doc/README.md" # Some RSpec test rely on this file
- - "doc/administration/raketasks/maintenance.md" # Some RSpec test rely on this file
.code-qa-patterns: &code-qa-patterns
- "{package.json,yarn.lock}"
@@ -171,7 +169,6 @@
- "{,ee/}rubocop/**/*"
- "{,ee/}spec/**/*"
- "doc/README.md" # Some RSpec test rely on this file
- - "doc/administration/raketasks/maintenance.md" # Some RSpec test rely on this file
# QA changes
- ".dockerignore"
- "qa/**/*"
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 40f79a44b51..f577a168e75 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -1,3 +1,8 @@
+export const BoardType = {
+ project: 'project',
+ group: 'group',
+};
+
export const ListType = {
assignee: 'assignee',
milestone: 'milestone',
@@ -11,5 +16,6 @@ export const ListType = {
export const inactiveListId = 0;
export default {
+ BoardType,
ListType,
};
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index a12db7a5f1a..7c41182d554 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -16,10 +16,13 @@ import {
getBoardsModalData,
} from 'ee_else_ce/boards/ee_functions';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import Flash from '~/flash';
import { __ } from '~/locale';
import './models/label';
import './models/assignee';
+import { BoardType } from './constants';
import FilteredSearchBoards from '~/boards/filtered_search_boards';
import eventHub from '~/boards/eventhub';
@@ -37,7 +40,16 @@ import {
convertObjectPropsToCamelCase,
parseBoolean,
} from '~/lib/utils/common_utils';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
+import projectBoardQuery from './queries/project_board.query.graphql';
+import groupQuery from './queries/group_board.query.graphql';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
let issueBoardsApp;
@@ -79,18 +91,22 @@ export default () => {
import('ee_component/boards/components/board_settings_sidebar.vue'),
},
store,
- data: {
- state: boardsStore.state,
- loading: true,
- boardsEndpoint: $boardApp.dataset.boardsEndpoint,
- recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
- listsEndpoint: $boardApp.dataset.listsEndpoint,
- boardId: $boardApp.dataset.boardId,
- disabled: parseBoolean($boardApp.dataset.disabled),
- issueLinkBase: $boardApp.dataset.issueLinkBase,
- rootPath: $boardApp.dataset.rootPath,
- bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
- detailIssue: boardsStore.detail,
+ apolloProvider,
+ data() {
+ return {
+ state: boardsStore.state,
+ loading: 0,
+ boardsEndpoint: $boardApp.dataset.boardsEndpoint,
+ recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
+ listsEndpoint: $boardApp.dataset.listsEndpoint,
+ boardId: $boardApp.dataset.boardId,
+ disabled: parseBoolean($boardApp.dataset.disabled),
+ issueLinkBase: $boardApp.dataset.issueLinkBase,
+ rootPath: $boardApp.dataset.rootPath,
+ bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
+ detailIssue: boardsStore.detail,
+ parent: $boardApp.dataset.parent,
+ };
},
computed: {
detailIssueVisible() {
@@ -124,31 +140,56 @@ export default () => {
this.filterManager.setup();
boardsStore.disabled = this.disabled;
- boardsStore
- .all()
- .then(res => res.data)
- .then(lists => {
- lists.forEach(listObj => {
- let { position } = listObj;
- if (listObj.list_type === 'closed') {
- position = Infinity;
- } else if (listObj.list_type === 'backlog') {
- position = -1;
+
+ if (gon.features.graphqlBoardLists) {
+ this.$apollo.addSmartQuery('lists', {
+ query() {
+ return this.parent === BoardType.group ? groupQuery : projectBoardQuery;
+ },
+ variables() {
+ return {
+ fullPath: this.state.endpoints.fullPath,
+ boardId: `gid://gitlab/Board/${this.boardId}`,
+ };
+ },
+ update(data) {
+ return this.getNodes(data);
+ },
+ result({ data, error }) {
+ if (error) {
+ throw error;
}
- boardsStore.addList({
- ...listObj,
- position,
- });
- });
+ const lists = this.getNodes(data);
+
+ lists.forEach(list =>
+ boardsStore.addList({
+ ...list,
+ id: getIdFromGraphQLId(list.id),
+ }),
+ );
- boardsStore.addBlankState();
- setPromotionState(boardsStore);
- this.loading = false;
- })
- .catch(() => {
- Flash(__('An error occurred while fetching the board lists. Please try again.'));
+ boardsStore.addBlankState();
+ setPromotionState(boardsStore);
+ },
+ error() {
+ Flash(__('An error occurred while fetching the board lists. Please try again.'));
+ },
});
+ } else {
+ boardsStore
+ .all()
+ .then(res => res.data)
+ .then(lists => {
+ lists.forEach(list => boardsStore.addList(list));
+ boardsStore.addBlankState();
+ setPromotionState(boardsStore);
+ this.loading = false;
+ })
+ .catch(() => {
+ Flash(__('An error occurred while fetching the board lists. Please try again.'));
+ });
+ }
},
methods: {
updateTokens() {
@@ -233,6 +274,9 @@ export default () => {
});
}
},
+ getNodes(data) {
+ return data[this.parent]?.board?.lists.nodes;
+ },
},
});
diff --git a/app/assets/javascripts/boards/models/assignee.js b/app/assets/javascripts/boards/models/assignee.js
index 5f5758583bb..1e822d06bfd 100644
--- a/app/assets/javascripts/boards/models/assignee.js
+++ b/app/assets/javascripts/boards/models/assignee.js
@@ -3,7 +3,7 @@ export default class ListAssignee {
this.id = obj.id;
this.name = obj.name;
this.username = obj.username;
- this.avatar = obj.avatar_url || obj.avatar || gon.default_avatar_url;
+ this.avatar = obj.avatarUrl || obj.avatar_url || obj.avatar || gon.default_avatar_url;
this.path = obj.path;
this.state = obj.state;
this.webUrl = obj.web_url || obj.webUrl;
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index af1a910149e..878f49cc6be 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -15,7 +15,7 @@ class ListIssue {
this.labels = [];
this.assignees = [];
this.selected = false;
- this.position = obj.relative_position || Infinity;
+ this.position = obj.position || obj.relative_position || Infinity;
this.isFetching = {
subscriptions: true,
};
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index cd46f8cd1a4..31c372b7a75 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -39,8 +39,8 @@ class List {
this.id = obj.id;
this._uid = this.guid();
this.position = obj.position;
- this.title = obj.list_type === 'backlog' ? __('Open') : obj.title;
- this.type = obj.list_type;
+ this.title = (obj.list_type || obj.listType) === 'backlog' ? __('Open') : obj.title;
+ this.type = obj.list_type || obj.listType;
const typeInfo = this.getTypeInfo(this.type);
this.preset = Boolean(typeInfo.isPreset);
@@ -51,14 +51,12 @@ class List {
this.loadingMore = false;
this.issues = obj.issues || [];
this.issuesSize = obj.issuesSize ? obj.issuesSize : 0;
- this.maxIssueCount = Object.hasOwnProperty.call(obj, 'max_issue_count')
- ? obj.max_issue_count
- : 0;
+ this.maxIssueCount = obj.maxIssueCount || obj.max_issue_count || 0;
if (obj.label) {
this.label = new ListLabel(obj.label);
- } else if (obj.user) {
- this.assignee = new ListAssignee(obj.user);
+ } else if (obj.user || obj.assignee) {
+ this.assignee = new ListAssignee(obj.user || obj.assignee);
this.title = this.assignee.name;
} else if (IS_EE && obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
diff --git a/app/assets/javascripts/boards/queries/board_list.fragment.graphql b/app/assets/javascripts/boards/queries/board_list.fragment.graphql
new file mode 100644
index 00000000000..bbf3314377e
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/board_list.fragment.graphql
@@ -0,0 +1,5 @@
+#import "./board_list_shared.fragment.graphql"
+
+fragment BoardListFragment on BoardList {
+ ...BoardListShared
+}
diff --git a/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql b/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql
new file mode 100644
index 00000000000..6ba6c05d6d9
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql
@@ -0,0 +1,15 @@
+fragment BoardListShared on BoardList {
+ id,
+ title,
+ position,
+ listType,
+ collapsed,
+ label {
+ id,
+ title,
+ color,
+ textColor,
+ description,
+ descriptionHtml
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/group_board.query.graphql b/app/assets/javascripts/boards/queries/group_board.query.graphql
new file mode 100644
index 00000000000..cb42cb3f73d
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/group_board.query.graphql
@@ -0,0 +1,13 @@
+#import "ee_else_ce/boards/queries/board_list.fragment.graphql"
+
+query GroupBoard($fullPath: ID!, $boardId: ID!) {
+ group(fullPath: $fullPath) {
+ board(id: $boardId) {
+ lists {
+ nodes {
+ ...BoardListFragment
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/project_board.query.graphql b/app/assets/javascripts/boards/queries/project_board.query.graphql
new file mode 100644
index 00000000000..4620a7e0fd5
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/project_board.query.graphql
@@ -0,0 +1,13 @@
+#import "ee_else_ce/boards/queries/board_list.fragment.graphql"
+
+query ProjectBoard($fullPath: ID!, $boardId: ID!) {
+ project(fullPath: $fullPath) {
+ board(id: $boardId) {
+ lists {
+ nodes {
+ ...BoardListFragment
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index d20b99ecfaa..b8ae6396475 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -80,7 +80,15 @@ const boardsStore = {
this.state.currentPage = page;
},
addList(listObj) {
- const list = new List(listObj);
+ const listType = listObj.listType || listObj.list_type;
+ let { position } = listObj;
+ if (listType === ListType.closed) {
+ position = Infinity;
+ } else if (listType === ListType.backlog) {
+ position = -1;
+ }
+
+ const list = new List({ ...listObj, position });
this.state.lists = sortBy([...this.state.lists, list], 'position');
return list;
},
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 0cc6f3df2d7..0a5538237f9 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -1,6 +1,5 @@
<script>
import { GlDeprecatedButton } from '@gitlab/ui';
-import envrionmentsAppMixin from 'ee_else_ce/environments/mixins/environments_app_mixin';
import Flash from '~/flash';
import { s__ } from '~/locale';
import emptyState from './empty_state.vue';
@@ -22,13 +21,18 @@ export default {
DeleteEnvironmentModal,
},
- mixins: [CIPaginationMixin, environmentsMixin, envrionmentsAppMixin],
+ mixins: [CIPaginationMixin, environmentsMixin],
props: {
endpoint: {
type: String,
required: true,
},
+ canaryDeploymentFeatureId: {
+ type: String,
+ required: false,
+ default: '',
+ },
canCreateEnvironment: {
type: Boolean,
required: true,
@@ -41,6 +45,11 @@ export default {
type: String,
required: true,
},
+ helpCanaryDeploymentsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
helpPagePath: {
type: String,
required: true,
@@ -50,17 +59,37 @@ export default {
required: false,
default: '',
},
+ lockPromotionSvgPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showCanaryDeploymentCallout: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ userCalloutsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
created() {
eventHub.$on('toggleFolder', this.toggleFolder);
+ eventHub.$on('toggleDeployBoard', this.toggleDeployBoard);
},
beforeDestroy() {
eventHub.$off('toggleFolder');
+ eventHub.$off('toggleDeployBoard');
},
methods: {
+ toggleDeployBoard(model) {
+ this.store.toggleDeployBoard(model.id);
+ },
toggleFolder(folder) {
this.store.toggleFolder(folder);
diff --git a/app/assets/javascripts/environments/mixins/environments_app_mixin.js b/app/assets/javascripts/environments/mixins/environments_app_mixin.js
deleted file mode 100644
index fc805b9235a..00000000000
--- a/app/assets/javascripts/environments/mixins/environments_app_mixin.js
+++ /dev/null
@@ -1,32 +0,0 @@
-export default {
- props: {
- canaryDeploymentFeatureId: {
- type: String,
- required: false,
- default: '',
- },
- showCanaryDeploymentCallout: {
- type: Boolean,
- required: false,
- default: false,
- },
- userCalloutsPath: {
- type: String,
- required: false,
- default: '',
- },
- lockPromotionSvgPath: {
- type: String,
- required: false,
- default: '',
- },
- helpCanaryDeploymentsPath: {
- type: String,
- required: false,
- default: '',
- },
- },
- metods: {
- toggleDeployBoard() {},
- },
-};
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 6b7c1ff627d..e07ec693948 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -133,6 +133,17 @@ export default class EnvironmentsStore {
}
/**
+ * Toggles deploy board visibility for the provided environment ID.
+ * Currently only works on EE.
+ *
+ * @param {Object} environment
+ * @return {Array}
+ */
+ toggleDeployBoard() {
+ return this.state.environments;
+ }
+
+ /**
* Toggles folder open property for the given folder.
*
* @param {Object} folder
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 6efddec1172..e4edcc2448c 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -12,6 +12,7 @@ import {
visibilityLevelDescriptions,
featureAccessLevelMembers,
featureAccessLevelEveryone,
+ featureAccessLevel,
} from '../constants';
import { toggleHiddenClassBySelector } from '../external';
@@ -127,7 +128,7 @@ export default {
wikiAccessLevel: 20,
snippetsAccessLevel: 20,
pagesAccessLevel: 20,
- metricsAccessLevel: visibilityOptions.PRIVATE,
+ metricsDashboardAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
containerRegistryEnabled: true,
lfsEnabled: true,
requestAccessEnabled: true,
@@ -174,6 +175,10 @@ export default {
return options;
},
+ metricsOptionsDropdownEnabled() {
+ return this.featureAccessLevelOptions.length < 2;
+ },
+
repositoryEnabled() {
return this.repositoryAccessLevel > 0;
},
@@ -211,6 +216,7 @@ export default {
this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel);
this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel);
this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel);
+ this.metricsDashboardAccessLevel = Math.min(10, this.metricsDashboardAccessLevel);
if (this.pagesAccessLevel === 20) {
// When from Internal->Private narrow access for only members
this.pagesAccessLevel = 10;
@@ -225,6 +231,7 @@ export default {
if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20;
if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20;
if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20;
+ if (this.metricsDashboardAccessLevel === 10) this.metricsDashboardAccessLevel = 20;
this.highlightChanges();
}
},
@@ -485,17 +492,18 @@ export default {
<div class="project-feature-controls">
<div class="select-wrapper">
<select
- v-model="metricsAccessLevel"
+ v-model="metricsDashboardAccessLevel"
+ :disabled="metricsOptionsDropdownEnabled"
name="project[project_feature_attributes][metrics_dashboard_access_level]"
- class="form-control select-control"
+ class="form-control project-repo-select select-control"
>
<option
- :value="visibilityOptions.PRIVATE"
- :disabled="!visibilityAllowed(visibilityOptions.PRIVATE)"
+ :value="featureAccessLevelMembers[0]"
+ :disabled="!visibilityAllowed(visibilityOptions.INTERNAL)"
>{{ featureAccessLevelMembers[1] }}</option
>
<option
- :value="visibilityOptions.PUBLIC"
+ :value="featureAccessLevelEveryone[0]"
:disabled="!visibilityAllowed(visibilityOptions.PUBLIC)"
>{{ featureAccessLevelEveryone[1] }}</option
>
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 320bd4adaaa..7f50c50145b 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -434,6 +434,7 @@ img.emoji {
.append-bottom-20 { margin-bottom: 20px; }
.append-bottom-default { margin-bottom: $gl-padding; }
.prepend-bottom-32 { margin-bottom: 32px; }
+.ml-10 { margin-left: 4.5rem; }
.inline { display: inline-block; }
.center { text-align: center; }
.block { display: block; }
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 91bba1eb617..a1bbcf34f69 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -26,7 +26,7 @@ class HelpController < ApplicationController
respond_to do |format|
format.any(:markdown, :md, :html) do
- # Note: We are purposefully NOT using `Rails.root.join`
+ # Note: We are purposefully NOT using `Rails.root.join` because of https://gitlab.com/gitlab-org/gitlab/-/issues/216028.
path = File.join(Rails.root, 'doc', "#{@path}.md")
if File.exist?(path)
@@ -42,7 +42,7 @@ class HelpController < ApplicationController
# Allow access to specific media files in the doc folder
format.any(:png, :gif, :jpeg, :mp4, :mp3) do
- # Note: We are purposefully NOT using `Rails.root.join`
+ # Note: We are purposefully NOT using `Rails.root.join` because of https://gitlab.com/gitlab-org/gitlab/-/issues/216028.
path = File.join(Rails.root, 'doc', "#{@path}.#{params[:format]}")
if File.exist?(path)
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 1fa362eff03..70724394ef5 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -4,6 +4,13 @@ class Projects::EnvironmentsController < Projects::ApplicationController
include MetricsDashboard
layout 'project'
+
+ before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
+ authorize_metrics_dashboard!
+
+ push_frontend_feature_flag(:prometheus_computed_alerts)
+ push_frontend_feature_flag(:metrics_dashboard_annotations, project)
+ end
before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_stop_environment!, only: [:stop]
@@ -12,10 +19,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics, :cancel_auto_stop]
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? }
- before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
- push_frontend_feature_flag(:prometheus_computed_alerts)
- push_frontend_feature_flag(:metrics_dashboard_annotations, project)
- end
after_action :expire_etag_cache, only: [:cancel_auto_stop]
def index
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index bb20ea1de49..92fc2d202f3 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -403,6 +403,7 @@ class ProjectsController < Projects::ApplicationController
snippets_access_level
wiki_access_level
pages_access_level
+ metrics_dashboard_access_level
]
]
end
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index c14bc454bb9..f8c00f3a4cd 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -16,7 +16,8 @@ module BoardsHelper
full_path: full_path,
bulk_update_path: @bulk_issues_path,
time_tracking_limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s,
- recent_boards_endpoint: recent_boards_path
+ recent_boards_endpoint: recent_boards_path,
+ parent: current_board_parent.model_name.param_key
}
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index bd207615e7c..a8cde2b723e 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -589,7 +589,8 @@ module ProjectsHelper
pagesAccessLevel: feature.pages_access_level,
containerRegistryEnabled: !!project.container_registry_enabled,
lfsEnabled: !!project.lfs_enabled,
- emailsDisabled: project.emails_disabled?
+ emailsDisabled: project.emails_disabled?,
+ metricsDashboardAccessLevel: feature.metrics_dashboard_access_level
}
end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 31a3fa12c00..9201cd24d66 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -23,7 +23,7 @@ class ProjectFeature < ApplicationRecord
PUBLIC = 30
FEATURES = %i(issues forking merge_requests wiki snippets builds repository pages metrics_dashboard).freeze
- PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER }.freeze
+ PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER, metrics_dashboard: Gitlab::Access::REPORTER }.freeze
PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT = { repository: Gitlab::Access::REPORTER }.freeze
STRING_OPTIONS = HashWithIndifferentAccess.new({
'disabled' => DISABLED,
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 0bb91782dc8..442d07dcb62 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -88,6 +88,11 @@ class ProjectPolicy < BasePolicy
@subject.feature_available?(:forking, @user)
end
+ with_scope :subject
+ condition(:metrics_dashboard_allowed) do
+ feature_available?(:metrics_dashboard)
+ end
+
with_scope :global
condition(:mirror_available, score: 0) do
::Gitlab::CurrentSettings.current_application_settings.mirror_available
@@ -134,6 +139,7 @@ class ProjectPolicy < BasePolicy
wiki
builds
pages
+ metrics_dashboard
]
features.each do |f|
@@ -227,6 +233,7 @@ class ProjectPolicy < BasePolicy
enable :read_prometheus
enable :read_metrics_dashboard_annotation
enable :read_alert_management_alerts
+ enable :metrics_dashboard
end
# We define `:public_user_access` separately because there are cases in gitlab-ee
@@ -249,6 +256,16 @@ class ProjectPolicy < BasePolicy
enable :fork_project
end
+ rule { metrics_dashboard_disabled }.policy do
+ prevent(:metrics_dashboard)
+ end
+
+ rule { can?(:metrics_dashboard) }.policy do
+ enable :read_prometheus
+ enable :read_environment
+ enable :read_deployment
+ end
+
rule { owner | admin | guest | group_member }.prevent :request_access
rule { ~request_access_enabled }.prevent :request_access
@@ -327,6 +344,14 @@ class ProjectPolicy < BasePolicy
enable :admin_terraform_state
end
+ rule { public_project & metrics_dashboard_allowed }.policy do
+ enable :metrics_dashboard
+ end
+
+ rule { internal_access & metrics_dashboard_allowed }.policy do
+ enable :metrics_dashboard
+ end
+
rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror
rule { can?(:push_code) }.enable :admin_tag
diff --git a/app/services/metrics/dashboard/base_service.rb b/app/services/metrics/dashboard/base_service.rb
index c112d75a9b5..514793694ba 100644
--- a/app/services/metrics/dashboard/base_service.rb
+++ b/app/services/metrics/dashboard/base_service.rb
@@ -42,7 +42,7 @@ module Metrics
def allowed?
return false unless params[:environment]
- Ability.allowed?(current_user, :read_environment, project)
+ project&.feature_available?(:metrics_dashboard, current_user)
end
# Returns a new dashboard Hash, supplemented with DB info
diff --git a/changelogs/unreleased/rc-add_metrics_dashboard_policy.yml b/changelogs/unreleased/rc-add_metrics_dashboard_policy.yml
new file mode 100644
index 00000000000..85459ef705c
--- /dev/null
+++ b/changelogs/unreleased/rc-add_metrics_dashboard_policy.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to change metrics dashboard visibility
+merge_request: 29634
+author:
+type: added
diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md
index 4e1ed276c0e..3074ebddc3b 100644
--- a/doc/administration/gitaly/praefect.md
+++ b/doc/administration/gitaly/praefect.md
@@ -5,10 +5,30 @@ NOTE: **Note:** Praefect is a
allows Gitaly to be run in a highly available configuration. While unexpected
data loss is not likely, Praefect is not yet ready for production environments.
-Praefect is an optional reverse-proxy for [Gitaly](../index.md) to manage a
-cluster of Gitaly nodes for high availability. High availability is currently
-implemented through asynchronous replication. If a Gitaly node becomes
-unavailable, Praefect will automatically route traffic to a warm Gitaly replica.
+[Gitaly](index.md) is the service that provides storage for Git repositories in
+the GitLab application. Praefect is an optional reverse proxy for Gitaly to
+manage multiple Gitaly nodes for high availability.
+
+High availability is currently implemented through **asynchronous replication**.
+If a Gitaly node becomes unavailable, Praefect will automatically route traffic
+to a warm Gitaly replica.
+
+- **Recovery Point Objective (RPO):** Less than 1 minute.
+
+ Writes are replicated asynchronously. Any writes that have not been replicated
+ to the newly promoted primary are lost.
+
+ [Strong Consistency](https://gitlab.com/groups/gitlab-org/-/epics/1189) is
+ planned to improve this to "no loss".
+
+- **Recovery Time Objective (RTO):** Less than 10 seconds.
+
+ Outages are detected by a health checks run by each Praefect node every
+ second. Failover requires ten consecutive failed health checks on each
+ Praefect node.
+
+ [Faster outage detection](https://gitlab.com/gitlab-org/gitaly/-/issues/2608)
+ is planned to improve this to less than 1 second.
The current version supports:
@@ -18,7 +38,6 @@ The current version supports:
Follow the [HA Gitaly epic](https://gitlab.com/groups/gitlab-org/-/epics/1489)
for improvements including
-[strong consistency](https://gitlab.com/groups/gitlab-org/-/epics/1189) and
[horizontally distributing reads](https://gitlab.com/groups/gitlab-org/-/epics/2013).
## Requirements for configuring Gitaly for High Availability
@@ -348,7 +367,7 @@ To complete this section you will need:
These should be dedicated nodes, do not run other services on these nodes.
Every Gitaly server assigned to the Praefect cluster needs to be configured. The
-configuration is the same as a normal [standalone Gitaly server](../index.md),
+configuration is the same as a normal [standalone Gitaly server](index.md),
except:
- the storage names are exposed to Praefect, not GitLab
@@ -428,7 +447,7 @@ documentation](index.md#3-gitaly-server-configuration).
1. Configure the GitLab Shell `secret_token`, and `internal_api_url` which are
needed for `git push` operations.
- If you have already configured [Gitaly on its own server](../index.md)
+ If you have already configured [Gitaly on its own server](index.md)
```ruby
gitlab_shell['secret_token'] = 'GITLAB_SHELL_SECRET_TOKEN'
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
index 92df3d5cd91..eb333bb66ed 100644
--- a/doc/ci/review_apps/index.md
+++ b/doc/ci/review_apps/index.md
@@ -205,7 +205,7 @@ if [route maps](#route-maps) are configured in the project.
![review button](img/review_button.png)
-The provided script should be added to the `<head>` of you application and
+The provided script should be added to the `<head>` of your application and
consists of some project and merge request specific values. Here's what it
looks like:
diff --git a/doc/user/group/roadmap/img/roadmap_view_v12_10.png b/doc/user/group/roadmap/img/roadmap_view_v12_10.png
deleted file mode 100644
index 69579fd1c1e..00000000000
--- a/doc/user/group/roadmap/img/roadmap_view_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/roadmap/img/roadmap_view_v13_0.png b/doc/user/group/roadmap/img/roadmap_view_v13_0.png
new file mode 100644
index 00000000000..a5b76b84418
--- /dev/null
+++ b/doc/user/group/roadmap/img/roadmap_view_v13_0.png
Binary files differ
diff --git a/doc/user/group/roadmap/index.md b/doc/user/group/roadmap/index.md
index 9f068adcd47..18b94328f9d 100644
--- a/doc/user/group/roadmap/index.md
+++ b/doc/user/group/roadmap/index.md
@@ -23,7 +23,7 @@ You can click the chevron **{chevron-down}** next to the epic title to expand an
On top of the milestone bars, you can see their title. When you hover a milestone bar or title, a popover appears with its title, start date and due date.
-![roadmap view](img/roadmap_view_v12_10.png)
+![roadmap view](img/roadmap_view_v13_0.png)
A dropdown menu allows you to show only open or closed epics. By default, all epics are shown.
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index b7b3f2a2711..0c98772237b 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -61,6 +61,7 @@ Use the switches to enable or disable the following features:
| **Wiki** | ✓ | Enables a separate system for [documentation](../wiki/) |
| **Snippets** | ✓ | Enables [sharing of code and text](../../snippets.md) |
| **Pages** | ✓ | Allows you to [publish static websites](../pages/) |
+| **Metrics Dashboard** | ✓ | Control access to [metrics dashboard](../integrations/prometheus.md)
Some features depend on others:
@@ -80,13 +81,15 @@ Some features depend on others:
- If you disable **Repository** functionality, GitLab also disables the following
features for your project:
-
- **Merge Requests**
- **Pipelines**
- **Container Registry**
- **Git Large File Storage**
- **Packages**
+- Metrics dashboard access requires reading both project environments and deployments.
+ Users with access to the metrics dashboard can also access environments and deployments.
+
#### Disabling email notifications
Project owners can disable all [email notifications](../../profile/notifications.md#gitlab-notification-emails)
diff --git a/lib/gitlab/jira_import/labels_importer.rb b/lib/gitlab/jira_import/labels_importer.rb
index f1f5708865a..6e6842e06bf 100644
--- a/lib/gitlab/jira_import/labels_importer.rb
+++ b/lib/gitlab/jira_import/labels_importer.rb
@@ -39,7 +39,7 @@ module Gitlab
def process_jira_page(start_at)
request = "/rest/api/2/label?maxResults=#{MAX_LABELS}&startAt=#{start_at}"
- response = JSON.parse(client.get(request))
+ response = client.get(request)
return true if response['values'].blank?
return true unless response.key?('isLast')
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e039b26112c..4ee563e9e4d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5992,7 +5992,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -13760,6 +13760,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -17455,7 +17458,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -19181,6 +19184,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -20654,6 +20660,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb
index f03fee8d3ae..fafbe6bffe1 100644
--- a/spec/controllers/help_controller_spec.rb
+++ b/spec/controllers/help_controller_spec.rb
@@ -99,6 +99,7 @@ describe HelpController do
context 'for Markdown formats' do
context 'when requested file exists' do
before do
+ expect(File).to receive(:read).and_return(fixture_file('blockquote_fence_after.md'))
get :show, params: { path: 'ssh/README' }, format: :md
end
@@ -108,7 +109,7 @@ describe HelpController do
it 'renders HTML' do
expect(response).to render_template('show.html.haml')
- expect(response.content_type).to eq 'text/html'
+ expect(response.media_type).to eq 'text/html'
end
end
@@ -129,7 +130,7 @@ describe HelpController do
},
format: :png
expect(response).to be_successful
- expect(response.content_type).to eq 'image/png'
+ expect(response.media_type).to eq 'image/png'
expect(response.headers['Content-Disposition']).to match(/^inline;/)
end
end
@@ -168,6 +169,6 @@ describe HelpController do
end
def stub_readme(content)
- allow(File).to receive(:read).and_return(content)
+ expect(File).to receive(:read).and_return(content)
end
end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 3b035eea7d5..56fff2771ec 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -410,6 +410,18 @@ describe Projects::EnvironmentsController do
expect(json_response['last_update']).to eq(42)
end
end
+
+ context 'permissions' do
+ before do
+ allow(controller).to receive(:can?).and_return true
+ end
+
+ it 'checks :metrics_dashboard ability' do
+ expect(controller).to receive(:can?).with(anything, :metrics_dashboard, anything)
+
+ get :metrics, params: environment_params
+ end
+ end
end
describe 'GET #additional_metrics' do
@@ -473,6 +485,18 @@ describe Projects::EnvironmentsController do
.to raise_error(ActionController::ParameterMissing)
end
end
+
+ context 'permissions' do
+ before do
+ allow(controller).to receive(:can?).and_return true
+ end
+
+ it 'checks :metrics_dashboard ability' do
+ expect(controller).to receive(:can?).with(anything, :metrics_dashboard, anything)
+
+ get :metrics, params: environment_params
+ end
+ end
end
describe 'GET #metrics_dashboard' do
@@ -648,6 +672,18 @@ describe Projects::EnvironmentsController do
it_behaves_like 'the default dashboard'
it_behaves_like 'dashboard can be specified'
it_behaves_like 'dashboard can be embedded'
+
+ context 'permissions' do
+ before do
+ allow(controller).to receive(:can?).and_return true
+ end
+
+ it 'checks :metrics_dashboard ability' do
+ expect(controller).to receive(:can?).with(anything, :metrics_dashboard, anything)
+
+ get :metrics, params: environment_params
+ end
+ end
end
describe 'GET #search' do
diff --git a/spec/features/dashboard/help_spec.rb b/spec/features/dashboard/help_spec.rb
deleted file mode 100644
index 73377453ba3..00000000000
--- a/spec/features/dashboard/help_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Dashboard Help' do
- before do
- sign_in(create(:user))
- end
-
- context 'documentation' do
- it 'renders correctly markdown' do
- visit help_page_path("administration/raketasks/maintenance")
-
- expect(page).to have_content('Gather GitLab and system information')
-
- node = find('.documentation h2 a#user-content-check-gitlab-configuration')
- expect(node[:href]).to eq '#check-gitlab-configuration'
- expect(find(:xpath, "#{node.path}/..").text).to eq 'Check GitLab configuration'
- end
- end
-end
diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
index 9c292fa0f2b..369b1a93957 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
@@ -489,15 +489,22 @@ describe('Settings Panel', () => {
.find('[name="project[project_feature_attributes][metrics_dashboard_access_level]"]')
.setValue(visibilityOptions.PUBLIC);
- expect(wrapper.vm.metricsAccessLevel).toBe(visibilityOptions.PUBLIC);
+ expect(wrapper.vm.metricsDashboardAccessLevel).toBe(visibilityOptions.PUBLIC);
});
it('should contain help text', () => {
- wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PRIVATE });
-
expect(wrapper.find({ ref: 'metrics-visibility-settings' }).props().helpText).toEqual(
'With Metrics Dashboard you can visualize this project performance metrics',
);
});
+
+ it('should disable the metrics visibility dropdown when the project visibility level changes to private', () => {
+ wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PRIVATE });
+
+ const metricsSettingsRow = wrapper.find({ ref: 'metrics-visibility-settings' });
+
+ expect(wrapper.vm.metricsOptionsDropdownEnabled).toBe(true);
+ expect(metricsSettingsRow.find('select').attributes('disabled')).toEqual('disabled');
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index 46e269e5071..4ef82b2dd4e 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -9,9 +9,9 @@ const markdownPreviewPath = `${TEST_HOST}/preview`;
const markdownDocsPath = `${TEST_HOST}/docs`;
function assertMarkdownTabs(isWrite, writeLink, previewLink, wrapper) {
- expect(writeLink.element.parentNode.classList.contains('active')).toEqual(isWrite);
- expect(previewLink.element.parentNode.classList.contains('active')).toEqual(!isWrite);
- expect(wrapper.find('.md-preview-holder').element.style.display).toEqual(isWrite ? 'none' : '');
+ expect(writeLink.element.parentNode.classList.contains('active')).toBe(isWrite);
+ expect(previewLink.element.parentNode.classList.contains('active')).toBe(!isWrite);
+ expect(wrapper.find('.md-preview-holder').element.style.display).toBe(isWrite ? 'none' : '');
}
function createComponent() {
@@ -67,6 +67,10 @@ describe('Markdown field component', () => {
let previewLink;
let writeLink;
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
it('renders textarea inside backdrop', () => {
wrapper = createComponent();
expect(wrapper.find('.zen-backdrop textarea').element).not.toBeNull();
@@ -92,32 +96,24 @@ describe('Markdown field component', () => {
previewLink = getPreviewLink(wrapper);
previewLink.trigger('click');
- wrapper.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick(() => {
expect(wrapper.find('.md-preview-holder').element.textContent.trim()).toContain(
'Loading…',
);
});
});
- it('renders markdown preview', () => {
+ it('renders markdown preview and GFM', () => {
wrapper = createComponent();
- previewLink = getPreviewLink(wrapper);
- previewLink.trigger('click');
+ const renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
- setTimeout(() => {
- expect(wrapper.find('.md-preview-holder').element.innerHTML).toContain(previewHTML);
- });
- });
-
- it('renders GFM with jQuery', () => {
- wrapper = createComponent();
previewLink = getPreviewLink(wrapper);
- jest.spyOn($.fn, 'renderGFM');
previewLink.trigger('click');
return axios.waitFor(markdownPreviewPath).then(() => {
expect(wrapper.find('.md-preview-holder').element.innerHTML).toContain(previewHTML);
+ expect(renderGFMSpy).toHaveBeenCalled();
});
});
@@ -176,7 +172,7 @@ describe('Markdown field component', () => {
const markdownButton = getMarkdownButton(wrapper);
markdownButton.trigger('click');
- wrapper.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick(() => {
expect(textarea.value).toContain('**testing**');
});
});
@@ -188,7 +184,7 @@ describe('Markdown field component', () => {
const markdownButton = getAllMarkdownButtons(wrapper).wrappers[5];
markdownButton.trigger('click');
- wrapper.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick(() => {
expect(textarea.value).toContain('* testing');
});
});
@@ -200,7 +196,7 @@ describe('Markdown field component', () => {
const markdownButton = getAllMarkdownButtons(wrapper).wrappers[5];
markdownButton.trigger('click');
- wrapper.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick(() => {
expect(textarea.value).toContain('* testing\n* 123');
});
});
diff --git a/spec/helpers/boards_helper_spec.rb b/spec/helpers/boards_helper_spec.rb
index f5e5285554c..cb9be9d5fb4 100644
--- a/spec/helpers/boards_helper_spec.rb
+++ b/spec/helpers/boards_helper_spec.rb
@@ -48,6 +48,10 @@ describe BoardsHelper do
it 'returns a board_lists_path as lists_endpoint' do
expect(helper.board_data[:lists_endpoint]).to eq(board_lists_path(board))
end
+
+ it 'returns board type as parent' do
+ expect(helper.board_data[:parent]).to eq('project')
+ end
end
describe '#current_board_json' do
diff --git a/spec/lib/gitlab/jira_import/labels_importer_spec.rb b/spec/lib/gitlab/jira_import/labels_importer_spec.rb
index 4d33ede136e..67eb541d376 100644
--- a/spec/lib/gitlab/jira_import/labels_importer_spec.rb
+++ b/spec/lib/gitlab/jira_import/labels_importer_spec.rb
@@ -10,7 +10,9 @@ describe Gitlab::JiraImport::LabelsImporter do
let_it_be(:project) { create(:project, group: group) }
let_it_be(:jira_service) { create(:jira_service, project: project) }
- subject { described_class.new(project).execute }
+ let(:importer) { described_class.new(project) }
+
+ subject { importer.execute }
before do
stub_feature_flags(jira_issue_import: true)
@@ -38,14 +40,13 @@ describe Gitlab::JiraImport::LabelsImporter do
let(:jira_labels_1) { { "maxResults" => 2, "startAt" => 0, "total" => 3, "isLast" => false, "values" => %w(backend bug) } }
let(:jira_labels_2) { { "maxResults" => 2, "startAt" => 2, "total" => 3, "isLast" => true, "values" => %w(feature) } }
- before do
- WebMock.stub_request(:get, 'https://jira.example.com/rest/api/2/label?maxResults=2&startAt=0')
- .to_return(body: jira_labels_1.to_json )
- WebMock.stub_request(:get, 'https://jira.example.com/rest/api/2/label?maxResults=2&startAt=2')
- .to_return(body: jira_labels_2.to_json )
- end
-
context 'when labels are returned from jira' do
+ before do
+ client = double
+ expect(importer).to receive(:client).twice.and_return(client)
+ allow(client).to receive(:get).twice.and_return(jira_labels_1, jira_labels_2)
+ end
+
it 'caches import label' do
expect(Gitlab::Cache::Import::Caching.read(Gitlab::JiraImport.import_label_cache_key(project.id))).to be nil
@@ -74,8 +75,9 @@ describe Gitlab::JiraImport::LabelsImporter do
let(:jira_labels) { { "maxResults" => 2, "startAt" => 0, "total" => 3, "values" => [] } }
before do
- WebMock.stub_request(:get, 'https://jira.example.com/rest/api/2/label?maxResults=2&startAt=0')
- .to_return(body: jira_labels.to_json )
+ client = double
+ expect(importer).to receive(:client).and_return(client)
+ allow(client).to receive(:get).and_return(jira_labels)
end
context 'when the labels field is empty' do
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index db643e3a31f..f214b1ccf17 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -29,6 +29,7 @@ describe ProjectPolicy do
admin_issue admin_label admin_list read_commit_status read_build
read_container_image read_pipeline read_environment read_deployment
read_merge_request download_wiki_code read_sentry_issue read_metrics_dashboard_annotation
+ metrics_dashboard
]
end
@@ -485,4 +486,190 @@ describe ProjectPolicy do
it { is_expected.to be_disallowed(:read_prometheus_alerts) }
end
end
+
+ describe 'metrics_dashboard feature' do
+ subject { described_class.new(current_user, project) }
+
+ context 'public project' do
+ let(:project) { create(:project, :public) }
+
+ context 'feature private' do
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_allowed(:metrics_dashboard) }
+ it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_allowed(:read_deployment) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+ end
+
+ context 'feature enabled' do
+ before do
+ project.project_feature.update(metrics_dashboard_access_level: ProjectFeature::ENABLED)
+ end
+
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_allowed(:metrics_dashboard) }
+ it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_allowed(:read_deployment) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_allowed(:metrics_dashboard) }
+ it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_allowed(:read_deployment) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_allowed(:metrics_dashboard) }
+ it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_allowed(:read_deployment) }
+ end
+ end
+ end
+
+ context 'internal project' do
+ let(:project) { create(:project, :internal) }
+
+ context 'feature private' do
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_allowed(:metrics_dashboard) }
+ it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_allowed(:read_deployment) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard)}
+ end
+ end
+
+ context 'feature enabled' do
+ before do
+ project.project_feature.update(metrics_dashboard_access_level: ProjectFeature::ENABLED)
+ end
+
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_allowed(:metrics_dashboard) }
+ it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_allowed(:read_deployment) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_allowed(:metrics_dashboard) }
+ it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_allowed(:read_deployment) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+ end
+ end
+
+ context 'private project' do
+ let(:project) { create(:project, :private) }
+
+ context 'feature private' do
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_allowed(:metrics_dashboard) }
+ it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_allowed(:read_deployment) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+ end
+
+ context 'feature enabled' do
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_allowed(:metrics_dashboard) }
+ it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_allowed(:read_deployment) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+ end
+ end
+
+ context 'feature disabled' do
+ before do
+ project.project_feature.update(metrics_dashboard_access_level: ProjectFeature::DISABLED)
+ end
+
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+ end
+ end
end
diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb
index 98040da9d2c..3831ddacb72 100644
--- a/spec/views/help/index.html.haml_spec.rb
+++ b/spec/views/help/index.html.haml_spec.rb
@@ -53,6 +53,18 @@ describe 'help/index' do
end
end
+ describe 'Markdown rendering' do
+ before do
+ assign(:help_index, 'Welcome to [GitLab](https://about.gitlab.com/) Documentation.')
+ end
+
+ it 'renders Markdown' do
+ render
+
+ expect(rendered).to have_link('GitLab', href: 'https://about.gitlab.com/')
+ end
+ end
+
def stub_user(user = double)
allow(view).to receive(:user_signed_in?).and_return(user)
end
diff --git a/spec/views/help/show.html.haml_spec.rb b/spec/views/help/show.html.haml_spec.rb
new file mode 100644
index 00000000000..539c647c1d3
--- /dev/null
+++ b/spec/views/help/show.html.haml_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'help/show' do
+ describe 'Markdown rendering' do
+ before do
+ assign(:path, 'ssh/README')
+ assign(:markdown, 'Welcome to [GitLab](https://about.gitlab.com/) Documentation.')
+ end
+
+ it 'renders Markdown' do
+ render
+
+ expect(rendered).to have_link('GitLab', href: 'https://about.gitlab.com/')
+ end
+ end
+end