Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-12-28 15:07:23 +00:00
parent d30b2ead9a
commit 20d52aa39e
32 changed files with 351 additions and 128 deletions

View File

@ -1,14 +1,5 @@
<script>
import {
GlButton,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlDropdownSectionHeader,
GlSearchBoxByType,
GlTruncate,
} from '@gitlab/ui';
import { GlButton, GlButtonGroup, GlCollapsibleListbox } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
import { s__ } from '~/locale';
@ -20,12 +11,7 @@ export default {
components: {
GlButton,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlDropdownSectionHeader,
GlSearchBoxByType,
GlTruncate,
GlCollapsibleListbox,
},
apollo: {
project: {
@ -61,24 +47,25 @@ export default {
};
},
computed: {
loading() {
return this.$apollo.queries.project.loading;
},
rootUrl() {
return `${gon.gitlab_url}/`;
},
namespaces() {
return this.project.forkTargets?.nodes || [];
},
hasMatches() {
return this.namespaces.length;
},
dropdownText() {
return this.selectedNamespace?.fullPath || s__('ForkProject|Select a namespace');
},
namespaceItems() {
return this.namespaces?.map(({ id, fullPath }) => ({ value: id, text: fullPath }));
},
},
methods: {
handleDropdownShown() {
this.$refs.search.focusInput();
},
setNamespace(namespace) {
setNamespace(namespaceId) {
const namespace = this.namespaces.find(({ id }) => id === namespaceId);
const id = getIdFromGraphQLId(namespace.id);
this.$emit('select', {
@ -89,6 +76,9 @@ export default {
this.selectedNamespace = { id, fullPath: namespace.fullPath };
},
searchNamespaces(search) {
this.search = search;
},
},
};
</script>
@ -98,39 +88,19 @@ export default {
<gl-button class="gl-text-truncate gl-flex-grow-0! gl-max-w-34" label :title="rootUrl">{{
rootUrl
}}</gl-button>
<gl-dropdown
<gl-collapsible-listbox
class="gl-flex-grow-1"
toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base! gl-w-20"
data-qa-selector="select_namespace_dropdown"
data-testid="select_namespace_dropdown"
no-flip
@shown="handleDropdownShown"
>
<template #button-text>
<gl-truncate :text="dropdownText" position="start" with-tooltip />
</template>
<gl-search-box-by-type
ref="search"
v-model.trim="search"
:is-loading="$apollo.queries.project.loading"
data-qa-selector="select_namespace_dropdown_search_field"
data-testid="select_namespace_dropdown_search_field"
/>
<template v-if="!$apollo.queries.project.loading">
<template v-if="hasMatches">
<gl-dropdown-section-header>{{ __('Namespaces') }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="namespace of namespaces"
:key="namespace.id"
data-qa-selector="select_namespace_dropdown_item"
@click="setNamespace(namespace)"
>
{{ namespace.fullPath }}
</gl-dropdown-item>
</template>
<gl-dropdown-text v-else>{{ __('No matches found') }}</gl-dropdown-text>
</template>
</gl-dropdown>
:items="namespaceItems"
:header-text="__('Namespaces')"
:no-results-text="__('No matches found')"
:searchable="true"
:searching="loading"
toggle-class="gl-flex-direction-column gl-align-items-stretch!"
:toggle-text="dropdownText"
@search="searchNamespaces"
@select="setNamespace"
/>
</gl-button-group>
</template>

View File

@ -623,10 +623,12 @@ export default {
<work-item-tree
v-if="workItemType === $options.WORK_ITEM_TYPE_VALUE_OBJECTIVE"
:work-item-type="workItemType"
:parent-work-item-type="workItem.workItemType.name"
:work-item-id="workItem.id"
:children="children"
:can-update="canUpdate"
:project-path="fullPath"
:confidential="workItem.confidential"
@addWorkItemChild="addChild"
@removeChild="removeChild"
/>

View File

@ -399,6 +399,7 @@ export default {
:parent-iteration="issuableIteration"
:parent-milestone="issuableMilestone"
:form-type="formType"
:parent-work-item-type="workItem.workItemType.name"
@cancel="hideAddForm"
@addWorkItemChild="addChild"
/>

View File

@ -1,5 +1,14 @@
<script>
import { GlAlert, GlFormGroup, GlForm, GlTokenSelector, GlButton, GlFormInput } from '@gitlab/ui';
import {
GlAlert,
GlFormGroup,
GlForm,
GlTokenSelector,
GlButton,
GlFormInput,
GlFormCheckbox,
GlTooltip,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
@ -17,6 +26,8 @@ import {
I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER,
I18N_WORK_ITEM_ADD_BUTTON_LABEL,
I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL,
I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL,
I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_TOOLTIP,
sprintfWorkItem,
} from '../../constants';
@ -28,6 +39,8 @@ export default {
GlButton,
GlFormGroup,
GlFormInput,
GlFormCheckbox,
GlTooltip,
},
mixins: [glFeatureFlagMixin()],
inject: ['projectPath', 'hasIterationsFeature'],
@ -61,6 +74,11 @@ export default {
type: String,
required: true,
},
parentWorkItemType: {
type: String,
required: false,
default: '',
},
childrenType: {
type: String,
required: false,
@ -108,6 +126,7 @@ export default {
error: null,
childToCreateTitle: null,
workItemsToAdd: [],
confidential: this.parentConfidential,
};
},
computed: {
@ -119,7 +138,7 @@ export default {
hierarchyWidget: {
parentId: this.issuableGid,
},
confidential: this.parentConfidential,
confidential: this.parentConfidential || this.confidential,
};
if (this.parentMilestoneId) {
@ -162,6 +181,16 @@ export default {
}
return sprintfWorkItem(I18N_WORK_ITEM_ADD_BUTTON_LABEL, this.childrenTypeName);
},
confidentialityCheckboxLabel() {
return sprintfWorkItem(I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL, this.childrenTypeName);
},
confidentialityCheckboxTooltip() {
return sprintfWorkItem(
I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_TOOLTIP,
this.childrenTypeName,
this.parentWorkItemType,
);
},
addOrCreateMethod() {
return this.isCreateForm ? this.createChild : this.addChild;
},
@ -192,6 +221,11 @@ export default {
},
methods: {
getIdFromGraphQLId,
getConfidentialityTooltipTarget() {
// We want tooltip to be anchored to `input` within checkbox component
// but `$el.querySelector('input')` doesn't work. 🤷
return this.$refs.confidentialityCheckbox?.$el;
},
unsetError() {
this.error = null;
},
@ -299,8 +333,22 @@ export default {
autofocus
/>
</gl-form-group>
<gl-form-checkbox
ref="confidentialityCheckbox"
v-model="confidential"
name="isConfidential"
class="gl-md-mt-5 gl-mb-5 gl-md-mb-3!"
:disabled="parentConfidential"
>{{ confidentialityCheckboxLabel }}</gl-form-checkbox
>
<gl-tooltip
v-if="parentConfidential"
:target="getConfidentialityTooltipTarget"
triggers="hover"
>{{ confidentialityCheckboxTooltip }}</gl-tooltip
>
<gl-token-selector
v-else
v-if="!isCreateForm"
v-model="workItemsToAdd"
:dropdown-items="availableWorkItems"
:loading="isLoading"

View File

@ -40,10 +40,20 @@ export default {
type: String,
required: true,
},
parentWorkItemType: {
type: String,
required: false,
default: '',
},
workItemId: {
type: String,
required: true,
},
confidential: {
type: Boolean,
required: false,
default: false,
},
children: {
type: Array,
required: false,
@ -221,8 +231,10 @@ export default {
data-testid="add-tree-form"
:issuable-gid="workItemId"
:form-type="formType"
:parent-work-item-type="parentWorkItemType"
:children-type="childType"
:children-ids="childrenIds"
:parent-confidential="confidential"
@addWorkItemChild="$emit('addWorkItemChild', $event)"
@cancel="hideAddForm"
/>
@ -233,6 +245,7 @@ export default {
:can-update="canUpdate"
:issuable-gid="workItemId"
:child-item="child"
:confidential="child.confidential"
:work-item-type="workItemType"
:has-indirect-children="hasIndirectChildren"
@mouseover="prefetchWorkItem(child)"

View File

@ -73,12 +73,19 @@ export const I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL = s__('WorkItem|Add %{work
export const I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER = s__(
'WorkItem|Search existing %{workItemType}s',
);
export const I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL = s__(
'WorkItem|This %{workItemType} is confidential and should only be visible to team members with at least Reporter access',
);
export const I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_TOOLTIP = s__(
'WorkItem|A non-confidential %{workItemType} cannot be assigned to a confidential parent %{parentWorkItemType}.',
);
export const sprintfWorkItem = (msg, workItemTypeArg) => {
export const sprintfWorkItem = (msg, workItemTypeArg, parentWorkItemType = '') => {
const workItemType = workItemTypeArg || s__('WorkItem|Work item');
return capitalizeFirstCharacter(
sprintf(msg, {
workItemType: workItemType.toLocaleLowerCase(),
parentWorkItemType: parentWorkItemType.toLocaleLowerCase(),
}),
);
};

View File

@ -3,6 +3,7 @@ query workItemLinksQuery($id: WorkItemID!) {
id
workItemType {
id
name
}
title
userPermissions {

View File

@ -255,6 +255,12 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
}
}
.gl-md-mb-3\! {
@include gl-media-breakpoint-up(md) {
margin-bottom: $gl-spacing-scale-3 !important;
}
}
.gl-gap-2 {
gap: $gl-spacing-scale-2;

View File

@ -16,8 +16,7 @@ module Groups
private
def verify_usage_quotas_enabled!
render_404 unless Feature.enabled?(:usage_quotas_for_all_editions, group)
render_404 if group.has_parent?
render_404 unless group.usage_quotas_enabled?
end
# To be overriden in ee/app/controllers/ee/groups/usage_quotas_controller.rb

View File

@ -915,6 +915,10 @@ class Group < Namespace
feature_flag_enabled_for_self_or_ancestor?(:work_items_create_from_markdown)
end
def usage_quotas_enabled?
::Feature.enabled?(:usage_quotas_for_all_editions, self) && root?
end
# Check for enabled features, similar to `Project#feature_available?`
# NOTE: We still want to keep this after removing `Namespace#feature_available?`.
override :feature_available?

View File

@ -0,0 +1,10 @@
---
table_name: elastic_group_index_statuses
classes:
- Elastic::GroupIndexStatus
feature_categories:
- global_search
description: Table for tracking Advanced Search indexing statuses for groups
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107579
milestone: '15.8'
gitlab_schema: gitlab_main

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class CreateElasticGroupIndexStatuses < Gitlab::Database::Migration[2.1]
def change
create_table :elastic_group_index_statuses, id: false do |t|
t.references :namespace,
primary_key: true,
foreign_key: { on_delete: :cascade },
index: false,
default: nil
t.timestamps_with_timezone null: false
t.datetime_with_timezone :wiki_indexed_at
t.binary :last_wiki_commit
end
end
end

View File

@ -0,0 +1 @@
b528d26acaf408f6d787542626bc8d86520b1058dde20596f7da63c1e5b87aee

View File

@ -15138,6 +15138,14 @@ CREATE SEQUENCE draft_notes_id_seq
ALTER SEQUENCE draft_notes_id_seq OWNED BY draft_notes.id;
CREATE TABLE elastic_group_index_statuses (
namespace_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
wiki_indexed_at timestamp with time zone,
last_wiki_commit bytea
);
CREATE TABLE elastic_index_settings (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@ -25959,6 +25967,9 @@ ALTER TABLE ONLY dora_daily_metrics
ALTER TABLE ONLY draft_notes
ADD CONSTRAINT draft_notes_pkey PRIMARY KEY (id);
ALTER TABLE ONLY elastic_group_index_statuses
ADD CONSTRAINT elastic_group_index_statuses_pkey PRIMARY KEY (namespace_id);
ALTER TABLE ONLY elastic_index_settings
ADD CONSTRAINT elastic_index_settings_pkey PRIMARY KEY (id);
@ -34591,6 +34602,9 @@ ALTER TABLE ONLY project_repository_storage_moves
ALTER TABLE ONLY ml_candidate_metadata
ADD CONSTRAINT fk_rails_5117dddf22 FOREIGN KEY (candidate_id) REFERENCES ml_candidates(id) ON DELETE CASCADE;
ALTER TABLE ONLY elastic_group_index_statuses
ADD CONSTRAINT fk_rails_52b9969b12 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY bulk_import_configurations
ADD CONSTRAINT fk_rails_536b96bff1 FOREIGN KEY (bulk_import_id) REFERENCES bulk_imports(id) ON DELETE CASCADE;

View File

@ -673,10 +673,10 @@ Using PromQL directly could be a steep learning curve for users. It would be rea
The following section enlists how we intend to implement the aforementioned proposal around building Metrics support into GitLab Observability Service. Each corresponding document and/or issue contains further details of how each next step is planned to be executed.
- **[DONE]** [Research & draft design proposal and/or requirements](https://docs.google.com/document/d/1kHyIoWEcs14sh3CGfKGiI8QbCsdfIHeYkzVstenpsdE/edit?usp=sharing)
- **[IN-PROGRESS]** [Submit system/schema designs (proposal) & gather feedback](https://docs.google.com/document/d/1kHyIoWEcs14sh3CGfKGiI8QbCsdfIHeYkzVstenpsdE/edit?usp=sharing)
- **[IN-PROGRESS]** [Develop table definitions and/or storage interfaces](https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/1666)
- **[IN-PROGRESS]** [Prototype reference implementation, instrument key metrics](https://gitlab.com/gitlab-org/opstrace/opstrace/-/merge_requests/1823)
- **DONE** [Research & draft design proposal and/or requirements](https://docs.google.com/document/d/1kHyIoWEcs14sh3CGfKGiI8QbCsdfIHeYkzVstenpsdE/edit?usp=sharing)
- **IN-PROGRESS** [Submit system/schema designs (proposal) & gather feedback](https://docs.google.com/document/d/1kHyIoWEcs14sh3CGfKGiI8QbCsdfIHeYkzVstenpsdE/edit?usp=sharing)
- **IN-PROGRESS** [Develop table definitions and/or storage interfaces](https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/1666)
- **IN-PROGRESS** [Prototype reference implementation, instrument key metrics](https://gitlab.com/gitlab-org/opstrace/opstrace/-/merge_requests/1823)
- [Benchmark Clickhouse and/or proposed schemas, gather expert advice from Clickhouse Inc.](https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/1666)
- Develop write path(s) - `remote_write` API
- Develop read path(s) - `remote_read` API, `PromQL`-based querier.

View File

@ -562,7 +562,7 @@ WARNING:
[very specific cases](https://about.gitlab.com/handbook/engineering/workflow/#criteria-for-merging-during-broken-master).
For other cases, follow these [handbook instructions](https://about.gitlab.com/handbook/engineering/workflow/#merging-during-broken-master).
- If the latest pipeline was created before the merge request was approved, start a new pipeline to ensure that full RSpec suite has been run. You may skip this step only if the merge request does not contain any backend change.
- If the **latest [merged results pipeline](../ci/pipelines/merged_results_pipelines.md)** finished less than 2 hours ago, you
- If the **latest [merged results pipeline](../ci/pipelines/merged_results_pipelines.md)** was **created less than 6 hours ago**, and **finished less than 2 hours ago**, you
may merge without starting a new pipeline as the merge request is close
enough to `main`.
- When you set the MR to "Merge When Pipeline Succeeds", you should take over

View File

@ -206,6 +206,6 @@ RSpec.describe Utils, feature_category: :not_owned do
### Tooling feature category
For Engineering Productivity internal tooling we use `feature_category: tooling`.
For Engineering Productivity internal tooling we use `feature_category: :tooling`.
For example in [`spec/tooling/danger/specs_spec.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/tooling/danger/specs_spec.rb#L12).

View File

@ -87,7 +87,7 @@ After this work completes, we must migrate data as described in
### Phase 2
- [Phase 2 epic](https://gitlab.com/groups/gitlab-org/-/epics/6768).
- **Goal**: Make `ProjectNamespace` the front entity to interact with instead of `Project`.
- **Goal**: Link `ProjectNamespace` to other entities on the database level.
In this phase:
@ -97,6 +97,10 @@ In this phase:
- Raise awareness to avoid regressions, and conflicting or duplicate work that
can be dealt with before phase 3.
### Phase 3
- [Phase 3 epic](https://gitlab.com/groups/gitlab-org/-/epics/6585).
- **Goal**: Achieve feature parity between the namespace types.
Problems to solve as part of this phase:
- Routes handling through `ProjectNamespace` rather than `Project`.
@ -105,14 +109,49 @@ Problems to solve as part of this phase:
- Import and export.
- Other interactions between project namespace and project models.
### Phase 3
- [Phase 3 epic](https://gitlab.com/groups/gitlab-org/-/epics/6585).
- **Goal**: Feature parity between the namespace types.
Phase 3 is when the active migration of features from `Project` to `ProjectNamespace`,
or directly to `Namespace`, happens.
### How to plan features that interact with Group and ProjectNamespace
As of now, every Project in the system has a record in the `namespaces` table. This makes it possible to
use common interface to create features that are shared between Groups and Projects. Shared behavior can be added using
a concerns mechanism. Because the `Namespace` model is responsible for `UserNamespace` methods as well, it is discouraged
to use the `Namespace` model for shared behavior for Projects and Groups.
#### Resource-based features
To migrate resource-based features, existing functionality will need to be supported. This can be achieved in two Phases.
**Phase 1 - Setup**
- Link into the namespaces table
- Add a column to the table
- For example, in issues a `project id` points to the projects table. We need to establish a link to the `namespaces` table.
- Modify code so that any new record already has the correct data in it
- Backfill
**Phase 2 - Prerequisite work**
- Investigate the permission model as well as any performance concerns related to that.
- Permissions need to be checked and kept in place.
- Investigate what other models need to support namespaces for functionality dependent on features you migrate in Phase 1.
- Adjust CRUD services and APIs (REST and GraphQL) to point to the new column you added in Phase 1.
- Consider performance when fetching resources.
Introducing new functionality is very much dependent on every single team and feature.
#### Settings-related features
Right now, cascading settings are available for `NamespaceSettings`. By creating `ProjectNamespace`,
we can use this framework to make sure that some settings are applicable on the project level as well.
When working on settings, we need to make sure that:
- They are not used in `join` queries or modify those queries.
- Updating settings is taken into consideration.
- If we want to move from project to project namespace, we follow a similar database process to the one described in [Phase 1](#phase-1).
## Related topics
- [Consolidating groups and projects](../../architecture/blueprints/consolidating_groups_and_projects/index.md)

View File

@ -84,9 +84,9 @@ Here is an example of a policy which enables operational container scanning with
The keys for a schedule rule are:
- cadence (required): a [CRON expression](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm) for when the scans will be run
- agents:<agent-name> (required): The name of the agent to use for scanning
- agents:<agent-name>:namespaces (optional): The Kubernetes namespaces to scan. If omitted, all namespaces will be scanned
- `cadence` (required): a [CRON expression](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm) for when the scans will be run
- `agents:<agent-name>` (required): The name of the agent to use for scanning
- `agents:<agent-name>:namespaces` (optional): The Kubernetes namespaces to scan. If omitted, all namespaces will be scanned
NOTE:
Other elements of the [CRON syntax](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm) may work in the cadence field if supported by the [cron](https://github.com/robfig/cron) we are using in our implementation, however, GitLab does not officially test or support them.

View File

@ -15,6 +15,7 @@ module Sidebars
add_item(ci_cd_menu_item)
add_item(applications_menu_item)
add_item(packages_and_registries_menu_item)
add_item(usage_quotas_menu_item)
return true
elsif Gitlab.ee? && can?(context.current_user, :change_push_rules, context.group)
# Push Rules are the only group setting that can also be edited by maintainers.
@ -115,6 +116,22 @@ module Sidebars
)
end
def usage_quotas_menu_item
return ::Sidebars::NilMenuItem.new(item_id: :usage_quotas) unless usage_quotas_menu_enabled?
::Sidebars::MenuItem.new(
title: s_('UsageQuota|Usage Quotas'),
link: group_usage_quotas_path(context.group),
active_routes: { path: 'usage_quotas#index' },
item_id: :usage_quotas
)
end
# overriden in ee/lib/ee/sidebars/groups/menus/settings_menu.rb
def usage_quotas_menu_enabled?
context.group.usage_quotas_enabled?
end
def packages_and_registries_menu_item
unless context.group.packages_feature_enabled?
return ::Sidebars::NilMenuItem.new(item_id: :packages_and_registries)

View File

@ -47081,6 +47081,9 @@ msgstr ""
msgid "WorkItem|%{workItemType} deleted"
msgstr ""
msgid "WorkItem|A non-confidential %{workItemType} cannot be assigned to a confidential parent %{parentWorkItemType}."
msgstr ""
msgid "WorkItem|Activity"
msgstr ""
@ -47296,6 +47299,9 @@ msgstr ""
msgid "WorkItem|Test case"
msgstr ""
msgid "WorkItem|This %{workItemType} is confidential and should only be visible to team members with at least Reporter access"
msgstr ""
msgid "WorkItem|This objective is confidential and should only be visible to team members with at least Reporter access"
msgstr ""

View File

@ -25,6 +25,12 @@ module QA
find('span.gl-dropdown-button-text').text
end
def all_items
raise NotImplementedError if use_select2?
find_all("li.gl-dropdown-item").map(&:text)
end
def clear_current_selection_if_present
return super if use_select2?

View File

@ -5,6 +5,8 @@ module QA
module Project
module Fork
class New < Page::Base
include ::QA::Page::Component::Dropdown
view 'app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue' do
element :fork_project_button
element :fork_privacy_button
@ -12,9 +14,6 @@ module QA
view 'app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue' do
element :select_namespace_dropdown
element :select_namespace_dropdown_item
element :select_namespace_dropdown_search_field
element :select_namespace_dropdown_item
end
def fork_project(namespace = Runtime::Namespace.path)
@ -25,20 +24,13 @@ module QA
def get_list_of_namespaces
click_element(:select_namespace_dropdown)
wait_until(reload: false) do
has_element?(:select_namespace_dropdown_item)
end
all_elements(:select_namespace_dropdown_item, minimum: 1).map(&:text)
all_items
end
def choose_namespace(namespace)
retry_on_exception do
click_element(:select_namespace_dropdown)
fill_element(:select_namespace_dropdown_search_field, namespace)
wait_until(reload: false) do
has_element?(:select_namespace_dropdown_item, text: namespace)
end
click_button(namespace)
search_and_select(namespace)
end
end
end

View File

@ -15,7 +15,8 @@ module QA
let!(:gitlab_source_project) { ENV["QA_LARGE_IMPORT_REPO"] || "migration-test-project" }
let!(:import_wait_duration) { { max_duration: (ENV["QA_LARGE_IMPORT_DURATION"] || 3600).to_i, sleep_interval: 30 } }
let!(:source_admin_user) { "no-op" }
# test uses production as source which doesn't have actual admin user
let!(:source_admin_user) { nil }
let!(:source_admin_api_client) do
Runtime::API::Client.new(
source_gitlab_address,
@ -227,8 +228,8 @@ module QA
comment_diff = verify_comments(type, actual, expected)
{
"missing_#{type}s": (expected.keys - actual.keys).map { |it| expected[it]&.slice(:title, :url) }.compact,
"extra_#{type}s": (actual.keys - expected.keys).map { |it| actual[it]&.slice(:title, :url) }.compact,
"missing_#{type}s": (expected.keys - actual.keys).filter_map { |it| expected[it]&.slice(:title, :url) },
"extra_#{type}s": (actual.keys - expected.keys).filter_map { |it| actual[it]&.slice(:title, :url) },
"#{type}_comments": comment_diff
}
end

View File

@ -93,7 +93,7 @@ module QA
end
before do
enable_bulk_import(source_admin_api_client) unless source_bulk_import_enabled
enable_bulk_import(source_admin_api_client) if source_admin_user && !source_bulk_import_enabled
enable_bulk_import(admin_api_client) unless target_bulk_import_enabled
target_sandbox.add_member(user, Resource::Members::AccessLevel::OWNER)

View File

@ -11,7 +11,7 @@ Color_Off='\033[0m' # Text Reset
function onexit_err() {
local exit_status=${1:-$?}
printf "\n❌❌❌ ${BRed}GLFM snapshot tests failed!${Color_Off} ❌❌❌\n"
printf "\n❌❌❌ ${BRed}GLFM spec tests failed!${Color_Off} ❌❌❌\n"
exit "${exit_status}"
}
trap onexit_err ERR

View File

@ -137,9 +137,9 @@ RSpec.describe 'Project fork', feature_category: :projects do
let(:user) { create(:group_member, :maintainer, user: create(:user), group: group).user }
def submit_form(group_obj = group)
find('[data-testid="select_namespace_dropdown"]').click
find('[data-testid="select_namespace_dropdown_search_field"]').fill_in(with: group_obj.name)
click_button group_obj.name
click_button(s_('ForkProject|Select a namespace'), disabled: false)
find('[data-testid="listbox-search-input"]').fill_in(with: group_obj.name)
find('.gl-dropdown-item-text-wrapper', text: group_obj.name).click
click_button 'Fork project'
end

View File

@ -1,11 +1,4 @@
import {
GlButton,
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
GlSearchBoxByType,
GlTruncate,
} from '@gitlab/ui';
import { GlButton, GlListboxItem, GlCollapsibleListbox } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@ -80,17 +73,16 @@ describe('ProjectNamespace component', () => {
};
const findButtonLabel = () => wrapper.findComponent(GlButton);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownText = () => wrapper.findComponent(GlTruncate);
const findInput = () => wrapper.findComponent(GlSearchBoxByType);
const findListBox = () => wrapper.findComponent(GlCollapsibleListbox);
const findListBoxText = () => findListBox().props('toggleText');
const clickDropdownItem = async () => {
wrapper.findComponent(GlDropdownItem).vm.$emit('click');
const clickListBoxItem = async (value = '') => {
wrapper.findComponent(GlListboxItem).vm.$emit('select', value);
await nextTick();
};
const showDropdown = () => {
findDropdown().vm.$emit('shown');
findListBox().vm.$emit('shown');
};
beforeAll(() => {
@ -115,7 +107,7 @@ describe('ProjectNamespace component', () => {
});
it('renders placeholder text', () => {
expect(findDropdownText().props('text')).toBe('Select a namespace');
expect(findListBoxText()).toBe('Select a namespace');
});
});
@ -127,24 +119,18 @@ describe('ProjectNamespace component', () => {
showDropdown();
});
it('focuses on the input when the dropdown is opened', () => {
const spy = jest.spyOn(findInput().vm, 'focusInput');
showDropdown();
expect(spy).toHaveBeenCalledTimes(1);
});
it('displays fetched namespaces', () => {
const listItems = wrapper.findAll('li');
expect(listItems).toHaveLength(3);
expect(listItems.at(0).findComponent(GlDropdownSectionHeader).text()).toBe('Namespaces');
expect(listItems.at(1).text()).toBe(data.project.forkTargets.nodes[0].fullPath);
expect(listItems.at(2).text()).toBe(data.project.forkTargets.nodes[1].fullPath);
expect(listItems).toHaveLength(2);
expect(listItems.at(0).text()).toBe(data.project.forkTargets.nodes[0].fullPath);
expect(listItems.at(1).text()).toBe(data.project.forkTargets.nodes[1].fullPath);
});
it('sets the selected namespace', async () => {
const { fullPath } = data.project.forkTargets.nodes[0];
await clickDropdownItem();
expect(findDropdownText().props('text')).toBe(fullPath);
await clickListBoxItem(fullPath);
expect(findListBoxText()).toBe(fullPath);
});
});
@ -155,7 +141,7 @@ describe('ProjectNamespace component', () => {
});
it('renders `No matches found`', () => {
expect(wrapper.find('li').text()).toBe('No matches found');
expect(findListBox().text()).toContain('No matches found');
});
});

View File

@ -655,12 +655,17 @@ describe('WorkItemDetail component', () => {
it('renders children tree when work item is an Objective', async () => {
const objectiveWorkItem = workItemResponseFactory({
workItemType: objectiveType,
confidential: true,
});
const handler = jest.fn().mockResolvedValue(objectiveWorkItem);
createComponent({ handler });
await waitForPromises();
expect(findHierarchyTree().exists()).toBe(true);
expect(findHierarchyTree().props()).toMatchObject({
parentWorkItemType: objectiveType.name,
confidential: objectiveWorkItem.data.workItem.confidential,
});
});
});
});

View File

@ -1,11 +1,18 @@
import Vue from 'vue';
import { GlForm, GlFormInput, GlTokenSelector } from '@gitlab/ui';
import { GlForm, GlFormInput, GlFormCheckbox, GlTooltip, GlTokenSelector } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { sprintf } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
import { FORM_TYPES } from '~/work_items/constants';
import {
FORM_TYPES,
WORK_ITEM_TYPE_ENUM_TASK,
WORK_ITEM_TYPE_VALUE_ISSUE,
I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL,
I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_TOOLTIP,
} from '~/work_items/constants';
import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
@ -36,6 +43,8 @@ describe('WorkItemLinksForm', () => {
workItemsMvcEnabled = false,
parentIteration = null,
formType = FORM_TYPES.create,
parentWorkItemType = WORK_ITEM_TYPE_VALUE_ISSUE,
childrenType = WORK_ITEM_TYPE_ENUM_TASK,
} = {}) => {
wrapper = shallowMountExtended(WorkItemLinksForm, {
apolloProvider: createMockApollo([
@ -48,6 +57,8 @@ describe('WorkItemLinksForm', () => {
issuableGid: 'gid://gitlab/WorkItem/1',
parentConfidential,
parentIteration,
parentWorkItemType,
childrenType,
formType,
},
provide: {
@ -65,6 +76,7 @@ describe('WorkItemLinksForm', () => {
const findForm = () => wrapper.findComponent(GlForm);
const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
const findInput = () => wrapper.findComponent(GlFormInput);
const findConfidentialCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findAddChildButton = () => wrapper.findByTestId('add-child-button');
afterEach(() => {
@ -124,6 +136,37 @@ describe('WorkItemLinksForm', () => {
},
});
});
describe('confidentiality checkbox', () => {
it('renders confidentiality checkbox', () => {
const confidentialCheckbox = findConfidentialCheckbox();
expect(confidentialCheckbox.exists()).toBe(true);
expect(wrapper.findComponent(GlTooltip).exists()).toBe(false);
expect(confidentialCheckbox.text()).toBe(
sprintf(I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL, {
workItemType: WORK_ITEM_TYPE_ENUM_TASK.toLocaleLowerCase(),
}),
);
});
it('renders confidentiality tooltip with checkbox checked and disabled when parent is confidential', () => {
createComponent({ parentConfidential: true });
const confidentialCheckbox = findConfidentialCheckbox();
const confidentialTooltip = wrapper.findComponent(GlTooltip);
expect(confidentialCheckbox.attributes('disabled')).toBe('true');
expect(confidentialCheckbox.attributes('checked')).toBe('true');
expect(confidentialTooltip.exists()).toBe(true);
expect(confidentialTooltip.text()).toBe(
sprintf(I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_TOOLTIP, {
workItemType: WORK_ITEM_TYPE_ENUM_TASK.toLocaleLowerCase(),
parentWorkItemType: WORK_ITEM_TYPE_VALUE_ISSUE.toLocaleLowerCase(),
}),
);
});
});
});
describe('adding an existing work item', () => {

View File

@ -34,6 +34,8 @@ describe('WorkItemTree', () => {
const createComponent = ({
workItemType = 'Objective',
parentWorkItemType = 'Objective',
confidential = false,
children = childrenWorkItems,
apolloProvider = null,
} = {}) => {
@ -55,7 +57,9 @@ describe('WorkItemTree', () => {
apolloProvider || createMockApollo([[workItemQuery, getWorkItemQueryHandler]]),
propsData: {
workItemType,
parentWorkItemType,
workItemId: 'gid://gitlab/WorkItem/515',
confidential,
children,
projectPath: 'test/project',
},
@ -90,7 +94,11 @@ describe('WorkItemTree', () => {
});
it('renders all hierarchy widget children', () => {
expect(findWorkItemLinkChildItems()).toHaveLength(4);
const workItemLinkChildren = findWorkItemLinkChildItems();
expect(workItemLinkChildren).toHaveLength(4);
expect(workItemLinkChildren.at(0).props().childItem.confidential).toBe(
childrenWorkItems[0].confidential,
);
});
it('does not display form by default', () => {
@ -110,8 +118,12 @@ describe('WorkItemTree', () => {
await nextTick();
expect(findForm().exists()).toBe(true);
expect(findForm().props('formType')).toBe(formType);
expect(findForm().props('childrenType')).toBe(childType);
expect(findForm().props()).toMatchObject({
formType,
childrenType: childType,
parentWorkItemType: 'Objective',
parentConfidential: false,
});
},
);

View File

@ -3560,4 +3560,26 @@ RSpec.describe Group do
it { is_expected.to be_nil }
end
end
describe '#usage_quotas_enabled?', feature_category: :subscription_cost_management, unless: Gitlab.ee? do
using RSpec::Parameterized::TableSyntax
where(:feature_enabled, :root_group, :result) do
false | true | false
false | false | false
true | false | false
true | true | true
end
with_them do
before do
stub_feature_flags(usage_quotas_for_all_editions: feature_enabled)
allow(group).to receive(:root?).and_return(root_group)
end
it 'returns the expected result' do
expect(group.usage_quotas_enabled?).to eq result
end
end
end
end