Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-01-31 06:12:59 +00:00
parent 358bd7fce3
commit 62aae3415c
43 changed files with 939 additions and 38 deletions

View File

@ -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"
>
<notifications-dropdown-item

View File

@ -21,6 +21,7 @@ export default () => {
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);

View File

@ -0,0 +1,3 @@
import { initWorkItemsHierarchy } from '~/work_items_hierarchy/work_items_hierarchy_bundle';
initWorkItemsHierarchy();

View File

@ -0,0 +1,95 @@
<script>
import { GlBanner } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils';
import RESPONSE from '../static_response';
import { WORK_ITEMS_SURVEY_COOKIE_NAME, workItemTypes } from '../constants';
import Hierarchy from './hierarchy.vue';
export default {
components: {
GlBanner,
Hierarchy,
},
inject: ['illustrationPath', 'licensePlan'],
data() {
return {
bannerVisible: !parseBoolean(Cookies.get(WORK_ITEMS_SURVEY_COOKIE_NAME)),
workItemHierarchy: RESPONSE[this.licensePlan],
};
},
computed: {
hasUnavailableStructure() {
return this.workItemTypes.unavailable.length > 0;
},
workItemTypes() {
return this.workItemHierarchy.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: [] },
);
},
},
methods: {
handleClose() {
Cookies.set(WORK_ITEMS_SURVEY_COOKIE_NAME, 'true', { expires: 365 * 10 });
this.bannerVisible = false;
},
},
};
</script>
<template>
<div>
<gl-banner
v-if="bannerVisible"
class="gl-mt-4 gl-px-5!"
:title="s__('Hierarchy|Help us improve work items in GitLab!')"
:button-text="s__('Hierarchy|Take the work items survey')"
button-link="https://forms.gle/u1BmRp8rTbwj52iq5"
:svg-path="illustrationPath"
@close="handleClose"
>
<p>
{{
s__(
'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.',
)
}}
</p>
</gl-banner>
<h3 class="gl-mt-5!">{{ s__('Hierarchy|Planning hierarchy') }}</h3>
<p>
{{
s__(
'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.',
)
}}
</p>
<div class="gl-font-weight-bold gl-mb-2">{{ s__('Hierarchy|Current structure') }}</div>
<p class="gl-mb-3!">{{ s__('Hierarchy|You can start using these items now.') }}</p>
<hierarchy :work-item-types="workItemTypes.available" />
<div
v-if="hasUnavailableStructure"
data-testid="unavailable-structure"
class="gl-font-weight-bold gl-mt-5 gl-mb-2"
>
{{ s__('Hierarchy|Unavailable structure') }}
</div>
<p v-if="hasUnavailableStructure" class="gl-mb-3!">
{{ s__('Hierarchy|These items are unavailable in the current structure.') }}
</p>
<hierarchy :work-item-types="workItemTypes.unavailable" />
</div>
</template>

View File

@ -0,0 +1,119 @@
<script>
import { GlIcon, GlBadge } from '@gitlab/ui';
export default {
components: {
GlIcon,
GlBadge,
},
props: {
workItemTypes: {
type: Array,
required: true,
},
},
methods: {
isLastItem(index, workItem) {
const hasMoreThanOneItem = workItem.nestedTypes.length > 1;
const isLastItemInArray = index === workItem.nestedTypes.length - 1;
return isLastItemInArray && hasMoreThanOneItem;
},
nestedWorkItemTypeMargin(index, workItem) {
const isLastItemInArray = index === workItem.nestedTypes.length - 1;
const hasMoreThanOneItem = workItem.nestedTypes.length > 1;
if (isLastItemInArray && hasMoreThanOneItem) {
return 'gl-ml-0';
}
return 'gl-ml-6';
},
},
};
</script>
<template>
<div>
<div
v-for="workItem in workItemTypes"
:key="workItem.id"
class="gl-mb-3"
:class="{ flex: !workItem.available }"
>
<span
class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal"
data-testid="work-item-wrapper"
>
<span
:style="{
backgroundColor: workItem.backgroundColor,
color: workItem.color,
}"
class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper"
>
<gl-icon :size="workItem.iconSize || 12" :name="workItem.icon" />
</span>
{{ workItem.title }}
</span>
<gl-badge
v-if="!workItem.available"
variant="info"
icon="license"
size="sm"
class="gl-ml-3 gl-align-self-center"
>{{ workItem.license }}</gl-badge
>
<div v-if="workItem.nestedTypes" :class="{ 'gl-relative': workItem.nestedTypes.length > 1 }">
<svg
v-if="workItem.nestedTypes.length > 1"
class="hierarchy-rounded-arrow-tail gl-text-gray-400"
data-testid="hierarchy-rounded-arrow-tail"
width="2"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="0.75"
y1="1"
x2="0.75"
y2="100%"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
</svg>
<template v-for="(nestedWorkItem, index) in workItem.nestedTypes">
<div :key="nestedWorkItem.id" class="gl-display-block gl-mt-2 gl-ml-6">
<gl-icon name="arrow-down" class="gl-text-gray-400" />
</div>
<gl-icon
v-if="isLastItem(index, workItem)"
:key="nestedWorkItem.id"
name="level-up"
class="gl-text-gray-400 gl-ml-2 hierarchy-rounded-arrow"
/>
<span
:key="nestedWorkItem.id"
class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-mt-2 gl-line-height-normal"
:class="nestedWorkItemTypeMargin(index, workItem)"
>
<span
:style="{
backgroundColor: nestedWorkItem.backgroundColor,
color: nestedWorkItem.color,
}"
class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper"
>
<gl-icon :size="nestedWorkItem.iconSize || 12" :name="nestedWorkItem.icon" />
</span>
{{ nestedWorkItem.title }}
</span>
</template>
</div>
</div>
</div>
</template>

View File

@ -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,
},
};

View File

@ -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;
}

View File

@ -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;

View File

@ -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);
},
});
};

View File

@ -31,3 +31,4 @@
@import './pages/storage_quota';
@import './pages/tree';
@import './pages/users';
@import './pages/hierarchy';

View File

@ -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: '🔥';
}

View File

@ -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);
}

View File

@ -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')

View File

@ -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]

View File

@ -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

View File

@ -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' }

View File

@ -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'

View File

@ -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') } }

View File

@ -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

View File

@ -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.

View File

@ -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).

View File

@ -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.

View File

@ -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 <https://rubular.com> 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 <https://rubular.com> to test your regular expression. The regular expression returns the **last**
match found in the output.
### Test coverage examples

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

@ -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

View File

@ -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', () => {

View File

@ -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);
});
});
});

View File

@ -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);
});
},
);
});
});

View File

@ -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);
},
);
});

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -22,6 +22,7 @@ RSpec.shared_context 'project navbar structure' do
nav_sub_items: [
_('Activity'),
_('Labels'),
_('Planning hierarchy'),
_('Members')
]
},

View File

@ -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
]