diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS
index 6f858f0e0a6..6019fe636a8 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS
@@ -1118,7 +1118,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/ee/lib/audit/group_push_rules_changes_auditor.rb @gitlab-org/manage/compliance
/ee/lib/ee/api/entities/audit_event.rb @gitlab-org/manage/compliance
/ee/lib/ee/audit/ @gitlab-org/manage/compliance
-/ee/lib/gitlab/audit/auditor.rb @gitlab-org/manage/compliance
+/ee/lib/ee/gitlab/audit/ @gitlab-org/manage/compliance
/ee/spec/controllers/admin/audit_log_reports_controller_spec.rb @gitlab-org/manage/compliance
/ee/spec/controllers/admin/audit_logs_controller_spec.rb @gitlab-org/manage/compliance
/ee/spec/controllers/groups/audit_events_controller_spec.rb @gitlab-org/manage/compliance
@@ -1163,13 +1163,10 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/ee/spec/support/shared_examples/features/audit_events_filter_shared_examples.rb @gitlab-org/manage/compliance
/ee/spec/support/shared_examples/services/audit_event_logging_shared_examples.rb @gitlab-org/manage/compliance
/ee/spec/workers/audit_events/audit_event_streaming_worker_spec.rb @gitlab-org/manage/compliance
+/lib/gitlab/audit/auditor.rb @gitlab-org/manage/compliance
/lib/gitlab/audit_json_logger.rb @gitlab-org/manage/compliance
-/qa/qa/ee/page/admin/monitoring/ @gitlab-org/manage/compliance
-/qa/qa/specs/features/ee/browser_ui/1_manage/group/group_audit_logs_1_spec.rb @gitlab-org/manage/compliance
-/qa/qa/specs/features/ee/browser_ui/1_manage/group/group_audit_logs_2_spec.rb @gitlab-org/manage/compliance
-/qa/qa/specs/features/ee/browser_ui/1_manage/instance/ @gitlab-org/manage/compliance
-/qa/qa/specs/features/ee/browser_ui/1_manage/project/project_audit_logs_spec.rb @gitlab-org/manage/compliance
/spec/factories/audit_events.rb @gitlab-org/manage/compliance
+/spec/lib/gitlab/audit/auditor_spec.rb @gitlab-org/manage/compliance
/spec/migrations/populate_audit_event_streaming_verification_token_spec.rb @gitlab-org/manage/compliance
/spec/models/audit_event_spec.rb @gitlab-org/manage/compliance
/spec/services/audit_event_service_spec.rb @gitlab-org/manage/compliance
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
new file mode 100644
index 00000000000..3af83ffa8ed
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql
new file mode 100644
index 00000000000..f8e4dc55fa4
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql
@@ -0,0 +1,30 @@
+#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+
+mutation addGroupVariable(
+ $variable: CiVariable!
+ $endpoint: String!
+ $fullPath: ID!
+ $groupId: ID!
+) {
+ addGroupVariable(
+ variable: $variable
+ endpoint: $endpoint
+ fullPath: $fullPath
+ groupId: $groupId
+ ) @client {
+ group {
+ id
+ ciVariables {
+ nodes {
+ ...BaseCiVariable
+ ... on CiGroupVariable {
+ environmentScope
+ masked
+ protected
+ }
+ }
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql
new file mode 100644
index 00000000000..310e4a6e551
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql
@@ -0,0 +1,30 @@
+#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+
+mutation deleteGroupVariable(
+ $variable: CiVariable!
+ $endpoint: String!
+ $fullPath: ID!
+ $groupId: ID!
+) {
+ deleteGroupVariable(
+ variable: $variable
+ endpoint: $endpoint
+ fullPath: $fullPath
+ groupId: $groupId
+ ) @client {
+ group {
+ id
+ ciVariables {
+ nodes {
+ ...BaseCiVariable
+ ... on CiGroupVariable {
+ environmentScope
+ masked
+ protected
+ }
+ }
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql
new file mode 100644
index 00000000000..5291942eb87
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql
@@ -0,0 +1,30 @@
+#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+
+mutation updateGroupVariable(
+ $variable: CiVariable!
+ $endpoint: String!
+ $fullPath: ID!
+ $groupId: ID!
+) {
+ updateGroupVariable(
+ variable: $variable
+ endpoint: $endpoint
+ fullPath: $fullPath
+ groupId: $groupId
+ ) @client {
+ group {
+ id
+ ciVariables {
+ nodes {
+ ...BaseCiVariable
+ ... on CiGroupVariable {
+ environmentScope
+ masked
+ protected
+ }
+ }
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql b/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql
new file mode 100644
index 00000000000..c6dd6d4faaf
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql
@@ -0,0 +1,17 @@
+#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+
+query getGroupVariables($fullPath: ID!) {
+ group(fullPath: $fullPath) {
+ id
+ ciVariables {
+ nodes {
+ ...BaseCiVariable
+ ... on CiGroupVariable {
+ environmentScope
+ masked
+ protected
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/resolvers.js b/app/assets/javascripts/ci_variable_list/graphql/resolvers.js
index 7b57e97a4b8..be7e3f88cfd 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/resolvers.js
+++ b/app/assets/javascripts/ci_variable_list/graphql/resolvers.js
@@ -4,8 +4,9 @@ import {
convertObjectPropsToSnakeCase,
} from '../../lib/utils/common_utils';
import { getIdFromGraphQLId } from '../../graphql_shared/utils';
-import { instanceString } from '../constants';
+import { GRAPHQL_GROUP_TYPE, groupString, instanceString } from '../constants';
import getAdminVariables from './queries/variables.query.graphql';
+import getGroupVariables from './queries/group_variables.query.graphql';
const prepareVariableForApi = ({ variable, destroy = false }) => {
return {
@@ -27,6 +28,20 @@ const mapVariableTypes = (variables = [], kind) => {
});
};
+const prepareGroupGraphQLResponse = ({ data, groupId, errors = [] }) => {
+ return {
+ errors,
+ group: {
+ __typename: GRAPHQL_GROUP_TYPE,
+ id: groupId,
+ ciVariables: {
+ __typename: 'CiVariableConnection',
+ nodes: mapVariableTypes(data.variables, groupString),
+ },
+ },
+ };
+};
+
const prepareAdminGraphQLResponse = ({ data, errors = [] }) => {
return {
errors,
@@ -37,6 +52,28 @@ const prepareAdminGraphQLResponse = ({ data, errors = [] }) => {
};
};
+const callGroupEndpoint = async ({
+ endpoint,
+ fullPath,
+ variable,
+ groupId,
+ cache,
+ destroy = false,
+}) => {
+ try {
+ const { data } = await axios.patch(endpoint, {
+ variables_attributes: [prepareVariableForApi({ variable, destroy })],
+ });
+ return prepareGroupGraphQLResponse({ data, groupId });
+ } catch (e) {
+ return prepareGroupGraphQLResponse({
+ data: cache.readQuery({ query: getGroupVariables, variables: { fullPath } }),
+ groupId,
+ errors: [...e.response.data],
+ });
+ }
+};
+
const callAdminEndpoint = async ({ endpoint, variable, cache, destroy = false }) => {
try {
const { data } = await axios.patch(endpoint, {
@@ -54,6 +91,15 @@ const callAdminEndpoint = async ({ endpoint, variable, cache, destroy = false })
export const resolvers = {
Mutation: {
+ addGroupVariable: async (_, { endpoint, fullPath, variable, groupId }, { cache }) => {
+ return callGroupEndpoint({ endpoint, fullPath, variable, groupId, cache });
+ },
+ updateGroupVariable: async (_, { endpoint, fullPath, variable, groupId }, { cache }) => {
+ return callGroupEndpoint({ endpoint, fullPath, variable, groupId, cache });
+ },
+ deleteGroupVariable: async (_, { endpoint, fullPath, variable, groupId }, { cache }) => {
+ return callGroupEndpoint({ endpoint, fullPath, variable, groupId, cache, destroy: true });
+ },
addAdminVariable: async (_, { endpoint, variable }, { cache }) => {
return callAdminEndpoint({ endpoint, variable, cache });
},
diff --git a/app/assets/javascripts/ci_variable_list/index.js b/app/assets/javascripts/ci_variable_list/index.js
index 713a453561e..a74af8aed12 100644
--- a/app/assets/javascripts/ci_variable_list/index.js
+++ b/app/assets/javascripts/ci_variable_list/index.js
@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import CiAdminVariables from './components/ci_admin_variables.vue';
+import CiGroupVariables from './components/ci_group_variables.vue';
import LegacyCiVariableSettings from './components/legacy_ci_variable_settings.vue';
import { resolvers } from './graphql/resolvers';
import createStore from './store';
@@ -32,7 +33,11 @@ const mountCiVariableListApp = (containerEl) => {
const parsedIsGroup = parseBoolean(isGroup);
const isProtectedByDefault = parseBoolean(protectedByDefault);
- const component = CiAdminVariables;
+ let component = CiAdminVariables;
+
+ if (parsedIsGroup) {
+ component = CiGroupVariables;
+ }
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue
index 72fec17ac9d..f4b939fb20f 100644
--- a/app/assets/javascripts/header_search/components/app.vue
+++ b/app/assets/javascripts/header_search/components/app.vue
@@ -23,6 +23,9 @@ import {
SEARCH_SHORTCUTS_MIN_CHARACTERS,
SCOPE_TOKEN_MAX_LENGTH,
INPUT_FIELD_PADDING,
+ IS_SEARCHING,
+ IS_FOCUSED,
+ IS_NOT_FOCUSED,
} from '../constants';
import HeaderSearchAutocompleteItems from './header_search_autocomplete_items.vue';
import HeaderSearchDefaultItems from './header_search_default_items.vue';
@@ -65,6 +68,7 @@ export default {
data() {
return {
showDropdown: false,
+ isFocused: false,
currentFocusIndex: SEARCH_BOX_INDEX,
};
},
@@ -92,20 +96,18 @@ export default {
if (!this.showDropdown || !this.isLoggedIn) {
return false;
}
-
return this.searchOptions?.length > 0;
},
showDefaultItems() {
return !this.searchText;
},
- showScopes() {
+ searchTermOverMin() {
return this.searchText?.length > SEARCH_SHORTCUTS_MIN_CHARACTERS;
},
defaultIndex() {
if (this.showDefaultItems) {
return SEARCH_BOX_INDEX;
}
-
return FIRST_DROPDOWN_INDEX;
},
@@ -132,12 +134,15 @@ export default {
count: this.searchOptions.length,
});
},
- searchBarStateIndicator() {
- const hasIcon =
- this.searchContext?.project || this.searchContext?.group ? 'has-icon' : 'has-no-icon';
- const isSearching = this.showScopes ? 'is-searching' : 'is-not-searching';
- const isActive = this.showSearchDropdown ? 'is-active' : 'is-not-active';
- return `${isActive} ${isSearching} ${hasIcon}`;
+ searchBarClasses() {
+ return {
+ [IS_SEARCHING]: this.searchTermOverMin,
+ [IS_FOCUSED]: this.isFocused,
+ [IS_NOT_FOCUSED]: !this.isFocused,
+ };
+ },
+ showScopeHelp() {
+ return this.searchTermOverMin && this.isFocused;
},
searchBarItem() {
return this.searchOptions?.[0];
@@ -158,11 +163,22 @@ export default {
...mapActions(['setSearch', 'fetchAutocompleteOptions', 'clearAutocomplete']),
openDropdown() {
this.showDropdown = true;
- this.$emit('toggleDropdown', this.showDropdown);
+ this.isFocused = true;
+ this.$emit('expandSearchBar', true);
},
closeDropdown() {
this.showDropdown = false;
- this.$emit('toggleDropdown', this.showDropdown);
+ },
+ collapseAndCloseSearchBar() {
+ // we need a delay on this method
+ // for the search bar not to remove
+ // the clear button from dom
+ // and register clicks on dropdown items
+ setTimeout(() => {
+ this.showDropdown = false;
+ this.isFocused = false;
+ this.$emit('collapseSearchBar');
+ }, 200);
},
submitSearch() {
if (this.search?.length <= SEARCH_SHORTCUTS_MIN_CHARACTERS && this.currentFocusIndex < 0) {
@@ -171,6 +187,7 @@ export default {
return visitUrl(this.currentFocusedOption?.url || this.searchQuery);
},
getAutocompleteOptions: debounce(function debouncedSearch(searchTerm) {
+ this.openDropdown();
if (!searchTerm) {
this.clearAutocomplete();
} else {
@@ -201,7 +218,7 @@ export default {
role="search"
:aria-label="$options.i18n.searchGitlab"
class="header-search gl-relative gl-rounded-base gl-w-full"
- :class="searchBarStateIndicator"
+ :class="searchBarClasses"
data-testid="header-search-form"
>
diff --git a/app/assets/javascripts/header_search/constants.js b/app/assets/javascripts/header_search/constants.js
index a026386b2bd..3a20fb0216d 100644
--- a/app/assets/javascripts/header_search/constants.js
+++ b/app/assets/javascripts/header_search/constants.js
@@ -51,3 +51,7 @@ export const SCOPE_TOKEN_MAX_LENGTH = 36;
export const INPUT_FIELD_PADDING = 52;
export const HEADER_INIT_EVENTS = ['input', 'focus'];
+
+export const IS_SEARCHING = 'is-searching';
+export const IS_FOCUSED = 'is-focused';
+export const IS_NOT_FOCUSED = 'is-not-focused';
diff --git a/app/assets/javascripts/header_search/index.js b/app/assets/javascripts/header_search/index.js
index b2c505d569f..f6f5c6a14fa 100644
--- a/app/assets/javascripts/header_search/index.js
+++ b/app/assets/javascripts/header_search/index.js
@@ -26,12 +26,11 @@ export const initHeaderSearchApp = (search = '') => {
render(createElement) {
return createElement(HeaderSearchApp, {
on: {
- toggleDropdown: (isVisible = false) => {
- if (isVisible) {
- navBarEl?.classList.add('header-search-is-active');
- } else {
- navBarEl?.classList.remove('header-search-is-active');
- }
+ expandSearchBar: () => {
+ navBarEl?.classList.add('header-search-is-active');
+ },
+ collapseSearchBar: () => {
+ navBarEl?.classList.remove('header-search-is-active');
},
},
});
diff --git a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
index 6748a62e777..9cce6723bf7 100644
--- a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
+++ b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
@@ -68,7 +68,7 @@ export default {
}),
tableCell({
key: 'created_at',
- label: __('Date'),
+ label: __('Start date'),
}),
tableCell({
key: 'status',
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 8e862aa8b32..a5580c14a7a 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -264,9 +264,15 @@ export default {
data-testid="work-item-type"
/>
- {{
- __('Confidential')
- }}
+ {{ __('Confidential') }}
ALL ('{scheduled,started}'::text[]));
+CREATE INDEX index_ml_candidate_metrics_on_candidate_id ON ml_candidate_metrics USING btree (candidate_id);
+
+CREATE INDEX index_ml_candidate_params_on_candidate_id ON ml_candidate_params USING btree (candidate_id);
+
+CREATE UNIQUE INDEX index_ml_candidates_on_experiment_id_and_iid ON ml_candidates USING btree (experiment_id, iid);
+
+CREATE INDEX index_ml_candidates_on_user_id ON ml_candidates USING btree (user_id);
+
+CREATE UNIQUE INDEX index_ml_experiments_on_project_id_and_iid ON ml_experiments USING btree (project_id, iid);
+
+CREATE UNIQUE INDEX index_ml_experiments_on_project_id_and_name ON ml_experiments USING btree (project_id, name);
+
+CREATE INDEX index_ml_experiments_on_user_id ON ml_experiments USING btree (user_id);
+
CREATE UNIQUE INDEX index_mr_blocks_on_blocking_and_blocked_mr_ids ON merge_request_blocks USING btree (blocking_merge_request_id, blocked_merge_request_id);
CREATE INDEX index_mr_cleanup_schedules_timestamps_status ON merge_request_cleanup_schedules USING btree (scheduled_at) WHERE ((completed_at IS NULL) AND (status = 0));
@@ -32180,6 +32293,9 @@ ALTER TABLE ONLY merge_request_metrics
ALTER TABLE ONLY vulnerability_feedback
ADD CONSTRAINT fk_563ff1912e FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE SET NULL;
+ALTER TABLE ONLY ml_candidates
+ ADD CONSTRAINT fk_56d6ed4d3d FOREIGN KEY (experiment_id) REFERENCES ml_experiments(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY deploy_keys_projects
ADD CONSTRAINT fk_58a901ca7e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
@@ -32477,6 +32593,9 @@ ALTER TABLE ONLY member_tasks
ALTER TABLE ONLY merge_requests
ADD CONSTRAINT fk_ad525e1f87 FOREIGN KEY (merge_user_id) REFERENCES users(id) ON DELETE SET NULL;
+ALTER TABLE ONLY ml_experiments
+ ADD CONSTRAINT fk_ad89c59858 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY merge_request_metrics
ADD CONSTRAINT fk_ae440388cc FOREIGN KEY (latest_closed_by_id) REFERENCES users(id) ON DELETE SET NULL;
@@ -32987,6 +33106,9 @@ ALTER TABLE ONLY vulnerability_user_mentions
ALTER TABLE ONLY packages_debian_file_metadata
ADD CONSTRAINT fk_rails_1ae85be112 FOREIGN KEY (package_file_id) REFERENCES packages_package_files(id) ON DELETE CASCADE;
+ALTER TABLE ONLY ml_candidates
+ ADD CONSTRAINT fk_rails_1b37441fe5 FOREIGN KEY (user_id) REFERENCES users(id);
+
ALTER TABLE ONLY issuable_slas
ADD CONSTRAINT fk_rails_1b8768cd63 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
@@ -33014,6 +33136,9 @@ ALTER TABLE ONLY geo_repository_created_events
ALTER TABLE ONLY external_status_checks
ADD CONSTRAINT fk_rails_1f5a8aa809 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY ml_experiments
+ ADD CONSTRAINT fk_rails_1fbc5e001f FOREIGN KEY (user_id) REFERENCES users(id);
+
ALTER TABLE ONLY dora_daily_metrics
ADD CONSTRAINT fk_rails_1fd07aff6f FOREIGN KEY (environment_id) REFERENCES environments(id) ON DELETE CASCADE;
@@ -34145,6 +34270,9 @@ ALTER TABLE ONLY alert_management_alert_assignees
ALTER TABLE ONLY geo_hashed_storage_attachments_events
ADD CONSTRAINT fk_rails_d496b088e9 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY ml_candidate_params
+ ADD CONSTRAINT fk_rails_d4a51d1185 FOREIGN KEY (candidate_id) REFERENCES ml_candidates(id);
+
ALTER TABLE ONLY merge_request_reviewers
ADD CONSTRAINT fk_rails_d9fec24b9d FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;
@@ -34292,6 +34420,9 @@ ALTER TABLE ONLY project_relation_exports
ALTER TABLE ONLY label_priorities
ADD CONSTRAINT fk_rails_ef916d14fa FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY ml_candidate_metrics
+ ADD CONSTRAINT fk_rails_efb613a25a FOREIGN KEY (candidate_id) REFERENCES ml_candidates(id);
+
ALTER TABLE ONLY fork_network_members
ADD CONSTRAINT fk_rails_efccadc4ec FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md
index caecdff6e75..49a9b68227d 100644
--- a/doc/api/project_import_export.md
+++ b/doc/api/project_import_export.md
@@ -15,10 +15,18 @@ See also:
Start a new export.
-The endpoint also accepts an `upload` parameter. This parameter is a hash. It contains
-all the necessary information to upload the exported project to a web server or
-to any S3-compatible platform. At the moment we only support binary
-data file uploads to the final server.
+The endpoint also accepts an `upload` hash parameter. It contains all the necessary information to upload the exported
+project to a web server or to any S3-compatible platform. For exports, GitLab:
+
+- Only supports binary data file uploads to the final server.
+- Sends the `Content-Type: application/gzip` header with upload requests. Ensure that your pre-signed URL includes this
+ as part of the signature.
+- Can take some time to complete the project export process. Make sure the upload URL doesn't have a short expiration
+ time and is available throughout the export process.
+- Administrators can modify the maximum export file size. By default, the maximum is unlimited (`0`). To change this,
+ edit `max_export_size` using either:
+ - [Application settings API](settings.md#change-application-settings)
+ - [GitLab UI](../user/admin_area/settings/account_and_limit_settings.md).
The `upload[url]` parameter is required if the `upload` parameter is present.
@@ -46,20 +54,6 @@ curl --request POST --header "PRIVATE-TOKEN: " "https://gitla
}
```
-NOTE:
-The upload request is sent with `Content-Type: application/gzip` header. Ensure that your pre-signed URL includes this as part of the signature.
-
-NOTE:
-The project export process may take some time to complete. Make sure the
-upload URL doesn't have a short expiration time and is available thought
-the export process.
-
-NOTE:
-As an administrator, you can modify the maximum export file size. By default,
-it is set to `0`, for unlimited. To change this value, edit `max_export_size`
-in the [Application settings API](settings.md#change-application-settings)
-or the [Admin UI](../user/admin_area/settings/account_and_limit_settings.md).
-
## Export status
Get the status of export.
diff --git a/doc/ci/yaml/script.md b/doc/ci/yaml/script.md
index 4bffcbca1cc..f1cdcf57e64 100644
--- a/doc/ci/yaml/script.md
+++ b/doc/ci/yaml/script.md
@@ -258,3 +258,27 @@ pages-job:
script:
- 'curl --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" "https://gitlab.example.com/api/v4/projects"'
```
+
+### Job does not fail when using `&&` in a script
+
+If you use `&&` to combine two commands together in a single script line, the job
+might return as successful, even if one of the commands failed. For example:
+
+```yaml
+job-does-not-fail:
+ script:
+ - invalid-command xyz && invalid-command abc
+ - echo $?
+ - echo "The job should have failed already, but this is executed unexpectedly."
+```
+
+The `&&` operator returns an exit code of `0` even though the two commands failed,
+and the job continues to run. To force the script to exit when either command fails,
+enclose the entire line in parentheses:
+
+```yaml
+job-fails:
+ script:
+ - (invalid-command xyz && invalid-command abc)
+ - echo "The job failed already, and this is not executed."
+```
diff --git a/doc/install/aws/gitlab_hybrid_on_aws.md b/doc/install/aws/gitlab_hybrid_on_aws.md
index bc811cab3bf..b7a01cf61f4 100644
--- a/doc/install/aws/gitlab_hybrid_on_aws.md
+++ b/doc/install/aws/gitlab_hybrid_on_aws.md
@@ -32,15 +32,18 @@ Amazon provides a managed Kubernetes service offering known as [Amazon Elastic K
## Available Infrastructure as Code for GitLab Cloud Native Hybrid
+The [GitLab Environment Toolkit (GET)](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/blob/main/README.md) is an effort made by GitLab to create a multi-cloud, multi-GitLab (Omnibus + Cloud Native Hybrid) toolkit to provision GitLab. GET is developed by GitLab developers and is open to community contributions. GET is where GitLab is investing its resources as the primary option for Infrastructure as Code, and is being actively used in production as a part of [GitLab Dedicated](../../subscriptions/gitlab_dedicated/index.md).
+
+Read the [GitLab Environment Toolkit (GET) direction](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/blob/main/README.md#direction) to learn more about the project and where it is going.
+
The [AWS Quick Start for GitLab Cloud Native Hybrid on EKS](https://aws-quickstart.github.io/quickstart-eks-gitlab/) is developed by AWS, GitLab, and the community that contributes to AWS Quick Starts, whether directly to the GitLab Quick Start or to the underlying Quick Start dependencies GitLab inherits (for example, EKS Quick Start).
+GET is recommended for most deployments. The AWS Quick Start can be used if the IaC language of choice is CloudFormation, integration with AWS services like Control Tower is desired, or preference for a UI-driven configuration experience or when any aspect in the below table is an overriding concern.
+
NOTE:
This automation is in **[Open Beta](https://about.gitlab.com/handbook/product/gitlab-the-product/#open-beta)**. GitLab is working with AWS on resolving [the outstanding issues](https://github.com/aws-quickstart/quickstart-eks-gitlab/issues?q=is%3Aissue+is%3Aopen+%5BHL%5D) before it is fully released. You can subscribe to this issue to be notified of progress and release announcements: [AWS Quick Start for GitLab Cloud Native Hybrid on EKS Status: Beta](https://gitlab.com/gitlab-com/alliances/aws/public-tracker/-/issues/11).
The Beta version deploys Aurora PostgreSQL, but the release version will deploy Amazon RDS PostgreSQL due to [known issues](https://gitlab.com/gitlab-com/alliances/aws/public-tracker/-/issues?label_name%5B%5D=AWS+Known+Issue) with Aurora. All performance testing results will also be redone after this change has been made.
-The [GitLab Environment Toolkit (GET)](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/tree/main) is an effort made by GitLab to create a multi-cloud, multi-GitLab (Omnibus + Cloud Native Hybrid) toolkit to provision GitLab. GET is developed by GitLab developers and is open to community contributions.
-It is helpful to review the [GitLab Environment Toolkit (GET) Issues](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/issues) to understand if any of them may affect your provisioning plans.
-
| | [AWS Quick Start for GitLab Cloud Native Hybrid on EKS](https://aws-quickstart.github.io/quickstart-eks-gitlab/) | [GitLab Environment Toolkit (GET)](https://gitlab.com/gitlab-org/gitlab-environment-toolkit) |
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| Overview and Vision | [AWS Quick Start](https://aws.amazon.com/quickstart/) | [GitLab Environment Toolkit](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/blob/main/README.md) |
@@ -56,9 +59,11 @@ It is helpful to review the [GitLab Environment Toolkit (GET) Issues](https://gi
| Results in a Ready-to-Use instance | Yes | Manual Actions or
Supplemental IaC Required |
| **Configuration Features** | | |
| Can deploy Omnibus GitLab (non-Kubernetes) | No | Yes |
-| Results in a self-healing Gitaly Cluster configuration | Yes | No |
+| Can deploy Single Instance Omnibus GitLab (non-Kubernetes) | No | Yes |
| Complete Internal Encryption | 85%, Targeting 100% | Manual |
| AWS GovCloud Support | Yes | TBD |
+| No Code Form-Based Deployment User Experience Available | Yes | No |
+| Full IaC User Experience Available | Yes | Yes |
### Two and Three Zone High Availability
diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb
index 96ac959a3f4..613f7ff3370 100644
--- a/lib/gitlab/ci/config/entry/image.rb
+++ b/lib/gitlab/ci/config/entry/image.rb
@@ -8,37 +8,13 @@ module Gitlab
# Entry that represents a Docker image.
#
class Image < ::Gitlab::Config::Entry::Node
- include ::Gitlab::Config::Entry::Validatable
- include ::Gitlab::Config::Entry::Attributable
- include ::Gitlab::Config::Entry::Configurable
-
- ALLOWED_KEYS = %i[name entrypoint ports pull_policy].freeze
- LEGACY_ALLOWED_KEYS = %i[name entrypoint ports].freeze
+ include ::Gitlab::Ci::Config::Entry::Imageable
validations do
- validates :config, hash_or_string: true
- validates :config, allowed_keys: ALLOWED_KEYS, if: :ci_docker_image_pull_policy_enabled?
- validates :config, allowed_keys: LEGACY_ALLOWED_KEYS, unless: :ci_docker_image_pull_policy_enabled?
- validates :config, disallowed_keys: %i[ports], unless: :with_image_ports?
-
- validates :name, type: String, presence: true
- validates :entrypoint, array_of_strings: true, allow_nil: true
- end
-
- entry :ports, Entry::Ports,
- description: 'Ports used to expose the image'
-
- entry :pull_policy, Entry::PullPolicy,
- description: 'Pull policy for the image'
-
- attributes :ports, :pull_policy
-
- def name
- value[:name]
- end
-
- def entrypoint
- value[:entrypoint]
+ validates :config, allowed_keys: IMAGEABLE_ALLOWED_KEYS,
+ if: :ci_docker_image_pull_policy_enabled?
+ validates :config, allowed_keys: IMAGEABLE_LEGACY_ALLOWED_KEYS,
+ unless: :ci_docker_image_pull_policy_enabled?
end
def value
@@ -55,18 +31,6 @@ module Gitlab
{}
end
end
-
- def with_image_ports?
- opt(:with_image_ports)
- end
-
- def ci_docker_image_pull_policy_enabled?
- ::Feature.enabled?(:ci_docker_image_pull_policy)
- end
-
- def skip_config_hash_validation?
- true
- end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/imageable.rb b/lib/gitlab/ci/config/entry/imageable.rb
new file mode 100644
index 00000000000..f045ee3d549
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/imageable.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Represents Imageable concern shared by Image and Service.
+ module Imageable
+ extend ActiveSupport::Concern
+
+ include ::Gitlab::Config::Entry::Attributable
+ include ::Gitlab::Config::Entry::Configurable
+
+ IMAGEABLE_ALLOWED_KEYS = %i[name entrypoint ports pull_policy].freeze
+ IMAGEABLE_LEGACY_ALLOWED_KEYS = %i[name entrypoint ports].freeze
+
+ included do
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, hash_or_string: true
+ validates :config, disallowed_keys: %i[ports], unless: :with_image_ports?
+
+ validates :name, type: String, presence: true
+ validates :entrypoint, array_of_strings: true, allow_nil: true
+ end
+
+ attributes :ports, :pull_policy
+
+ entry :ports, Entry::Ports,
+ description: 'Ports used to expose the image/service'
+
+ entry :pull_policy, Entry::PullPolicy,
+ description: 'Pull policy for the image/service'
+ end
+
+ def name
+ value[:name]
+ end
+
+ def entrypoint
+ value[:entrypoint]
+ end
+
+ def with_image_ports?
+ opt(:with_image_ports)
+ end
+
+ def ci_docker_image_pull_policy_enabled?
+ ::Feature.enabled?(:ci_docker_image_pull_policy)
+ end
+
+ def skip_config_hash_validation?
+ true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb
index 1a35f7de6cf..0e19447dff8 100644
--- a/lib/gitlab/ci/config/entry/service.rb
+++ b/lib/gitlab/ci/config/entry/service.rb
@@ -7,41 +7,28 @@ module Gitlab
##
# Entry that represents a configuration of Docker service.
#
- # TODO: remove duplication with Image superclass by defining a common
- # Imageable concern.
- # https://gitlab.com/gitlab-org/gitlab/issues/208774
class Service < ::Gitlab::Config::Entry::Node
- include ::Gitlab::Config::Entry::Validatable
- include ::Gitlab::Config::Entry::Attributable
- include ::Gitlab::Config::Entry::Configurable
+ include ::Gitlab::Ci::Config::Entry::Imageable
- ALLOWED_KEYS = %i[name entrypoint command alias ports variables pull_policy].freeze
- LEGACY_ALLOWED_KEYS = %i[name entrypoint command alias ports variables].freeze
+ ALLOWED_KEYS = %i[command alias variables].freeze
+ LEGACY_ALLOWED_KEYS = %i[command alias variables].freeze
validations do
- validates :config, hash_or_string: true
- validates :config, allowed_keys: ALLOWED_KEYS, if: :ci_docker_image_pull_policy_enabled?
- validates :config, allowed_keys: LEGACY_ALLOWED_KEYS, unless: :ci_docker_image_pull_policy_enabled?
- validates :config, disallowed_keys: %i[ports], unless: :with_image_ports?
- validates :name, type: String, presence: true
- validates :entrypoint, array_of_strings: true, allow_nil: true
+ validates :config, allowed_keys: ALLOWED_KEYS + IMAGEABLE_ALLOWED_KEYS,
+ if: :ci_docker_image_pull_policy_enabled?
+ validates :config, allowed_keys: LEGACY_ALLOWED_KEYS + IMAGEABLE_LEGACY_ALLOWED_KEYS,
+ unless: :ci_docker_image_pull_policy_enabled?
validates :command, array_of_strings: true, allow_nil: true
validates :alias, type: String, allow_nil: true
validates :alias, type: String, presence: true, unless: ->(record) { record.ports.blank? }
end
- entry :ports, Entry::Ports,
- description: 'Ports used to expose the service'
-
- entry :pull_policy, Entry::PullPolicy,
- description: 'Pull policy for the service'
-
entry :variables, ::Gitlab::Ci::Config::Entry::Variables,
description: 'Environment variables available for this service.',
inherit: false
- attributes :ports, :pull_policy, :variables
+ attributes :variables
def alias
value[:alias]
@@ -51,14 +38,6 @@ module Gitlab
value[:command]
end
- def name
- value[:name]
- end
-
- def entrypoint
- value[:entrypoint]
- end
-
def value
if string?
{ name: @config }
@@ -70,18 +49,6 @@ module Gitlab
{}
end
end
-
- def with_image_ports?
- opt(:with_image_ports)
- end
-
- def ci_docker_image_pull_policy_enabled?
- ::Feature.enabled?(:ci_docker_image_pull_policy)
- end
-
- def skip_config_hash_validation?
- true
- end
end
end
end
diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml
index 06287126978..d05eee7d6e6 100644
--- a/lib/gitlab/database/gitlab_schemas.yml
+++ b/lib/gitlab/database/gitlab_schemas.yml
@@ -324,6 +324,10 @@ metrics_dashboard_annotations: :gitlab_main
metrics_users_starred_dashboards: :gitlab_main
milestone_releases: :gitlab_main
milestones: :gitlab_main
+ml_candidates: :gitlab_main
+ml_experiments: :gitlab_main
+ml_candidate_metrics: :gitlab_main
+ml_candidate_params: :gitlab_main
namespace_admin_notes: :gitlab_main
namespace_aggregation_schedules: :gitlab_main
namespace_bans: :gitlab_main
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 93b74d3d54c..8e7d31fd88e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -44391,6 +44391,9 @@ msgstr ""
msgid "WorkItem|None"
msgstr ""
+msgid "WorkItem|Only project members with at least the Reporter role, the author, and assignees can view or be notified about this task."
+msgstr ""
+
msgid "WorkItem|Open"
msgstr ""
diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb
index 9af9baeb5bb..ab24162ad5a 100644
--- a/spec/features/group_variables_spec.rb
+++ b/spec/features/group_variables_spec.rb
@@ -23,7 +23,11 @@ RSpec.describe 'Group variables', :js do
it_behaves_like 'variable list'
end
- # TODO: Uncomment when the new graphQL app for variable settings
- # is enabled.
- # it_behaves_like 'variable list'
+ context 'with enabled ff `ci_variable_settings_graphql' do
+ before do
+ visit page_path
+ end
+
+ it_behaves_like 'variable list'
+ end
end
diff --git a/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js
new file mode 100644
index 00000000000..e45656acfd8
--- /dev/null
+++ b/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js
@@ -0,0 +1,183 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import { resolvers } from '~/ci_variable_list/graphql/resolvers';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+
+import ciGroupVariables from '~/ci_variable_list/components/ci_group_variables.vue';
+import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
+import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
+import getGroupVariables from '~/ci_variable_list/graphql/queries/group_variables.query.graphql';
+
+import addGroupVariable from '~/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql';
+import deleteGroupVariable from '~/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql';
+import updateGroupVariable from '~/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql';
+
+import { genericMutationErrorText, variableFetchErrorText } from '~/ci_variable_list/constants';
+
+import { mockGroupVariables, newVariable } from '../mocks';
+
+jest.mock('~/flash');
+
+Vue.use(VueApollo);
+
+const mockProvide = {
+ endpoint: '/variables',
+ groupPath: '/namespace/group',
+ groupId: 1,
+};
+
+describe('Ci Group Variable list', () => {
+ let wrapper;
+
+ let mockApollo;
+ let mockVariables;
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findCiTable = () => wrapper.findComponent(GlTable);
+ const findCiSettings = () => wrapper.findComponent(ciVariableSettings);
+
+ // eslint-disable-next-line consistent-return
+ const createComponentWithApollo = async ({ isLoading = false } = {}) => {
+ const handlers = [[getGroupVariables, mockVariables]];
+
+ mockApollo = createMockApollo(handlers, resolvers);
+
+ wrapper = shallowMount(ciGroupVariables, {
+ provide: mockProvide,
+ apolloProvider: mockApollo,
+ stubs: { ciVariableSettings, ciVariableTable },
+ });
+
+ if (!isLoading) {
+ return waitForPromises();
+ }
+ };
+
+ beforeEach(() => {
+ mockVariables = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('while queries are being fetch', () => {
+ beforeEach(() => {
+ createComponentWithApollo({ isLoading: true });
+ });
+
+ it('shows a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findCiTable().exists()).toBe(false);
+ });
+ });
+
+ describe('when queries are resolved', () => {
+ describe('successfuly', () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+
+ await createComponentWithApollo();
+ });
+
+ it('passes down the expected environments as props', () => {
+ expect(findCiSettings().props('environments')).toEqual([]);
+ });
+
+ it('passes down the expected variables as props', () => {
+ expect(findCiSettings().props('variables')).toEqual(
+ mockGroupVariables.data.group.ciVariables.nodes,
+ );
+ });
+
+ it('createFlash was not called', () => {
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('with an error for variables', () => {
+ beforeEach(async () => {
+ mockVariables.mockRejectedValue();
+
+ await createComponentWithApollo();
+ });
+
+ it('calls createFlash with the expected error message', () => {
+ expect(createFlash).toHaveBeenCalledWith({ message: variableFetchErrorText });
+ });
+ });
+ });
+
+ describe('mutations', () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+
+ await createComponentWithApollo();
+ });
+ it.each`
+ actionName | mutation | event
+ ${'add'} | ${addGroupVariable} | ${'add-variable'}
+ ${'update'} | ${updateGroupVariable} | ${'update-variable'}
+ ${'delete'} | ${deleteGroupVariable} | ${'delete-variable'}
+ `(
+ 'calls the right mutation when user performs $actionName variable',
+ async ({ event, mutation }) => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
+ await findCiSettings().vm.$emit(event, newVariable);
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation,
+ variables: {
+ endpoint: mockProvide.endpoint,
+ fullPath: mockProvide.groupPath,
+ groupId: convertToGraphQLId('Group', mockProvide.groupId),
+ variable: newVariable,
+ },
+ });
+ },
+ );
+
+ it.each`
+ actionName | event | mutationName
+ ${'add'} | ${'add-variable'} | ${'addGroupVariable'}
+ ${'update'} | ${'update-variable'} | ${'updateGroupVariable'}
+ ${'delete'} | ${'delete-variable'} | ${'deleteGroupVariable'}
+ `(
+ 'throws with the specific graphql error if present when user performs $actionName variable',
+ async ({ event, mutationName }) => {
+ const graphQLErrorMessage = 'There is a problem with this graphQL action';
+ jest
+ .spyOn(wrapper.vm.$apollo, 'mutate')
+ .mockResolvedValue({ data: { [mutationName]: { errors: [graphQLErrorMessage] } } });
+ await findCiSettings().vm.$emit(event, newVariable);
+ await nextTick();
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledWith({ message: graphQLErrorMessage });
+ },
+ );
+
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'throws generic error when the mutation fails with no graphql errors and user performs $actionName variable',
+ async ({ event }) => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => {
+ throw new Error();
+ });
+ await findCiSettings().vm.$emit(event, newVariable);
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledWith({ message: genericMutationErrorText });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/ci_variable_list/mocks.js b/spec/frontend/ci_variable_list/mocks.js
index 07dc7a8c91f..89ba77858dc 100644
--- a/spec/frontend/ci_variable_list/mocks.js
+++ b/spec/frontend/ci_variable_list/mocks.js
@@ -1,4 +1,4 @@
-import { variableTypes, instanceString } from '~/ci_variable_list/constants';
+import { variableTypes, groupString, instanceString } from '~/ci_variable_list/constants';
export const devName = 'dev';
export const prodName = 'prod';
@@ -82,22 +82,12 @@ export const mockProjectVariables = {
},
};
-export const mockGroupEnvironments = {
- data: {
- group: {
- __typename: 'Group',
- id: 1,
- environments: defaultEnvs,
- },
- },
-};
-
export const mockGroupVariables = {
data: {
group: {
__typename: 'Group',
id: 1,
- ciVariables: createDefaultVars(),
+ ciVariables: createDefaultVars({ kind: groupString }),
},
},
};
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js
index d89218f5542..6a138f9a247 100644
--- a/spec/frontend/header_search/components/app_spec.js
+++ b/spec/frontend/header_search/components/app_spec.js
@@ -15,6 +15,10 @@ import {
ICON_GROUP,
ICON_SUBGROUP,
SCOPE_TOKEN_MAX_LENGTH,
+ IS_SEARCHING,
+ IS_NOT_FOCUSED,
+ IS_FOCUSED,
+ SEARCH_SHORTCUTS_MIN_CHARACTERS,
} from '~/header_search/constants';
import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
import { ENTER_KEY } from '~/lib/utils/keys';
@@ -170,6 +174,14 @@ describe('HeaderSearchApp', () => {
it(`should render the Dropdown Navigation Component`, () => {
expect(findDropdownKeyboardNavigation().exists()).toBe(true);
});
+
+ it(`should close the dropdown when press escape key`, async () => {
+ findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: 27 }));
+ await nextTick();
+ expect(findHeaderSearchDropdown().exists()).toBe(false);
+ // only one event emmited from findHeaderSearchInput().vm.$emit('click');
+ expect(wrapper.emitted().expandSearchBar.length).toBe(1);
+ });
});
});
@@ -245,6 +257,7 @@ describe('HeaderSearchApp', () => {
searchOptions: () => searchOptions,
},
);
+ findHeaderSearchInput().vm.$emit('click');
});
it(`${hasToken ? 'is' : 'is NOT'} rendered when data set has type "${
@@ -263,47 +276,43 @@ describe('HeaderSearchApp', () => {
});
});
- describe('form wrapper', () => {
+ describe('form', () => {
describe.each`
- searchContext | search | searchOptions
- ${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]}
- ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]}
- ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
- ${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
- ${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS}
- ${null} | ${null} | ${[]}
- `('', ({ searchContext, search, searchOptions }) => {
+ searchContext | search | searchOptions | isFocused
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]} | ${true}
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]} | ${true}
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true}
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${false}
+ ${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true}
+ ${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true}
+ ${null} | ${null} | ${[]} | ${true}
+ `('wrapper', ({ searchContext, search, searchOptions, isFocused }) => {
beforeEach(() => {
window.gon.current_username = MOCK_USERNAME;
-
createComponent({ search, searchContext }, { searchOptions: () => searchOptions });
-
- findHeaderSearchInput().vm.$emit('click');
+ if (isFocused) {
+ findHeaderSearchInput().vm.$emit('click');
+ }
});
- const hasIcon = Boolean(searchContext?.group);
- const isSearching = Boolean(search);
- const isActive = Boolean(searchOptions.length > 0);
+ const isSearching = search?.length > SEARCH_SHORTCUTS_MIN_CHARACTERS;
- it(`${hasIcon ? 'with' : 'without'} search context classes contain "${
- hasIcon ? 'has-icon' : 'has-no-icon'
- }"`, () => {
- const iconClassRegex = hasIcon ? 'has-icon' : 'has-no-icon';
- expect(findHeaderSearchForm().classes()).toContain(iconClassRegex);
+ it(`classes ${isSearching ? 'contain' : 'do not contain'} "${IS_SEARCHING}"`, () => {
+ if (isSearching) {
+ expect(findHeaderSearchForm().classes()).toContain(IS_SEARCHING);
+ return;
+ }
+ if (!isSearching) {
+ expect(findHeaderSearchForm().classes()).not.toContain(IS_SEARCHING);
+ }
});
- it(`${isSearching ? 'with' : 'without'} search string classes contain "${
- isSearching ? 'is-searching' : 'is-not-searching'
+ it(`classes ${isSearching ? 'contain' : 'do not contain'} "${
+ isFocused ? IS_FOCUSED : IS_NOT_FOCUSED
}"`, () => {
- const iconClassRegex = isSearching ? 'is-searching' : 'is-not-searching';
- expect(findHeaderSearchForm().classes()).toContain(iconClassRegex);
- });
-
- it(`${isActive ? 'with' : 'without'} search results classes contain "${
- isActive ? 'is-active' : 'is-not-active'
- }"`, () => {
- const iconClassRegex = isActive ? 'is-active' : 'is-not-active';
- expect(findHeaderSearchForm().classes()).toContain(iconClassRegex);
+ expect(findHeaderSearchForm().classes()).toContain(
+ isFocused ? IS_FOCUSED : IS_NOT_FOCUSED,
+ );
});
});
});
@@ -323,6 +332,7 @@ describe('HeaderSearchApp', () => {
searchOptions: () => searchOptions,
},
);
+ findHeaderSearchInput().vm.$emit('click');
});
it(`icon for data set type "${searchOptions[0]?.html_id}" ${
diff --git a/spec/frontend/ide/components/repo_tab_spec.js b/spec/frontend/ide/components/repo_tab_spec.js
index 0dda176b47c..b26edc5a85b 100644
--- a/spec/frontend/ide/components/repo_tab_spec.js
+++ b/spec/frontend/ide/components/repo_tab_spec.js
@@ -164,7 +164,7 @@ describe('RepoTab', () => {
await wrapper.find('.multi-file-tab-close').trigger('click');
- expect(tab.opened).toBeFalsy();
+ expect(tab.opened).toBe(false);
expect(wrapper.vm.$store.state.changedFiles).toHaveLength(1);
});
@@ -180,7 +180,7 @@ describe('RepoTab', () => {
await wrapper.find('.multi-file-tab-close').trigger('click');
- expect(tab.opened).toBeFalsy();
+ expect(tab.opened).toBe(false);
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
index 8f79c74368f..ed0abaaf576 100644
--- a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
@@ -128,7 +128,7 @@ describe('SignInOauthButton', () => {
});
it('does not emit `sign-in` event', () => {
- expect(wrapper.emitted('sign-in')).toBeFalsy();
+ expect(wrapper.emitted('sign-in')).toBeUndefined();
});
it('sets `loading` prop of button to `false`', () => {
@@ -179,7 +179,7 @@ describe('SignInOauthButton', () => {
});
it('emits `sign-in` event with user data', () => {
- expect(wrapper.emitted('sign-in')[0]).toBeTruthy();
+ expect(wrapper.emitted('sign-in')).toHaveLength(1);
});
});
@@ -200,7 +200,7 @@ describe('SignInOauthButton', () => {
});
it('does not emit `sign-in` event', () => {
- expect(wrapper.emitted('sign-in')).toBeFalsy();
+ expect(wrapper.emitted('sign-in')).toBeUndefined();
});
it('sets `loading` prop of button to `false`', () => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
index ef6c4a1fa32..b163557618e 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
@@ -4,7 +4,6 @@ import { GlEmptyState } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { stripTypenames } from 'helpers/graphql_helpers';
import component from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue';
import TagsListRow from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue';
@@ -96,8 +95,8 @@ describe('Tags List', () => {
it('binds the correct props', () => {
expect(findRegistryList().props()).toMatchObject({
title: '2 tags',
- pagination: stripTypenames(tagsPageInfo),
- items: stripTypenames(tags),
+ pagination: tagsPageInfo,
+ items: tags,
idProperty: 'name',
});
});
diff --git a/spec/frontend/releases/__snapshots__/util_spec.js.snap b/spec/frontend/releases/__snapshots__/util_spec.js.snap
index 2a12d945792..55e3dda60a0 100644
--- a/spec/frontend/releases/__snapshots__/util_spec.js.snap
+++ b/spec/frontend/releases/__snapshots__/util_spec.js.snap
@@ -210,12 +210,15 @@ exports[`releases/util.js convertOneReleaseForEditingGraphQLResponse matches sna
Object {
"data": Object {
"_links": Object {
+ "__typename": "ReleaseLinks",
"self": "http://localhost/releases-namespace/releases-project/-/releases/v1.1",
"selfUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1",
},
"assets": Object {
+ "count": undefined,
"links": Array [
Object {
+ "__typename": "ReleaseAssetLink",
"directAssetPath": "/binaries/awesome-app-3",
"id": "gid://gitlab/Releases::Link/13",
"linkType": "image",
@@ -223,6 +226,7 @@ Object {
"url": "https://example.com/image",
},
Object {
+ "__typename": "ReleaseAssetLink",
"directAssetPath": "/binaries/awesome-app-2",
"id": "gid://gitlab/Releases::Link/12",
"linkType": "package",
@@ -230,6 +234,7 @@ Object {
"url": "https://example.com/package",
},
Object {
+ "__typename": "ReleaseAssetLink",
"directAssetPath": "/binaries/awesome-app-1",
"id": "gid://gitlab/Releases::Link/11",
"linkType": "runbook",
@@ -237,6 +242,7 @@ Object {
"url": "http://localhost/releases-namespace/releases-project/runbook",
},
Object {
+ "__typename": "ReleaseAssetLink",
"directAssetPath": "/binaries/linux-amd64",
"id": "gid://gitlab/Releases::Link/10",
"linkType": "other",
@@ -246,22 +252,31 @@ Object {
],
"sources": Array [],
},
+ "author": undefined,
"description": "Best. Release. **Ever.** :rocket:",
"evidences": Array [],
"milestones": Array [
Object {
+ "__typename": "Milestone",
"id": "gid://gitlab/Milestone/123",
"issueStats": Object {},
+ "stats": undefined,
"title": "12.3",
+ "webPath": undefined,
+ "webUrl": undefined,
},
Object {
+ "__typename": "Milestone",
"id": "gid://gitlab/Milestone/124",
"issueStats": Object {},
+ "stats": undefined,
"title": "12.4",
+ "webPath": undefined,
+ "webUrl": undefined,
},
],
"name": "The first release",
- "releasedAt": "2018-12-10T00:00:00.000Z",
+ "releasedAt": 2018-12-10T00:00:00.000Z,
"tagName": "v1.1",
"tagPath": "/releases-namespace/releases-project/-/tags/v1.1",
},
diff --git a/spec/frontend/releases/util_spec.js b/spec/frontend/releases/util_spec.js
index dfea3fc0037..055c8e8b39f 100644
--- a/spec/frontend/releases/util_spec.js
+++ b/spec/frontend/releases/util_spec.js
@@ -7,7 +7,6 @@ import {
convertAllReleasesGraphQLResponse,
convertOneReleaseGraphQLResponse,
} from '~/releases/util';
-import { stripTypenames } from 'helpers/graphql_helpers';
describe('releases/util.js', () => {
describe('convertGraphQLRelease', () => {
@@ -137,7 +136,7 @@ describe('releases/util.js', () => {
describe('convertOneReleaseForEditingGraphQLResponse', () => {
it('matches snapshot', () => {
expect(
- stripTypenames(convertOneReleaseGraphQLResponse(originalOneReleaseForEditingQueryResponse)),
+ convertOneReleaseGraphQLResponse(originalOneReleaseForEditingQueryResponse),
).toMatchSnapshot();
});
});
diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js
index ee69e3bee83..f0ef8aee7a9 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -5,7 +5,6 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking } from 'helpers/tracking_helper';
-import { stripTypenames } from 'helpers/graphql_helpers';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
@@ -311,9 +310,7 @@ describe('WorkItemAssignees component', () => {
findAssignSelfButton().vm.$emit('click', new MouseEvent('click'));
await nextTick();
- expect(findTokenSelector().props('selectedTokens')).toMatchObject([
- stripTypenames(currentUser),
- ]);
+ expect(findTokenSelector().props('selectedTokens')).toMatchObject([currentUser]);
expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({
input: {
id: workItemId,
@@ -330,9 +327,7 @@ describe('WorkItemAssignees component', () => {
await waitForPromises();
expect(findTokenSelector().props('dropdownItems')[0]).toEqual(
- expect.objectContaining({
- ...stripTypenames(currentUserResponse.data.currentUser),
- }),
+ expect.objectContaining(currentUserResponse.data.currentUser),
);
});
diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/pages/work_item_detail_spec.js
index 5fef151be81..823981df880 100644
--- a/spec/frontend/work_items/pages/work_item_detail_spec.js
+++ b/spec/frontend/work_items/pages/work_item_detail_spec.js
@@ -247,6 +247,9 @@ describe('WorkItemDetail component', () => {
variant: 'warning',
icon: 'eye-slash',
});
+ expect(confidentialBadge.attributes('title')).toBe(
+ 'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this task.',
+ );
expect(confidentialBadge.text()).toBe('Confidential');
});
diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb
index 0fa6d4f8804..6121c28070f 100644
--- a/spec/lib/gitlab/ci/config/entry/image_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb
@@ -1,12 +1,8 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
-require 'support/helpers/stubbed_feature'
-require 'support/helpers/stub_feature_flags'
+require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Image do
- include StubFeatureFlags
-
before do
stub_feature_flags(ci_docker_image_pull_policy: true)
diff --git a/spec/lib/gitlab/ci/config/entry/imageable_spec.rb b/spec/lib/gitlab/ci/config/entry/imageable_spec.rb
new file mode 100644
index 00000000000..88f8e260611
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/imageable_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Config::Entry::Imageable do
+ let(:node_class) do
+ Class.new(::Gitlab::Config::Entry::Node) do
+ include ::Gitlab::Ci::Config::Entry::Imageable
+
+ validations do
+ validates :config, allowed_keys: ::Gitlab::Ci::Config::Entry::Imageable::IMAGEABLE_ALLOWED_KEYS
+ end
+
+ def self.name
+ 'node'
+ end
+
+ def value
+ if string?
+ { name: @config }
+ elsif hash?
+ {
+ name: @config[:name]
+ }.compact
+ else
+ {}
+ end
+ end
+ end
+ end
+
+ subject(:entry) { node_class.new(config) }
+
+ before do
+ entry.compose!
+ end
+
+ context 'when entry value is correct' do
+ let(:config) { 'image:1.0' }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ let(:config) { ['image:1.0'] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors.first)
+ .to match /config should be a hash or a string/
+ end
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+
+ context 'when unexpected key is specified' do
+ let(:config) { { name: 'image:1.0', non_existing: 'test' } }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors.first)
+ .to match /config contains unknown keys: non_existing/
+ end
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
index 10b4cba3bbb..c85fe366da6 100644
--- a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
@@ -2,7 +2,6 @@
require 'fast_spec_helper'
require 'gitlab_chronic_duration'
-require 'support/helpers/stub_feature_flags'
require_dependency 'active_model'
RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do
diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb
index 3c000fd09ed..821ab442d61 100644
--- a/spec/lib/gitlab/ci/config/entry/service_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb
@@ -1,12 +1,8 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
-require 'support/helpers/stubbed_feature'
-require 'support/helpers/stub_feature_flags'
+require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Service do
- include StubFeatureFlags
-
before do
stub_feature_flags(ci_docker_image_pull_policy: true)
entry.compose!
diff --git a/spec/models/ml/candidate_metric_spec.rb b/spec/models/ml/candidate_metric_spec.rb
new file mode 100644
index 00000000000..5ee6030fb8e
--- /dev/null
+++ b/spec/models/ml/candidate_metric_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ml::CandidateMetric do
+ describe 'associations' do
+ it { is_expected.to belong_to(:candidate) }
+ end
+end
diff --git a/spec/models/ml/candidate_param_spec.rb b/spec/models/ml/candidate_param_spec.rb
new file mode 100644
index 00000000000..ff38e471219
--- /dev/null
+++ b/spec/models/ml/candidate_param_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ml::CandidateParam do
+ describe 'associations' do
+ it { is_expected.to belong_to(:candidate) }
+ end
+end
diff --git a/spec/models/ml/candidate_spec.rb b/spec/models/ml/candidate_spec.rb
new file mode 100644
index 00000000000..a48e291fa55
--- /dev/null
+++ b/spec/models/ml/candidate_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ml::Candidate do
+ describe 'associations' do
+ it { is_expected.to belong_to(:experiment) }
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to have_many(:params) }
+ it { is_expected.to have_many(:metrics) }
+ end
+end
diff --git a/spec/models/ml/experiment_spec.rb b/spec/models/ml/experiment_spec.rb
new file mode 100644
index 00000000000..dca5280a8fe
--- /dev/null
+++ b/spec/models/ml/experiment_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ml::Experiment do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to have_many(:candidates) }
+ end
+end
diff --git a/tooling/config/CODEOWNERS.yml b/tooling/config/CODEOWNERS.yml
index 9a3a7e8e5be..71818b67ab1 100644
--- a/tooling/config/CODEOWNERS.yml
+++ b/tooling/config/CODEOWNERS.yml
@@ -70,9 +70,12 @@
keywords:
- '*.png'
- '*bundler-audit*'
+ - '**/merge_requests/**'
- '/ee/app/services/audit_events/*'
+ - '/ee/config/feature_flags/development/auditor_group_runner_access.yml'
- '/ee/spec/services/audit_events/*'
- '/ee/spec/services/ci/*'
- '/ee/spec/services/personal_access_tokens/*'
+ - '/qa/**/*'
patterns:
- '%{keyword}'