Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d30b2ead9a
commit
20d52aa39e
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ query workItemLinksQuery($id: WorkItemID!) {
|
|||
id
|
||||
workItemType {
|
||||
id
|
||||
name
|
||||
}
|
||||
title
|
||||
userPermissions {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
b528d26acaf408f6d787542626bc8d86520b1058dde20596f7da63c1e5b87aee
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue