diff --git a/app/assets/javascripts/notifications/components/notifications_dropdown.vue b/app/assets/javascripts/notifications/components/notifications_dropdown.vue index 69eb2115bf4..6b450c2b5fd 100644 --- a/app/assets/javascripts/notifications/components/notifications_dropdown.vue +++ b/app/assets/javascripts/notifications/components/notifications_dropdown.vue @@ -42,6 +42,9 @@ export default { showLabel: { default: false, }, + noFlip: { + default: false, + }, }, data() { return { @@ -127,6 +130,7 @@ export default { :disabled="disabled" :split="isCustomNotification" :text="buttonText" + :no-flip="noFlip" @click="openNotificationsModal" > { projectId, groupId, showLabel, + noFlip, } = el.dataset; return new Vue({ @@ -35,6 +36,7 @@ export default () => { projectId, groupId, showLabel: parseBoolean(showLabel), + noFlip: parseBoolean(noFlip), }, render(h) { return h(NotificationsDropdown); diff --git a/app/assets/javascripts/pages/projects/planning_hierarchy/index.js b/app/assets/javascripts/pages/projects/planning_hierarchy/index.js new file mode 100644 index 00000000000..d5dfe2d5f37 --- /dev/null +++ b/app/assets/javascripts/pages/projects/planning_hierarchy/index.js @@ -0,0 +1,3 @@ +import { initWorkItemsHierarchy } from '~/work_items_hierarchy/work_items_hierarchy_bundle'; + +initWorkItemsHierarchy(); diff --git a/app/assets/javascripts/work_items_hierarchy/components/app.vue b/app/assets/javascripts/work_items_hierarchy/components/app.vue new file mode 100644 index 00000000000..cc04ff026ff --- /dev/null +++ b/app/assets/javascripts/work_items_hierarchy/components/app.vue @@ -0,0 +1,95 @@ + + + diff --git a/app/assets/javascripts/work_items_hierarchy/components/hierarchy.vue b/app/assets/javascripts/work_items_hierarchy/components/hierarchy.vue new file mode 100644 index 00000000000..9b81218b6e4 --- /dev/null +++ b/app/assets/javascripts/work_items_hierarchy/components/hierarchy.vue @@ -0,0 +1,119 @@ + + diff --git a/app/assets/javascripts/work_items_hierarchy/constants.js b/app/assets/javascripts/work_items_hierarchy/constants.js new file mode 100644 index 00000000000..c9d78c300e3 --- /dev/null +++ b/app/assets/javascripts/work_items_hierarchy/constants.js @@ -0,0 +1,61 @@ +import { __ } from '~/locale'; + +export const WORK_ITEMS_SURVEY_COOKIE_NAME = 'hide_work_items_hierarchy_survey'; + +/** + * Hard-coded strings since we're rendering hierarchy + * items from mock responses. Remove this when we + * have a real hierarchy endpoint. + */ +export const LICENSE_PLAN = { + FREE: 'free', + PREMIUM: 'premium', + ULTIMATE: 'ultimate', +}; + +export const workItemTypes = { + EPIC: { + title: __('Epic'), + icon: 'epic', + color: '#694CC0', + backgroundColor: '#E1D8F9', + }, + ISSUE: { + title: __('Issue'), + icon: 'issues', + color: '#1068BF', + backgroundColor: '#CBE2F9', + }, + TASK: { + title: __('Task'), + icon: 'task-done', + color: '#217645', + backgroundColor: '#C3E6CD', + }, + INCIDENT: { + title: __('Incident'), + icon: 'issue-type-incident', + backgroundColor: '#db2a0f', + color: '#FDD4CD', + iconSize: 16, + }, + SUB_EPIC: { + title: __('Child epic'), + icon: 'epic', + color: '#AB6100', + backgroundColor: '#F5D9A8', + }, + REQUIREMENT: { + title: __('Requirement'), + icon: 'requirements', + color: '#0068c5', + backgroundColor: '#c5e3fb', + }, + TEST_CASE: { + title: __('Test case'), + icon: 'issue-type-test-case', + backgroundColor: '#007a3f', + color: '#bae8cb', + iconSize: 16, + }, +}; diff --git a/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js b/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js new file mode 100644 index 00000000000..61d93acdb91 --- /dev/null +++ b/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js @@ -0,0 +1,10 @@ +import { LICENSE_PLAN } from './constants'; + +export function inferLicensePlan({ hasSubEpics, hasEpics }) { + if (hasSubEpics) { + return LICENSE_PLAN.ULTIMATE; + } else if (hasEpics) { + return LICENSE_PLAN.PREMIUM; + } + return LICENSE_PLAN.FREE; +} diff --git a/app/assets/javascripts/work_items_hierarchy/static_response.js b/app/assets/javascripts/work_items_hierarchy/static_response.js new file mode 100644 index 00000000000..d1e2e486082 --- /dev/null +++ b/app/assets/javascripts/work_items_hierarchy/static_response.js @@ -0,0 +1,142 @@ +const FREE_TIER = 'free'; +const ULTIMATE_TIER = 'ultimate'; +const PREMIUM_TIER = 'premium'; + +const RESPONSE = { + [FREE_TIER]: [ + { + id: '1', + type: 'ISSUE', + available: true, + license: null, + nestedTypes: null, + }, + { + id: '2', + type: 'TASK', + available: true, + license: null, + nestedTypes: null, + }, + { + id: '3', + type: 'INCIDENT', + available: true, + license: null, + nestedTypes: null, + }, + { + id: '4', + type: 'EPIC', + available: false, + license: 'Premium', // eslint-disable-line @gitlab/require-i18n-strings + nestedTypes: null, + }, + { + id: '5', + type: 'SUB_EPIC', + available: false, + license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings + nestedTypes: null, + }, + { + id: '6', + type: 'REQUIREMENT', + available: false, + license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings + nestedTypes: null, + }, + { + id: '7', + type: 'TEST_CASE', + available: false, + license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings + nestedTypes: null, + }, + ], + + [PREMIUM_TIER]: [ + { + id: '1', + type: 'EPIC', + available: true, + license: null, + nestedTypes: ['ISSUE'], + }, + { + id: '2', + type: 'TASK', + available: true, + license: null, + nestedTypes: null, + }, + { + id: '3', + type: 'INCIDENT', + available: true, + license: null, + nestedTypes: null, + }, + { + id: '5', + type: 'SUB_EPIC', + available: false, + license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings + nestedTypes: null, + }, + { + id: '6', + type: 'REQUIREMENT', + available: false, + license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings + nestedTypes: null, + }, + { + id: '7', + type: 'TEST_CASE', + available: false, + license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings + nestedTypes: null, + }, + ], + + [ULTIMATE_TIER]: [ + { + id: '1', + type: 'EPIC', + available: true, + license: null, + nestedTypes: ['SUB_EPIC', 'ISSUE'], + }, + { + id: '2', + type: 'TASK', + available: true, + license: null, + nestedTypes: null, + }, + { + id: '3', + type: 'INCIDENT', + available: true, + license: null, + nestedTypes: null, + }, + { + id: '6', + type: 'REQUIREMENT', + available: true, + license: null, + nestedTypes: null, + }, + { + id: '7', + type: 'TEST_CASE', + available: true, + license: null, + nestedTypes: null, + }, + ], +}; + +export default RESPONSE; diff --git a/app/assets/javascripts/work_items_hierarchy/work_items_hierarchy_bundle.js b/app/assets/javascripts/work_items_hierarchy/work_items_hierarchy_bundle.js new file mode 100644 index 00000000000..2258c725301 --- /dev/null +++ b/app/assets/javascripts/work_items_hierarchy/work_items_hierarchy_bundle.js @@ -0,0 +1,26 @@ +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import App from './components/app.vue'; +import { inferLicensePlan } from './hierarchy_util'; + +export const initWorkItemsHierarchy = () => { + const el = document.querySelector('#js-work-items-hierarchy'); + + const { illustrationPath, hasEpics, hasSubEpics } = el.dataset; + + const licensePlan = inferLicensePlan({ + hasEpics: parseBoolean(hasEpics), + hasSubEpics: parseBoolean(hasSubEpics), + }); + + return new Vue({ + el, + provide: { + illustrationPath, + licensePlan, + }, + render(createElement) { + return createElement(App); + }, + }); +}; diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss index ff2b82d1806..24549a170bd 100644 --- a/app/assets/stylesheets/_page_specific_files.scss +++ b/app/assets/stylesheets/_page_specific_files.scss @@ -31,3 +31,4 @@ @import './pages/storage_quota'; @import './pages/tree'; @import './pages/users'; +@import './pages/hierarchy'; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 639b07bcc6b..b23b4e7d35d 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -525,32 +525,26 @@ -moz-osx-font-smoothing: grayscale; } - .fa-2x, .admonitionblock td.icon [class^='fa icon-'] { font-size: 2em; } - .fa-exclamation-triangle::before, .admonitionblock td.icon .icon-warning::before { content: '⚠'; } - .fa-exclamation-circle::before, .admonitionblock td.icon .icon-important::before { content: '❗'; } - .fa-lightbulb-o::before, .admonitionblock td.icon .icon-tip::before { content: '💡'; } - .fa-thumb-tack::before, .admonitionblock td.icon .icon-note::before { content: '📌'; } - .fa-fire::before, .admonitionblock td.icon .icon-caution::before { content: '🔥'; } diff --git a/app/assets/stylesheets/pages/hierarchy.scss b/app/assets/stylesheets/pages/hierarchy.scss new file mode 100644 index 00000000000..0812e4cc41e --- /dev/null +++ b/app/assets/stylesheets/pages/hierarchy.scss @@ -0,0 +1,15 @@ +.hierarchy-rounded-arrow-tail { + position: absolute; + top: 4px; + left: 5px; + height: calc(100% - 20px); +} + +.hierarchy-icon-wrapper { + height: $default-icon-size; + width: $default-icon-size; +} + +.hierarchy-rounded-arrow { + transform: scale(1, -1) rotate(90deg); +} diff --git a/app/controllers/concerns/planning_hierarchy.rb b/app/controllers/concerns/planning_hierarchy.rb new file mode 100644 index 00000000000..66f17ee688b --- /dev/null +++ b/app/controllers/concerns/planning_hierarchy.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module PlanningHierarchy + extend ActiveSupport::Concern + + # rubocop:disable Gitlab/ModuleWithInstanceVariables + def planning_hierarchy + return access_denied! unless can?(current_user, :read_planning_hierarchy, @project) + + return render_404 unless Feature.enabled?(:work_items_hierarchy, @project, default_enabled: :yaml) + + render 'shared/planning_hierarchy' + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables +end + +PlanningHierarchy.prepend_mod_with('PlanningHierarchy') diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 64abcd7cc33..c84d6256339 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -10,6 +10,7 @@ class ProjectsController < Projects::ApplicationController include ImportUrlParams include FiltersEvents include SourcegraphDecorator + include PlanningHierarchy prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) } @@ -54,6 +55,7 @@ class ProjectsController < Projects::ApplicationController feature_category :team_planning, [:preview_markdown, :new_issuable_address] feature_category :importers, [:export, :remove_export, :generate_new_export, :download_export] feature_category :code_review, [:unfoldered_environment_names] + feature_category :portfolio_management, [:planning_hierarchy] urgency :low, [:refs] urgency :high, [:unfoldered_environment_names] diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 0a3f0c6d424..1c3d6032d63 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -240,6 +240,7 @@ class ProjectPolicy < BasePolicy enable :read_wiki enable :read_issue enable :read_label + enable :read_planning_hierarchy enable :read_milestone enable :read_snippet enable :read_project_member @@ -572,6 +573,7 @@ class ProjectPolicy < BasePolicy enable :read_issue_board_list enable :read_wiki enable :read_label + enable :read_planning_hierarchy enable :read_milestone enable :read_snippet enable :read_project_member diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml index e5d67831c71..9b3a8c31d54 100644 --- a/app/views/groups/_home_panel.html.haml +++ b/app/views/groups/_home_panel.html.haml @@ -31,7 +31,7 @@ data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('admin') - if @notification_setting - .js-vue-notification-dropdown{ data: { disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), group_id: @group.id, container_class: 'gl-mx-2 gl-mt-3 gl-vertical-align-top' } } + .js-vue-notification-dropdown{ data: { disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), group_id: @group.id, container_class: 'gl-mx-2 gl-mt-3 gl-vertical-align-top', no_flip: 'true' } } - if can_create_subgroups .gl-px-2.gl-sm-w-auto.gl-w-full = link_to _("New subgroup"), new_group_path(parent_id: @group.id), class: "btn btn-default gl-button gl-mt-3 gl-sm-w-auto gl-w-full", data: { qa_selector: 'new_subgroup_button' } diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 016bdcf2ce6..8e6cc6da65d 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -37,7 +37,7 @@ = sprite_icon('admin') .gl-display-flex.gl-align-items-start.gl-mr-3 - if @notification_setting - .js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id } } + .js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id, no_flip: 'true' } } .count-buttons.gl-display-flex.gl-align-items-flex-start = render 'projects/buttons/star' diff --git a/app/views/shared/planning_hierarchy.html.haml b/app/views/shared/planning_hierarchy.html.haml new file mode 100644 index 00000000000..7ab5347b33d --- /dev/null +++ b/app/views/shared/planning_hierarchy.html.haml @@ -0,0 +1,5 @@ +- page_title _("Planning hierarchy") +- has_sub_epics = @project&.licensed_feature_available?(:subepics) +- has_epics = @project&.licensed_feature_available?(:epics) + +#js-work-items-hierarchy{ data: { has_sub_epics: has_sub_epics.to_s, has_epics: has_epics.to_s, illustration_path: image_path('illustrations/rocket-launch-md.svg') } } diff --git a/config/feature_flags/development/work_items_hierarchy.yml b/config/feature_flags/development/work_items_hierarchy.yml new file mode 100644 index 00000000000..0b3bc1bbe87 --- /dev/null +++ b/config/feature_flags/development/work_items_hierarchy.yml @@ -0,0 +1,8 @@ +--- +name: work_items_hierarchy +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79315 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350451 +milestone: '14.8' +type: development +group: group::product planning +default_enabled: false diff --git a/config/routes/project.rb b/config/routes/project.rb index 50e958106d9..e05033587df 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -465,6 +465,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do namespace :integrations do resource :shimo, only: [:show] end + + get :planning_hierarchy end # End of the /-/ scope. diff --git a/data/removals/14_0/deprecation_manage_access_14_0.yml b/data/removals/14_0/deprecation_manage_access_14_0.yml index 224a709085a..30167d23d60 100644 --- a/data/removals/14_0/deprecation_manage_access_14_0.yml +++ b/data/removals/14_0/deprecation_manage_access_14_0.yml @@ -7,7 +7,7 @@ body: | To improve performance, we are limiting the number of projects returned from the `GET /groups/:id/` API call to 100. A complete list of projects can still be retrieved with the `GET /groups/:id/projects` API call. -- name: "GitLab OAuth implicit grant deprecation" +- name: "GitLab OAuth implicit grant" removal_date: "2021-06-22" removal_milestone: "14.0" reporter: ogolowinski @@ -16,4 +16,4 @@ body: | GitLab is deprecating the [OAuth 2 implicit grant flow](https://docs.gitlab.com/ee/api/oauth2.html#implicit-grant-flow) as it has been removed for [OAuth 2.1](https://oauth.net/2.1/). - Beginning in 14.0, new applications can't be created with the OAuth 2 implicit grant flow. Existing OAuth implicit grant flows are no longer supported in 14.4. Migrate your existing applications to other supported [OAuth2 flows](https://docs.gitlab.com/ee/api/oauth2.html#supported-oauth2-flows) before release 14.4. + Migrate your existing applications to other supported [OAuth2 flows](https://docs.gitlab.com/ee/api/oauth2.html#supported-oauth2-flows). diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index 91fd8502bc9..7d83607ab28 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -32,7 +32,7 @@ GitLab supports the following authorization flows: hosted, first-party services. GitLab recommends against use of this flow. The draft specification for [OAuth 2.1](https://oauth.net/2.1/) specifically omits both the -Implicit grant and Resource Owner Password Credentials flows. It will be deprecated in the next OAuth specification version. +Implicit grant and Resource Owner Password Credentials flows. Refer to the [OAuth RFC](https://tools.ietf.org/html/rfc6749) to find out how all those flows work and pick the right one for your use case. @@ -239,19 +239,13 @@ You can now make requests to the API with the access token returned. ### Implicit grant flow -NOTE: -For a detailed flow diagram, see the [RFC specification](https://tools.ietf.org/html/rfc6749#section-4.2). - WARNING: Implicit grant flow is inherently insecure and the IETF has removed it in [OAuth 2.1](https://oauth.net/2.1/). -For this reason, [support for it is deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/288516). -In GitLab 14.0, new applications can't be created using it. In GitLab 14.4, support for it is -scheduled to be removed for existing applications. +It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/288516) for use in GitLab 14.0, and is planned for +[removal](https://gitlab.com/gitlab-org/gitlab/-/issues/344609) in GitLab 15.0. -We recommend that you use [Authorization code with PKCE](#authorization-code-with-proof-key-for-code-exchange-pkce) instead. If you choose to use Implicit flow, be sure to verify the -`application id` (or `client_id`) associated with the access token before granting -access to the data. To learn more, read -[Retrieving the token information](#retrieve-the-token-information)). +We recommend that you use [Authorization code with PKCE](#authorization-code-with-proof-key-for-code-exchange-pkce) +instead. Unlike the authorization code flow, the client receives an `access token` immediately as a result of the authorization request. The flow does not use the @@ -415,7 +409,7 @@ The following is an example response: The fields `scopes` and `expires_in_seconds` are included in the response. -These are aliases for `scope` and `expires_in` respectively, and have been included to +These fields are aliases for `scope` and `expires_in` respectively, and have been included to prevent breaking changes introduced in [doorkeeper 5.0.2](https://github.com/doorkeeper-gem/doorkeeper/wiki/Migration-from-old-versions#from-4x-to-5x). Don't rely on these fields as they are slated for removal in a later release. diff --git a/doc/ci/pipelines/settings.md b/doc/ci/pipelines/settings.md index 8ba84338dfb..38516d83b89 100644 --- a/doc/ci/pipelines/settings.md +++ b/doc/ci/pipelines/settings.md @@ -211,16 +211,17 @@ averaged. To define a coverage-parsing regular expression: -1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Settings > CI/CD**. -1. Expand **General pipelines**. -1. In the **Test coverage parsing** field, enter a regular expression. - Leave blank to disable this feature. +- In the GitLab UI: -Alternatively, provide a regular expression using the [`coverage`](../yaml/index.md#coverage) -keyword in your project's `.gitlab-ci.yml`. + 1. On the top bar, select **Menu > Projects** and find your project. + 1. On the left sidebar, select **Settings > CI/CD**. + 1. Expand **General pipelines**. + 1. In the **Test coverage parsing** field, enter a regular expression. Leave blank to disable this feature. -You can use to test your regex. The regex returns the **last** +- Using the project's `.gitlab-ci.yml`, provide a regular expression using the [`coverage`](../yaml/index.md#coverage) + keyword. + +You can use to test your regular expression. The regular expression returns the **last** match found in the output. ### Test coverage examples diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 29228d56848..bdfda0252a2 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -510,6 +510,9 @@ When reviewing merge requests added by wider community contributors: fetching of malicious packages. - Review links and images, especially in documentation MRs. - When in doubt, ask someone from `@gitlab-com/gl-security/appsec` to review the merge request **before manually starting any merge request pipeline**. +- Only set the milestone when the merge request is likely to be included in + the current milestone. This is to avoid confusion around when it'll be + merged and avoid moving milestone too often when it's not yet ready. If the MR source branch is more than 1,000 commits behind the target branch: diff --git a/doc/development/testing_guide/flaky_tests.md b/doc/development/testing_guide/flaky_tests.md index 333ebd8370f..09fc8f4d33e 100644 --- a/doc/development/testing_guide/flaky_tests.md +++ b/doc/development/testing_guide/flaky_tests.md @@ -106,6 +106,7 @@ reproduction. - [Lazy loaded images can cause Capybara to mis-click](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18713) - [Triggering JS events before the event handlers are set up](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18742) - [Wait for the image to be lazy-loaded when asserting on a Markdown image's `src` attribute](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25408) +- [Avoid asserting against flash notice banners](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79432) #### Capybara viewport size related issues diff --git a/doc/update/removals.md b/doc/update/removals.md index 550de5ff41a..15acc2c189d 100644 --- a/doc/update/removals.md +++ b/doc/update/removals.md @@ -175,7 +175,7 @@ As [announced in GitLab 13.3](https://about.gitlab.com/releases/2020/08/22/gitla - `geo_postgresql['fdw_external_password']` - `gitlab-_rails['geo_migrated_local_files_clean_up_worker_cron']` -### GitLab OAuth implicit grant deprecation +### GitLab OAuth implicit grant WARNING: This feature was changed or removed in 14.0 @@ -185,7 +185,7 @@ changes to your code, settings, or workflow. GitLab is deprecating the [OAuth 2 implicit grant flow](https://docs.gitlab.com/ee/api/oauth2.html#implicit-grant-flow) as it has been removed for [OAuth 2.1](https://oauth.net/2.1/). -Beginning in 14.0, new applications can't be created with the OAuth 2 implicit grant flow. Existing OAuth implicit grant flows are no longer supported in 14.4. Migrate your existing applications to other supported [OAuth2 flows](https://docs.gitlab.com/ee/api/oauth2.html#supported-oauth2-flows) before release 14.4. +Migrate your existing applications to other supported [OAuth2 flows](https://docs.gitlab.com/ee/api/oauth2.html#supported-oauth2-flows). ### GitLab Runner helper image in GitLab.com Container Registry diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md index 5d365491e30..4b601f0f877 100644 --- a/doc/user/application_security/dast/index.md +++ b/doc/user/application_security/dast/index.md @@ -53,7 +53,7 @@ results. On failure, the analyzer outputs an - [GitLab Runner](../../../ci/runners/index.md) available, with the [`docker` executor](https://docs.gitlab.com/runner/executors/docker.html). - Target application deployed. For more details, read [Deployment options](#deployment-options). -- DAST runs in the `test` stage, which is available by default. If you redefine the stages in the `.gitlab-ci.yml` file, the `test` stage is required. +- DAST runs in the `dast` stage, which must be added manually to your `.gitlab-ci.yml`. ### Deployment options diff --git a/doc/user/group/planning_hierarchy/img/view-project-work-item-hierarchy_v14_8.png b/doc/user/group/planning_hierarchy/img/view-project-work-item-hierarchy_v14_8.png new file mode 100644 index 00000000000..2ddd551ee46 Binary files /dev/null and b/doc/user/group/planning_hierarchy/img/view-project-work-item-hierarchy_v14_8.png differ diff --git a/doc/user/group/planning_hierarchy/index.md b/doc/user/group/planning_hierarchy/index.md index 5887328abe4..2abacf2741c 100644 --- a/doc/user/group/planning_hierarchy/index.md +++ b/doc/user/group/planning_hierarchy/index.md @@ -20,6 +20,20 @@ To learn about hierarchies in general, common frameworks, and using GitLab for portfolio management, see [How to use GitLab for Agile portfolio planning and project management](https://about.gitlab.com/blog/2020/11/11/gitlab-for-agile-portfolio-planning-project-management/). +## View planning hierarchies + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340844/) in GitLab 14.8 and is behind the feature flag `work_items_hierarchy`. + +To view the planning hierarchy in a project: + +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Project information > Planning hierarchy**. + +Under **Current structure**, you can see a hierarchy diagram that matches your current planning hierarchy. +The work items outside your subscription plan show up below **Unavailable structure**. + +![Screenshot showing hierarchy page](img/view-project-work-item-hierarchy_v14_8.png) + ## Hierarchies with epics With epics, you can achieve the following hierarchy: diff --git a/lib/sidebars/concerns/work_item_hierarchy.rb b/lib/sidebars/concerns/work_item_hierarchy.rb new file mode 100644 index 00000000000..8e48f004bc7 --- /dev/null +++ b/lib/sidebars/concerns/work_item_hierarchy.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# This module has the necessary methods to render +# work items hierarchy menu +module Sidebars + module Concerns + module WorkItemHierarchy + def hierarchy_menu_item(container, url, path) + unless show_hierarachy_menu_item?(container) + return ::Sidebars::NilMenuItem.new(item_id: :hierarchy) + end + + ::Sidebars::MenuItem.new( + title: _('Planning hierarchy'), + link: url, + active_routes: { path: path }, + item_id: :hierarchy + ) + end + + def show_hierarachy_menu_item?(container) + Feature.enabled?(:work_items_hierarchy, container, default_enabled: :yaml) && + can?(context.current_user, :read_planning_hierarchy, container) + end + end + end +end diff --git a/lib/sidebars/projects/menus/project_information_menu.rb b/lib/sidebars/projects/menus/project_information_menu.rb index 44b94ee3522..4056d50d324 100644 --- a/lib/sidebars/projects/menus/project_information_menu.rb +++ b/lib/sidebars/projects/menus/project_information_menu.rb @@ -4,10 +4,13 @@ module Sidebars module Projects module Menus class ProjectInformationMenu < ::Sidebars::Menu + include ::Sidebars::Concerns::WorkItemHierarchy + override :configure_menu_items def configure_menu_items add_item(activity_menu_item) add_item(labels_menu_item) + add_item(hierarchy_menu_item(context.project, project_planning_hierarchy_path(context.project), 'projects#planning_hierarchy')) add_item(members_menu_item) true diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 25a83ad9cf7..0d7b2f75729 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -7113,6 +7113,9 @@ msgstr "" msgid "Child" msgstr "" +msgid "Child epic" +msgstr "" + msgid "Child epic does not exist." msgstr "" @@ -17725,6 +17728,33 @@ msgstr[1] "" msgid "Hide values" msgstr "" +msgid "Hierarchy|Current structure" +msgstr "" + +msgid "Hierarchy|Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals." +msgstr "" + +msgid "Hierarchy|Help us improve work items in GitLab!" +msgstr "" + +msgid "Hierarchy|Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you." +msgstr "" + +msgid "Hierarchy|Planning hierarchy" +msgstr "" + +msgid "Hierarchy|Take the work items survey" +msgstr "" + +msgid "Hierarchy|These items are unavailable in the current structure." +msgstr "" + +msgid "Hierarchy|Unavailable structure" +msgstr "" + +msgid "Hierarchy|You can start using these items now." +msgstr "" + msgid "High or unknown vulnerabilities present" msgstr "" @@ -26636,6 +26666,9 @@ msgstr "" msgid "Plan:" msgstr "" +msgid "Planning hierarchy" +msgstr "" + msgid "PlantUML" msgstr "" @@ -30373,6 +30406,9 @@ msgstr "" msgid "Required only if you are not using role instance credentials." msgstr "" +msgid "Requirement" +msgstr "" + msgid "Requirement %{reference} has been added" msgstr "" @@ -33033,6 +33069,9 @@ msgstr "" msgid "Show all breadcrumbs" msgstr "" +msgid "Show all epics" +msgstr "" + msgid "Show all issues." msgstr "" @@ -33045,6 +33084,9 @@ msgstr "" msgid "Show archived projects only" msgstr "" +msgid "Show closed epics" +msgstr "" + msgid "Show command" msgstr "" @@ -33081,6 +33123,9 @@ msgstr "" msgid "Show one file at a time" msgstr "" +msgid "Show open epics" +msgstr "" + msgid "Show the Closed list" msgstr "" @@ -35376,6 +35421,9 @@ msgstr "" msgid "Test Cases" msgstr "" +msgid "Test case" +msgstr "" + msgid "Test coverage parsing" msgstr "" diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh index a6d9899bf2a..3bcd87aee2e 100644 --- a/scripts/rspec_helpers.sh +++ b/scripts/rspec_helpers.sh @@ -275,7 +275,7 @@ function rspec_paralellized_job() { export MEMORY_TEST_PATH="tmp/memory_test/${report_name}_memory.csv" if [[ -n $RSPEC_TESTS_MAPPING_ENABLED ]]; then - tooling/bin/parallel_rspec --rspec_args "$(rspec_args)" --filter "tmp/matching_tests.txt" || rspec_run_status=$? + tooling/bin/parallel_rspec --rspec_args "$(rspec_args "${rspec_opts}")" --filter "tmp/matching_tests.txt" || rspec_run_status=$? else tooling/bin/parallel_rspec --rspec_args "$(rspec_args "${rspec_opts}")" || rspec_run_status=$? fi diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb index 09025e56728..c38292f81bf 100644 --- a/spec/features/projects/members/member_leaves_project_spec.rb +++ b/spec/features/projects/members/member_leaves_project_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'Projects > Members > Member leaves project' do + include Spec::Support::Helpers::Features::MembersHelpers + let(:user) { create(:user) } let(:project) { create(:project, :repository) } @@ -25,10 +27,14 @@ RSpec.describe 'Projects > Members > Member leaves project' do visit project_path(project, leave: 1) page.accept_confirm - wait_for_all_requests - expect(find('.flash-notice')).to have_content "You left the \"#{project.full_name}\" project" + expect(current_path).to eq(dashboard_projects_path) - expect(project.users.exists?(user.id)).to be_falsey + + sign_in(project.first_owner) + + visit project_project_members_path(project) + + expect(members_table).not_to have_content(user.name) end end diff --git a/spec/frontend/notifications/components/notifications_dropdown_spec.js b/spec/frontend/notifications/components/notifications_dropdown_spec.js index e12251ce6d9..7ca6c2052ae 100644 --- a/spec/frontend/notifications/components/notifications_dropdown_spec.js +++ b/spec/frontend/notifications/components/notifications_dropdown_spec.js @@ -195,6 +195,14 @@ describe('NotificationsDropdown', () => { ); }); }); + + it('passes provided `noFlip` value to `GlDropdown`', () => { + wrapper = createComponent({ + noFlip: true, + }); + + expect(findDropdown().attributes('no-flip')).toBe('true'); + }); }); describe('when selecting an item', () => { diff --git a/spec/frontend/work_items_hierarchy/components/app_spec.js b/spec/frontend/work_items_hierarchy/components/app_spec.js new file mode 100644 index 00000000000..092e9c90553 --- /dev/null +++ b/spec/frontend/work_items_hierarchy/components/app_spec.js @@ -0,0 +1,63 @@ +import { nextTick } from 'vue'; +import { createLocalVue, mount } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import { GlBanner } from '@gitlab/ui'; +import App from '~/work_items_hierarchy/components/app.vue'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +describe('WorkItemsHierarchy App', () => { + let wrapper; + const createComponent = (props = {}, data = {}) => { + wrapper = extendedWrapper( + mount(App, { + localVue, + provide: { + illustrationPath: '/foo.svg', + licensePlan: 'free', + ...props, + }, + data() { + return data; + }, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('survey banner', () => { + it('shows when the banner is visible', () => { + createComponent({}, { bannerVisible: true }); + + expect(wrapper.find(GlBanner).exists()).toBe(true); + }); + + it('hide when close is called', async () => { + createComponent({}, { bannerVisible: true }); + + wrapper.findByTestId('close-icon').trigger('click'); + + await nextTick(); + + expect(wrapper.find(GlBanner).exists()).toBe(false); + }); + }); + + describe('Unavailable structure', () => { + it.each` + licensePlan | visible + ${'free'} | ${true} + ${'premium'} | ${true} + ${'ultimate'} | ${false} + `('visibility is $visible when plan is $licensePlan', ({ licensePlan, visible }) => { + createComponent({ licensePlan }); + + expect(wrapper.findByTestId('unavailable-structure').exists()).toBe(visible); + }); + }); +}); diff --git a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js new file mode 100644 index 00000000000..74774e38d6b --- /dev/null +++ b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js @@ -0,0 +1,118 @@ +import { createLocalVue, mount } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import { GlBadge } from '@gitlab/ui'; +import Hierarchy from '~/work_items_hierarchy/components/hierarchy.vue'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import RESPONSE from '~/work_items_hierarchy/static_response'; +import { workItemTypes } from '~/work_items_hierarchy/constants'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +describe('WorkItemsHierarchy Hierarchy', () => { + let wrapper; + + const workItemsFromResponse = (response) => { + return response.reduce( + (itemTypes, item) => { + const key = item.available ? 'available' : 'unavailable'; + itemTypes[key].push({ + ...item, + ...workItemTypes[item.type], + nestedTypes: item.nestedTypes + ? item.nestedTypes.map((type) => workItemTypes[type]) + : null, + }); + return itemTypes; + }, + { available: [], unavailable: [] }, + ); + }; + + const createComponent = (props = {}) => { + wrapper = extendedWrapper( + mount(Hierarchy, { + localVue, + propsData: { + workItemTypes: props.workItemTypes, + ...props, + }, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('available structure', () => { + let items = []; + + beforeEach(() => { + items = workItemsFromResponse(RESPONSE.ultimate).available; + createComponent({ workItemTypes: items }); + }); + + it('renders all work items', () => { + expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length); + }); + + it('does not render badges', () => { + expect(wrapper.find(GlBadge).exists()).toBe(false); + }); + }); + + describe('unavailable structure', () => { + let items = []; + + beforeEach(() => { + items = workItemsFromResponse(RESPONSE.premium).unavailable; + createComponent({ workItemTypes: items }); + }); + + it('renders all work items', () => { + expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length); + }); + + it('renders license badges for all work items', () => { + expect(wrapper.findAll(GlBadge)).toHaveLength(items.length); + }); + + it('does not render svg icon for linking', () => { + expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe(false); + expect(wrapper.findByTestId('level-up-icon').exists()).toBe(false); + }); + }); + + describe('nested work items', () => { + describe.each` + licensePlan | arrowTailVisible | levelUpIconVisible | arrowDownIconVisible + ${'ultimate'} | ${true} | ${true} | ${true} + ${'premium'} | ${false} | ${false} | ${true} + ${'free'} | ${false} | ${false} | ${false} + `( + 'when $licensePlan license', + ({ licensePlan, arrowTailVisible, levelUpIconVisible, arrowDownIconVisible }) => { + let items = []; + beforeEach(() => { + items = workItemsFromResponse(RESPONSE[licensePlan]).available; + createComponent({ workItemTypes: items }); + }); + + it(`${arrowTailVisible ? 'render' : 'does not render'} arrow tail svg`, () => { + expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe( + arrowTailVisible, + ); + }); + + it(`${levelUpIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => { + expect(wrapper.findByTestId('level-up-icon').exists()).toBe(levelUpIconVisible); + }); + + it(`${arrowDownIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => { + expect(wrapper.findByTestId('arrow-down-icon').exists()).toBe(arrowDownIconVisible); + }); + }, + ); + }); +}); diff --git a/spec/frontend/work_items_hierarchy/hierarchy_util_spec.js b/spec/frontend/work_items_hierarchy/hierarchy_util_spec.js new file mode 100644 index 00000000000..9042fa27d16 --- /dev/null +++ b/spec/frontend/work_items_hierarchy/hierarchy_util_spec.js @@ -0,0 +1,16 @@ +import { inferLicensePlan } from '~/work_items_hierarchy/hierarchy_util'; +import { LICENSE_PLAN } from '~/work_items_hierarchy/constants'; + +describe('inferLicensePlan', () => { + it.each` + epics | subEpics | licensePlan + ${true} | ${true} | ${LICENSE_PLAN.ULTIMATE} + ${true} | ${false} | ${LICENSE_PLAN.PREMIUM} + ${false} | ${false} | ${LICENSE_PLAN.FREE} + `( + 'returns $licensePlan when epic is $epics and sub-epic is $subEpics', + ({ epics, subEpics, licensePlan }) => { + expect(inferLicensePlan({ hasEpics: epics, hasSubEpics: subEpics })).toBe(licensePlan); + }, + ); +}); diff --git a/spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb b/spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb new file mode 100644 index 00000000000..f0a5e032764 --- /dev/null +++ b/spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Concerns::WorkItemHierarchy do + shared_examples 'hierarchy menu' do + let(:item_id) { :hierarchy } + + context 'when the feature is disabled does not render' do + before do + stub_feature_flags(work_items_hierarchy: false) + end + + specify { is_expected.to be_nil } + end + + context 'when the feature is enabled does render' do + before do + stub_feature_flags(work_items_hierarchy: true) + end + + specify { is_expected.not_to be_nil } + end + end + + describe 'Project hierarchy menu item' do + let_it_be_with_reload(:project) { create(:project, :repository) } + + let(:user) { project.owner } + let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) } + + subject { Sidebars::Projects::Menus::ProjectInformationMenu.new(context).renderable_items.index { |e| e.item_id == item_id } } + + it_behaves_like 'hierarchy menu' + end +end diff --git a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb index 7ff06ac229e..76367782d68 100644 --- a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb @@ -59,5 +59,25 @@ RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do specify { is_expected.to be_nil } end end + + describe 'Hierarchy' do + let(:item_id) { :hierarchy } + + context 'when the feature is disabled' do + before do + stub_feature_flags(work_items_hierarchy: false) + end + + specify { is_expected.to be_nil } + end + + context 'when the feature is enabled' do + before do + stub_feature_flags(work_items_hierarchy: true) + end + + specify { is_expected.not_to be_nil } + end + end end end diff --git a/spec/requests/concerns/planning_hierarchy_spec.rb b/spec/requests/concerns/planning_hierarchy_spec.rb new file mode 100644 index 00000000000..c8d5a3b6b53 --- /dev/null +++ b/spec/requests/concerns/planning_hierarchy_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe PlanningHierarchy, type: :request do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + + before do + project.add_maintainer(user) + sign_in(user) + end + + describe 'GET #planning_hierarchy' do + it 'renders planning hierarchy' do + stub_feature_flags(work_items_hierarchy: true) + + get project_planning_hierarchy_path(project) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to match(/id="js-work-items-hierarchy"/) + end + + it 'renders 404 page' do + stub_feature_flags(work_items_hierarchy: false) + + get project_planning_hierarchy_path(project) + + expect(response).to have_gitlab_http_status(:not_found) + expect(response.body).not_to match(/id="js-work-items-hierarchy"/) + end + end +end diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 27967850389..576a8aa44fa 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -22,6 +22,7 @@ RSpec.shared_context 'project navbar structure' do nav_sub_items: [ _('Activity'), _('Labels'), + _('Planning hierarchy'), _('Members') ] }, diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb index c39252cef13..3641edc845a 100644 --- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb @@ -17,7 +17,7 @@ RSpec.shared_context 'ProjectPolicy context' do %i[ award_emoji create_issue create_merge_request_in create_note create_project read_issue_board read_issue read_issue_iid read_issue_link - read_label read_issue_board_list read_milestone read_note read_project + read_label read_planning_hierarchy read_issue_board_list read_milestone read_note read_project read_project_for_iids read_project_member read_release read_snippet read_wiki upload_file ]