Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-01-23 06:08:33 +00:00
parent b2ee478d3e
commit 9cc33a92d0
11 changed files with 275 additions and 60 deletions

View File

@ -1,5 +1,11 @@
<script>
import { GlCollapsibleListbox } from '@gitlab/ui';
import {
GlDropdown,
GlSearchBoxByType,
GlDropdownItem,
GlDropdownText,
GlLoadingIcon,
} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import {
I18N_NO_RESULTS_MESSAGE,
@ -10,7 +16,11 @@ import {
export default {
name: 'BranchesDropdown',
components: {
GlCollapsibleListbox,
GlDropdown,
GlSearchBoxByType,
GlDropdownItem,
GlDropdownText,
GlLoadingIcon,
},
props: {
value: {
@ -36,16 +46,13 @@ export default {
},
computed: {
...mapGetters(['joinedBranches']),
...mapState(['isFetching']),
...mapState(['isFetching', 'branch', 'branches']),
filteredResults() {
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
return this.joinedBranches.filter((resultString) =>
resultString.toLowerCase().includes(lowerCasedSearchTerm),
);
},
listboxItems() {
return this.filteredResults.map((value) => ({ value, text: value }));
},
},
watch: {
// Parent component can set the branch value (e.g. when the user selects a different project)
@ -61,6 +68,10 @@ export default {
...mapActions(['fetchBranches']),
selectBranch(branch) {
this.$emit('selectBranch', branch);
this.searchTerm = branch; // enables isSelected to work as expected
},
isSelected(selectedBranch) {
return selectedBranch === this.branch;
},
searchTermChanged(value) {
this.searchTerm = value;
@ -70,16 +81,36 @@ export default {
};
</script>
<template>
<gl-collapsible-listbox
:header-text="$options.i18n.branchHeaderTitle"
:toggle-text="value"
:items="listboxItems"
searchable
:search-placeholder="$options.i18n.branchSearchPlaceholder"
:searching="isFetching"
:selected="value"
:no-results-text="$options.i18n.noResultsMessage"
@search="searchTermChanged"
@select="selectBranch"
/>
<gl-dropdown :text="value" :header-text="$options.i18n.branchHeaderTitle">
<gl-search-box-by-type
:value="searchTerm"
trim
autocomplete="off"
:debounce="250"
:placeholder="$options.i18n.branchSearchPlaceholder"
data-testid="dropdown-search-box"
@input="searchTermChanged"
/>
<gl-dropdown-item
v-for="branch in filteredResults"
v-show="!isFetching"
:key="branch"
:name="branch"
:is-checked="isSelected(branch)"
is-check-item
data-testid="dropdown-item"
@click="selectBranch(branch)"
>
{{ branch }}
</gl-dropdown-item>
<gl-dropdown-text v-show="isFetching" data-testid="dropdown-text-loading-icon">
<gl-loading-icon size="sm" class="gl-mx-auto" />
</gl-dropdown-text>
<gl-dropdown-text
v-if="!filteredResults.length && !isFetching"
data-testid="empty-result-message"
>
<span class="gl-text-gray-500">{{ $options.i18n.noResultsMessage }}</span>
</gl-dropdown-text>
</gl-dropdown>
</template>

View File

@ -141,7 +141,11 @@ export default {
:value="targetProjectId"
/>
<projects-dropdown :value="targetProjectName" @selectProject="setSelectedProject" />
<projects-dropdown
class="gl-w-half"
:value="targetProjectName"
@selectProject="setSelectedProject"
/>
</gl-form-group>
<gl-form-group
@ -151,7 +155,12 @@ export default {
>
<input id="start_branch" type="hidden" name="start_branch" :value="branch" />
<branches-dropdown :value="branch" :blanked="isRevert" @selectBranch="setBranch" />
<branches-dropdown
class="gl-w-half"
:value="branch"
:blanked="isRevert"
@selectBranch="setBranch"
/>
</gl-form-group>
<gl-form-checkbox

View File

@ -1,5 +1,5 @@
<script>
import { GlCollapsibleListbox } from '@gitlab/ui';
import { GlDropdown, GlSearchBoxByType, GlDropdownItem, GlDropdownText } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
import {
I18N_NO_RESULTS_MESSAGE,
@ -10,7 +10,10 @@ import {
export default {
name: 'ProjectsDropdown',
components: {
GlCollapsibleListbox,
GlDropdown,
GlSearchBoxByType,
GlDropdownItem,
GlDropdownText,
},
props: {
value: {
@ -38,20 +41,17 @@ export default {
project.name.toLowerCase().includes(lowerCasedFilterTerm),
);
},
listboxItems() {
return this.filteredResults.map(({ id, name }) => ({ value: id, text: name }));
},
selectedProject() {
return this.sortedProjects.find((project) => project.id === this.targetProjectId) || {};
},
},
methods: {
selectProject(value) {
this.$emit('selectProject', value);
// when we select a project, we want the dropdown to filter to the selected project
const project = this.listboxItems.find((x) => x.value === value);
this.filterTerm = project?.text || '';
selectProject(project) {
this.$emit('selectProject', project.id);
this.filterTerm = project.name; // when we select a project, we want the dropdown to filter to the selected project
},
isSelected(selectedProject) {
return selectedProject === this.selectedProject;
},
filterTermChanged(value) {
this.filterTerm = value;
@ -60,15 +60,28 @@ export default {
};
</script>
<template>
<gl-collapsible-listbox
:header-text="$options.i18n.projectHeaderTitle"
:items="listboxItems"
searchable
:search-placeholder="$options.i18n.projectSearchPlaceholder"
:selected="selectedProject.id"
:toggle-text="selectedProject.name"
:no-results-text="$options.i18n.noResultsMessage"
@search="filterTermChanged"
@select="selectProject"
/>
<gl-dropdown :text="selectedProject.name" :header-text="$options.i18n.projectHeaderTitle">
<gl-search-box-by-type
:value="filterTerm"
trim
autocomplete="off"
:placeholder="$options.i18n.projectSearchPlaceholder"
data-testid="dropdown-search-box"
@input="filterTermChanged"
/>
<gl-dropdown-item
v-for="project in filteredResults"
:key="project.name"
:name="project.name"
:is-checked="isSelected(project)"
is-check-item
data-testid="dropdown-item"
@click="selectProject(project)"
>
{{ project.name }}
</gl-dropdown-item>
<gl-dropdown-text v-if="!filteredResults.length" data-testid="empty-result-message">
<span class="gl-text-gray-500">{{ $options.i18n.noResultsMessage }}</span>
</gl-dropdown-text>
</gl-dropdown>
</template>

View File

@ -122,9 +122,9 @@ for details.
- [Viewing projects and designs data from a primary site is not possible when using a unified URL](../index.md#view-replication-data-on-the-primary-site).
- When secondary proxying is used together with separate URLs, registering [GitLab runners](https://docs.gitlab.com/runner/) to clone from
secondary sites is not supported. The runner registration will succeed, but the clone URL will default to the primary site. The runner
secondary sites is not supported. The runner registration succeeds, but the clone URL defaults to the primary site. The runner
[clone URL](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section) is configured per GitLab deployment
and cannot be configured per Geo site. Therefore, all runners will clone from the primary site (or configured clone URL) irrespective of
and cannot be configured per Geo site. Therefore, all runners clone from the primary site (or configured clone URL) irrespective of
which Geo site they register on. For information about GitLab CI using a specific Geo secondary to clone from, see issue
[3294](https://gitlab.com/gitlab-org/gitlab/-/issues/3294#note_1009488466).
@ -147,7 +147,7 @@ secondary Geo sites are able to support write requests. Certain **read** request
sites for improved latency and bandwidth nearby. All write requests are proxied to the primary site.
The following table details the components currently tested through the Geo secondary site Workhorse proxy.
It does not cover all data types, more will be added in the future as they are tested.
It does not cover all data types.
| Feature / component | Accelerated reads? |
|:----------------------------------------------------|:-----------------------|

View File

@ -5,7 +5,7 @@ info: "To determine the technical writer assigned to the Stage/Group associated
description: 'Learn how to install, configure, update, and maintain your GitLab instance.'
---
# Administrator documentation **(FREE SELF)**
# Administer GitLab **(FREE SELF)**
If you use GitLab.com, only GitLab team members have access to administration tools and settings.
If you use a self-managed GitLab instance, learn how to administer it.

View File

@ -4,7 +4,7 @@ group: Integrations
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# API Docs **(FREE)**
# GitLab APIs **(FREE)**
Use the GitLab APIs to automate GitLab.

View File

@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
comments: false
---
# GitLab integrations **(FREE)**
# Integrate with GitLab **(FREE)**
You can integrate GitLab with external services for enhanced functionality.

View File

@ -360,13 +360,14 @@ including a large number of false positives.
|:------------------------------------------------|:--------------|:------------------------------|
| `DAST_ADVERTISE_SCAN` | boolean | Set to `true` to add a `Via` header to every request sent, advertising that the request was sent as part of a GitLab DAST scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/334947) in GitLab 14.1. |
| `DAST_AGGREGATE_VULNERABILITIES` | boolean | Vulnerability aggregation is set to `true` by default. To disable this feature and see each vulnerability individually set to `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/254043) in GitLab 14.0. |
| `DAST_ALLOWED_HOSTS` | Comma-separated list of strings | Hostnames included in this variable are considered in scope when crawled. By default the `DAST_WEBSITE` hostname is included in the allowed hosts list. Headers set using `DAST_REQUEST_HEADERS` are added to every request made to these hostnames. Example, `site.com,another.com`. |
| `DAST_API_HOST_OVERRIDE` <sup>1</sup> | string | **{warning}** **[Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/383467)** in GitLab 15.7. Replaced by [DAST API scan](../dast_api/index.md#available-cicd-variables). Used to override domains defined in API specification files. Only supported when importing the API specification from a URL. Example: `example.com:8080`. |
| `DAST_API_SPECIFICATION` <sup>1</sup> | URL or string | **{warning}** **[Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/383467)** in GitLab 15.7. Replaced by [DAST API scan](../dast_api/index.md#available-cicd-variables). The API specification to import. The specification can be hosted at a URL, or the name of a file present in the `/zap/wrk` directory. The variable `DAST_WEBSITE` must be specified if this is omitted. |
| `DAST_AUTH_EXCLUDE_URLS` | URLs | **{warning}** **[Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/289959)** in GitLab 14.0. Replaced by `DAST_EXCLUDE_URLS`. The URLs to skip during the authenticated scan; comma-separated. Regular expression syntax can be used to match multiple URLs. For example, `.*` matches an arbitrary character sequence. |
| `DAST_AUTO_UPDATE_ADDONS` | boolean | ZAP add-ons are pinned to specific versions in the DAST Docker image. Set to `true` to download the latest versions when the scan starts. Default: `false`. |
| `DAST_DEBUG` <sup>1</sup> | boolean | Enable debug message output. Default: `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |
| `DAST_EXCLUDE_RULES` | string | Set to a comma-separated list of Vulnerability Rule IDs to exclude them from running during the scan. Rule IDs are numbers and can be found from the DAST log or on the [ZAP project](https://www.zaproxy.org/docs/alerts/). For example, `HTTP Parameter Override` has a rule ID of `10026`. Cannot be used when `DAST_ONLY_INCLUDE_RULES` is set. **Note:** In earlier versions of GitLab the excluded rules were executed but vulnerabilities they generated were suppressed. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118641) in GitLab 12.10. |
| `DAST_EXCLUDE_URLS` <sup>1</sup> | URLs | The URLs to skip during the authenticated scan; comma-separated. Regular expression syntax can be used to match multiple URLs. For example, `.*` matches an arbitrary character sequence. Example, `http://example.com/sign-out`. |
| `DAST_EXCLUDE_URLS` <sup>1</sup> | URLs | The URLs to skip during the authenticated scan; comma-separated. Regular expression syntax can be used to match multiple URLs. For example, `.*` matches an arbitrary character sequence. Example, `http://example.com/sign-out`. |
| `DAST_FULL_SCAN_DOMAIN_VALIDATION_REQUIRED` | boolean | **{warning}** **[Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/293595)** in GitLab 14.0. Set to `true` to require domain validation when running DAST full scans. Default: `false` |
| `DAST_FULL_SCAN_ENABLED` <sup>1</sup> | boolean | Set to `true` to run a [ZAP Full Scan](https://github.com/zaproxy/zaproxy/wiki/ZAP-Full-Scan) instead of a [ZAP Baseline Scan](https://github.com/zaproxy/zaproxy/wiki/ZAP-Baseline-Scan). Default: `false` |
| `DAST_HTML_REPORT` | string | **{warning}** **[Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/384340)** in GitLab 15.7. The filename of the HTML report written at the end of a scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |

View File

@ -78,9 +78,9 @@ RSpec.describe 'Cherry-pick Commits', :js, feature_category: :source_code_manage
end
page.within("#{modal_selector} .dropdown-menu") do
fill_in 'Search branches', with: 'feature'
find('[data-testid="dropdown-search-box"]').set('feature')
wait_for_requests
find('.gl-dropdown-item-text-wrapper', exact_text: 'feature').click
click_button 'feature'
end
submit_cherry_pick

View File

@ -1,8 +1,9 @@
import { GlCollapsibleListbox } from '@gitlab/ui';
import { GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import BranchesDropdown from '~/projects/commit/components/branches_dropdown.vue';
Vue.use(Vuex);
@ -33,7 +34,12 @@ describe('BranchesDropdown', () => {
}),
);
};
const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
const findNoResults = () => wrapper.findByTestId('empty-result-message');
const findLoading = () => wrapper.findByTestId('dropdown-text-loading-icon');
afterEach(() => {
wrapper.destroy();
@ -49,6 +55,72 @@ describe('BranchesDropdown', () => {
it('invokes fetchBranches', () => {
expect(spyFetchBranches).toHaveBeenCalled();
});
describe('with a value but visually blanked', () => {
beforeEach(() => {
createComponent({ value: '_main_', blanked: true }, { branch: '_main_' });
});
it('renders all branches', () => {
expect(findAllDropdownItems()).toHaveLength(3);
expect(findDropdownItemByIndex(0).text()).toBe('_main_');
expect(findDropdownItemByIndex(1).text()).toBe('_branch_1_');
expect(findDropdownItemByIndex(2).text()).toBe('_branch_2_');
});
it('selects the active branch', () => {
expect(wrapper.vm.isSelected('_main_')).toBe(true);
});
});
});
describe('Loading states', () => {
it('shows loading icon while fetching', () => {
createComponent({ value: '' }, { isFetching: true });
expect(findLoading().isVisible()).toBe(true);
});
it('does not show loading icon', () => {
createComponent({ value: '' });
expect(findLoading().isVisible()).toBe(false);
});
});
describe('No branches found', () => {
beforeEach(() => {
createComponent({ value: '_non_existent_branch_' });
});
it('renders empty results message', () => {
expect(findNoResults().text()).toBe('No matching results');
});
it('shows GlSearchBoxByType with default attributes', () => {
expect(findSearchBoxByType().exists()).toBe(true);
expect(findSearchBoxByType().vm.$attrs).toMatchObject({
placeholder: 'Search branches',
debounce: DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
});
});
});
describe('Search term is empty', () => {
beforeEach(() => {
createComponent({ value: '' });
});
it('renders all branches when search term is empty', () => {
expect(findAllDropdownItems()).toHaveLength(3);
expect(findDropdownItemByIndex(0).text()).toBe('_main_');
expect(findDropdownItemByIndex(1).text()).toBe('_branch_1_');
expect(findDropdownItemByIndex(2).text()).toBe('_branch_2_');
});
it('should not be selected on the inactive branch', () => {
expect(wrapper.vm.isSelected('_main_')).toBe(false);
});
});
describe('When searching', () => {
@ -59,7 +131,7 @@ describe('BranchesDropdown', () => {
it('invokes fetchBranches', async () => {
const spy = jest.spyOn(wrapper.vm, 'fetchBranches');
findDropdown().vm.$emit('search', '_anything_');
findSearchBoxByType().vm.$emit('input', '_anything_');
await nextTick();
@ -68,13 +140,46 @@ describe('BranchesDropdown', () => {
});
});
describe('Branches found', () => {
beforeEach(() => {
createComponent({ value: '_branch_1_' }, { branch: '_branch_1_' });
});
it('renders only the branch searched for', () => {
expect(findAllDropdownItems()).toHaveLength(1);
expect(findDropdownItemByIndex(0).text()).toBe('_branch_1_');
});
it('should not display empty results message', () => {
expect(findNoResults().exists()).toBe(false);
});
it('should signify this branch is selected', () => {
expect(wrapper.vm.isSelected('_branch_1_')).toBe(true);
});
it('should signify the branch is not selected', () => {
expect(wrapper.vm.isSelected('_not_selected_branch_')).toBe(false);
});
describe('Custom events', () => {
it('should emit selectBranch if an branch is clicked', () => {
findDropdownItemByIndex(0).vm.$emit('click');
expect(wrapper.emitted('selectBranch')).toEqual([['_branch_1_']]);
expect(wrapper.vm.searchTerm).toBe('_branch_1_');
});
});
});
describe('Case insensitive for search term', () => {
beforeEach(() => {
createComponent({ value: '_BrAnCh_1_' });
});
it('returns only the branch searched for', () => {
expect(findDropdown().props('items')).toEqual([{ text: '_branch_1_', value: '_branch_1_' }]);
it('renders only the branch searched for', () => {
expect(findAllDropdownItems()).toHaveLength(1);
expect(findDropdownItemByIndex(0).text()).toBe('_branch_1_');
});
});
});

View File

@ -1,4 +1,4 @@
import { GlCollapsibleListbox } from '@gitlab/ui';
import { GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
@ -35,23 +35,78 @@ describe('ProjectsDropdown', () => {
);
};
const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
const findNoResults = () => wrapper.findByTestId('empty-result-message');
afterEach(() => {
wrapper.destroy();
spyFetchProjects.mockReset();
});
describe('No projects found', () => {
beforeEach(() => {
createComponent('_non_existent_project_');
});
it('renders empty results message', () => {
expect(findNoResults().text()).toBe('No matching results');
});
it('shows GlSearchBoxByType with default attributes', () => {
expect(findSearchBoxByType().exists()).toBe(true);
expect(findSearchBoxByType().vm.$attrs).toMatchObject({
placeholder: 'Search projects',
});
});
});
describe('Search term is empty', () => {
beforeEach(() => {
createComponent('');
});
it('renders all projects when search term is empty', () => {
expect(findAllDropdownItems()).toHaveLength(3);
expect(findDropdownItemByIndex(0).text()).toBe('_project_1_');
expect(findDropdownItemByIndex(1).text()).toBe('_project_2_');
expect(findDropdownItemByIndex(2).text()).toBe('_project_3_');
});
it('should not be selected on the inactive project', () => {
expect(wrapper.vm.isSelected('_project_1_')).toBe(false);
});
});
describe('Projects found', () => {
beforeEach(() => {
createComponent('_project_1_', { targetProjectId: '1' });
});
it('renders only the project searched for', () => {
expect(findAllDropdownItems()).toHaveLength(1);
expect(findDropdownItemByIndex(0).text()).toBe('_project_1_');
});
it('should not display empty results message', () => {
expect(findNoResults().exists()).toBe(false);
});
it('should signify this project is selected', () => {
expect(findDropdownItemByIndex(0).props('isChecked')).toBe(true);
});
it('should signify the project is not selected', () => {
expect(wrapper.vm.isSelected('_not_selected_project_')).toBe(false);
});
describe('Custom events', () => {
it('should emit selectProject if a project is clicked', () => {
findDropdown().vm.$emit('select', '1');
findDropdownItemByIndex(0).vm.$emit('click');
expect(wrapper.emitted('selectProject')).toEqual([['1']]);
expect(wrapper.vm.filterTerm).toBe('_project_1_');
});
});
});
@ -62,7 +117,8 @@ describe('ProjectsDropdown', () => {
});
it('renders only the project searched for', () => {
expect(findDropdown().props('items')).toEqual([{ text: '_project_1_', value: '1' }]);
expect(findAllDropdownItems()).toHaveLength(1);
expect(findDropdownItemByIndex(0).text()).toBe('_project_1_');
});
});
});