diff options
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 Binary files differdeleted file mode 100644 index 69579fd1c1e..00000000000 --- a/doc/user/group/roadmap/img/roadmap_view_v12_10.png +++ /dev/null diff --git a/doc/user/group/roadmap/img/roadmap_view_v13_0.png b/doc/user/group/roadmap/img/roadmap_view_v13_0.png Binary files differnew file mode 100644 index 00000000000..a5b76b84418 --- /dev/null +++ b/doc/user/group/roadmap/img/roadmap_view_v13_0.png 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 |