diff options
95 files changed, 917 insertions, 848 deletions
diff --git a/.rubocop_todo/rspec/expect_in_hook.yml b/.rubocop_todo/rspec/expect_in_hook.yml index aa150006d08..dbf7abf6905 100644 --- a/.rubocop_todo/rspec/expect_in_hook.yml +++ b/.rubocop_todo/rspec/expect_in_hook.yml @@ -73,7 +73,6 @@ RSpec/ExpectInHook: - 'ee/spec/services/members/await_service_spec.rb' - 'ee/spec/services/merge_requests/mergeability/check_approved_service_spec.rb' - 'ee/spec/services/merge_requests/mergeability/check_blocked_by_other_mrs_service_spec.rb' - - 'ee/spec/services/merge_requests/mergeability/check_denied_policies_service_spec.rb' - 'ee/spec/services/projects/create_from_template_service_spec.rb' - 'ee/spec/services/projects/mark_for_deletion_service_spec.rb' - 'ee/spec/services/projects/update_mirror_service_spec.rb' diff --git a/.rubocop_todo/rspec/feature_category.yml b/.rubocop_todo/rspec/feature_category.yml index 57bd3343860..eb740c099c3 100644 --- a/.rubocop_todo/rspec/feature_category.yml +++ b/.rubocop_todo/rspec/feature_category.yml @@ -1,7 +1,6 @@ --- RSpec/FeatureCategory: Exclude: - - 'ee/spec/components/namespaces/storage/subgroup_pre_enforcement_alert_component_spec.rb' - 'ee/spec/controllers/admin/application_settings_controller_spec.rb' - 'ee/spec/controllers/admin/clusters_controller_spec.rb' - 'ee/spec/controllers/autocomplete_controller_spec.rb' @@ -652,7 +651,6 @@ RSpec/FeatureCategory: - 'ee/spec/lib/ee/gitlab/issuable_metadata_spec.rb' - 'ee/spec/lib/ee/gitlab/metrics/samplers/database_sampler_spec.rb' - 'ee/spec/lib/ee/gitlab/middleware/read_only_spec.rb' - - 'ee/spec/lib/ee/gitlab/namespace_storage_size_error_message_spec.rb' - 'ee/spec/lib/ee/gitlab/omniauth_initializer_spec.rb' - 'ee/spec/lib/ee/gitlab/pages/deployment_update_spec.rb' - 'ee/spec/lib/ee/gitlab/rack_attack/request_spec.rb' @@ -950,7 +948,6 @@ RSpec/FeatureCategory: - 'ee/spec/mailers/ee/emails/projects_spec.rb' - 'ee/spec/mailers/emails/epics_spec.rb' - 'ee/spec/mailers/emails/group_memberships_spec.rb' - - 'ee/spec/mailers/emails/namespace_storage_usage_mailer_spec.rb' - 'ee/spec/mailers/emails/requirements_spec.rb' - 'ee/spec/mailers/emails/user_cap_spec.rb' - 'ee/spec/mailers/license_mailer_spec.rb' @@ -1058,7 +1055,6 @@ RSpec/FeatureCategory: - 'ee/spec/models/ee/members_preloader_spec.rb' - 'ee/spec/models/ee/merge_request/metrics_spec.rb' - 'ee/spec/models/ee/merge_request_diff_spec.rb' - - 'ee/spec/models/ee/namespace/root_storage_statistics_spec.rb' - 'ee/spec/models/ee/namespace_ci_cd_setting_spec.rb' - 'ee/spec/models/ee/namespace_spec.rb' - 'ee/spec/models/ee/namespace_statistics_spec.rb' @@ -1131,8 +1127,6 @@ RSpec/FeatureCategory: - 'ee/spec/models/milestone_spec.rb' - 'ee/spec/models/namespace_limit_spec.rb' - 'ee/spec/models/namespace_setting_spec.rb' - - 'ee/spec/models/namespaces/storage/root_excess_size_spec.rb' - - 'ee/spec/models/namespaces/storage/root_size_spec.rb' - 'ee/spec/models/packages/package_file_spec.rb' - 'ee/spec/models/path_lock_spec.rb' - 'ee/spec/models/plan_spec.rb' @@ -2355,7 +2349,6 @@ RSpec/FeatureCategory: - 'spec/graphql/types/release_type_spec.rb' - 'spec/graphql/types/repository_type_spec.rb' - 'spec/graphql/types/resolvable_interface_spec.rb' - - 'spec/graphql/types/root_storage_statistics_type_spec.rb' - 'spec/graphql/types/saved_reply_type_spec.rb' - 'spec/graphql/types/security/report_types_enum_spec.rb' - 'spec/graphql/types/snippet_type_spec.rb' @@ -2502,7 +2495,6 @@ RSpec/FeatureCategory: - 'spec/helpers/ssh_keys_helper_spec.rb' - 'spec/helpers/startupjs_helper_spec.rb' - 'spec/helpers/stat_anchors_helper_spec.rb' - - 'spec/helpers/storage_helper_spec.rb' - 'spec/helpers/subscribable_banner_helper_spec.rb' - 'spec/helpers/tab_helper_spec.rb' - 'spec/helpers/terms_helper_spec.rb' @@ -4683,7 +4675,6 @@ RSpec/FeatureCategory: - 'spec/models/namespace/aggregation_schedule_spec.rb' - 'spec/models/namespace/detail_spec.rb' - 'spec/models/namespace/package_setting_spec.rb' - - 'spec/models/namespace/root_storage_statistics_spec.rb' - 'spec/models/namespace_ci_cd_setting_spec.rb' - 'spec/models/namespace_statistics_spec.rb' - 'spec/models/namespaces/project_namespace_spec.rb' diff --git a/.rubocop_todo/rspec/file_path.yml b/.rubocop_todo/rspec/file_path.yml index 06832184c04..cf457973e0e 100644 --- a/.rubocop_todo/rspec/file_path.yml +++ b/.rubocop_todo/rspec/file_path.yml @@ -31,6 +31,7 @@ RSpec/FilePath: - 'spec/requests/api/ci/runner/jobs_artifacts_spec.rb' - 'spec/requests/api/ci/runner/jobs_put_spec.rb' - 'spec/requests/api/ci/runner/jobs_request_post_spec.rb' + - 'spec/requests/api/ci/runner/jobs_request_yamls_spec.rb' - 'spec/requests/api/ci/runner/jobs_trace_spec.rb' - 'spec/requests/api/ci/runner/runners_delete_spec.rb' - 'spec/requests/api/ci/runner/runners_post_spec.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index e53a8e9c1eb..c93d718b5a0 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -fc7f05c95d10184fa76b2e613779bce4e46dd190 +d6591534c5cabaaf8b4ebce6d4923b32310ef576 diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue b/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue index f6a375ab94c..ab2fe63ae05 100644 --- a/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue +++ b/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue @@ -92,7 +92,7 @@ export default { 'is-upstream': isUpstream, 'is-downstream': isDownstream, }" - class="linked-pipeline-mini-list gl-display-inline gl-vertical-align-middle" + class="linked-pipeline-mini-list gl-display-inline-flex gl-gap-2 gl-vertical-align-middle" > <ci-icon v-for="pipeline in linkedPipelinesTrimmed" diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json index 308a68544bc..2319ecccd5b 100644 --- a/app/assets/javascripts/editor/schema/ci.json +++ b/app/assets/javascripts/editor/schema/ci.json @@ -509,6 +509,18 @@ "description": "Command or script that should be executed as the container's entrypoint. It will be translated to Docker's --entrypoint option while creating the container. The syntax is similar to Dockerfile's ENTRYPOINT directive, where each shell token is a separate string in the array.", "minItems": 1 }, + "docker": { + "type": "object", + "description": "Options to pass to Runners Docker Executor", + "additionalProperties": false, + "properties": { + "platform": { + "type": "string", + "minLength": 1, + "description": "Image architecture to pull." + } + } + }, "pull_policy": { "markdownDescription": "Specifies how to pull the image in Runner. It can be one of `always`, `never` or `if-not-present`. The default value is `always`. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#imagepull_policy).", "default": "always", @@ -579,6 +591,18 @@ "type": "string" } }, + "docker": { + "type": "object", + "description": "Options to pass to Runners Docker Executor", + "additionalProperties": false, + "properties": { + "platform": { + "type": "string", + "minLength": 1, + "description": "Image architecture to pull." + } + } + }, "pull_policy": { "markdownDescription": "Specifies how to pull the image in Runner. It can be one of `always`, `never` or `if-not-present`. The default value is `always`. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#servicepull_policy).", "default": "always", diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js index f98369c2fde..c4279e9d8e7 100644 --- a/app/assets/javascripts/emoji/index.js +++ b/app/assets/javascripts/emoji/index.js @@ -77,6 +77,8 @@ async function loadEmojiWithNames() { } export async function loadCustomEmojiWithNames() { + const emojiData = { emojis: {}, names: [] }; + if (document.body?.dataset?.groupFullPath && window.gon?.features?.customEmoji) { const client = createApolloClient(); const { data } = await client.query({ @@ -86,26 +88,21 @@ export async function loadCustomEmojiWithNames() { }, }); - return data?.group?.customEmoji?.nodes?.reduce( - (acc, e) => { - // Map the custom emoji into the format of the normal emojis - acc.emojis[e.name] = { - c: 'custom', - d: e.name, - e: undefined, - name: e.name, - src: e.url, - u: 'custom', - }; - acc.names.push(e.name); - - return acc; - }, - { emojis: {}, names: [] }, - ); + data?.group?.customEmoji?.nodes?.forEach((e) => { + // Map the custom emoji into the format of the normal emojis + emojiData.emojis[e.name] = { + c: 'custom', + d: e.name, + e: undefined, + name: e.name, + src: e.url, + u: 'custom', + }; + emojiData.names.push(e.name); + }); } - return { emojis: {}, names: [] }; + return emojiData; } async function prepareEmojiMap() { diff --git a/app/assets/javascripts/organizations/users/components/app.vue b/app/assets/javascripts/organizations/users/components/app.vue index ae22bedd69a..1d5ea895ff0 100644 --- a/app/assets/javascripts/organizations/users/components/app.vue +++ b/app/assets/javascripts/organizations/users/components/app.vue @@ -2,9 +2,13 @@ import { __, s__ } from '~/locale'; import { createAlert } from '~/alert'; import organizationUsersQuery from '../graphql/organization_users.query.graphql'; +import UsersView from './users_view.vue'; export default { name: 'OrganizationsUsersApp', + components: { + UsersView, + }, i18n: { users: __('Users'), loadingPlaceholder: __('Loading'), @@ -25,7 +29,9 @@ export default { return { id: this.organizationGid }; }, update(data) { - return data.organization.organizationUsers.nodes; + return data.organization.organizationUsers.nodes.map(({ badges, user }) => { + return { ...user, badges, email: user.publicEmail }; + }); }, error(error) { createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true }); @@ -43,9 +49,6 @@ export default { <template> <section> <h1 class="gl-my-4 gl-font-size-h-display">{{ $options.i18n.users }}</h1> - <template v-if="loading"> - {{ $options.i18n.loadingPlaceholder }} - </template> - <div data-testid="organization-users">{{ users }}</div> + <users-view :users="users" :loading="loading" /> </section> </template> diff --git a/app/assets/javascripts/organizations/users/components/users_view.vue b/app/assets/javascripts/organizations/users/components/users_view.vue new file mode 100644 index 00000000000..fac353bdaf6 --- /dev/null +++ b/app/assets/javascripts/organizations/users/components/users_view.vue @@ -0,0 +1,30 @@ +<script> +import { GlLoadingIcon } from '@gitlab/ui'; +import UsersTable from '~/vue_shared/components/users_table/users_table.vue'; + +export default { + name: 'UsersView', + components: { + GlLoadingIcon, + UsersTable, + }, + inject: ['paths'], + props: { + users: { + type: Array, + required: false, + default: () => [], + }, + loading: { + type: Boolean, + required: false, + default: false, + }, + }, +}; +</script> + +<template> + <gl-loading-icon v-if="loading" class="gl-mt-5" size="md" /> + <users-table v-else :users="users" :admin-user-path="paths.adminUser" /> +</template> diff --git a/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql b/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql index a0b2a639401..d98ebf9cd26 100644 --- a/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql +++ b/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql @@ -10,6 +10,12 @@ query getOrganizationUsers($id: OrganizationsOrganizationID!) { id user { id + username + avatarUrl + name + publicEmail + createdAt + lastActivityOn } } } diff --git a/app/assets/javascripts/organizations/users/index.js b/app/assets/javascripts/organizations/users/index.js index 76656243075..794ae9e70a6 100644 --- a/app/assets/javascripts/organizations/users/index.js +++ b/app/assets/javascripts/organizations/users/index.js @@ -13,7 +13,9 @@ export const initOrganizationsUsers = () => { defaultClient: createDefaultClient(), }); - const { organizationGid } = convertObjectPropsToCamelCase(el.dataset); + const { organizationGid, paths } = convertObjectPropsToCamelCase(JSON.parse(el.dataset.appData), { + deep: true, + }); return new Vue({ el, @@ -21,6 +23,7 @@ export const initOrganizationsUsers = () => { apolloProvider, provide: { organizationGid, + paths, }, render(createElement) { return createElement(OrganizationsUsersApp); diff --git a/app/assets/javascripts/search/index.js b/app/assets/javascripts/search/index.js index f83130213f2..dfb2c519c28 100644 --- a/app/assets/javascripts/search/index.js +++ b/app/assets/javascripts/search/index.js @@ -15,7 +15,6 @@ export const initSearchApp = () => { const store = createStore({ query, navigation, - useSidebarNavigation: gon.use_new_navigation, searchType, }); diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue index 86a5f5107f8..e0c49412d56 100644 --- a/app/assets/javascripts/search/sidebar/components/app.vue +++ b/app/assets/javascripts/search/sidebar/components/app.vue @@ -2,9 +2,7 @@ // eslint-disable-next-line no-restricted-imports import { mapState, mapGetters } from 'vuex'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue'; import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue'; -import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue'; import SidebarPortal from '~/super_sidebar/components/sidebar_portal.vue'; import { toggleSuperSidebarCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager'; import DomElementListener from '~/vue_shared/components/dom_element_listener.vue'; @@ -37,18 +35,15 @@ export default { ProjectsFilters, NotesFilters, WikiBlobsFilters, - ScopeLegacyNavigation, ScopeSidebarNavigation, SidebarPortal, DomElementListener, - SmallScreenDrawerNavigation, CommitsFilters, MilestonesFilters, }, mixins: [glFeatureFlagsMixin()], computed: { - // useSidebarNavigation refers to whether the new left sidebar navigation is enabled - ...mapState(['useSidebarNavigation', 'searchType']), + ...mapState(['searchType']), ...mapGetters(['currentScope']), showIssuesFilters() { return this.currentScope === SCOPE_ISSUES; @@ -77,12 +72,6 @@ export default { this.glFeatures?.searchProjectWikisHideArchivedProjects ); }, - showScopeNavigation() { - // showScopeNavigation refers to whether the scope navigation should be shown - // while the legacy navigation is being used and there are no search results - // the scope navigation has to be hidden - return Boolean(this.currentScope); - }, }, methods: { toggleFiltersFromSidebar() { @@ -93,7 +82,7 @@ export default { </script> <template> - <section v-if="useSidebarNavigation"> + <section> <dom-element-listener selector="#js-open-mobile-filters" @click="toggleFiltersFromSidebar" /> <sidebar-portal> <scope-sidebar-navigation /> @@ -107,32 +96,4 @@ export default { <wiki-blobs-filters v-if="showWikiBlobsFilters" /> </sidebar-portal> </section> - - <section - v-else-if="showScopeNavigation" - class="gl-display-flex gl-flex-direction-column gl-lg-mr-0 gl-md-mr-5 gl-lg-mb-6 gl-lg-mt-5" - > - <div class="search-sidebar gl-display-none gl-lg-display-block"> - <scope-legacy-navigation /> - <issues-filters v-if="showIssuesFilters" /> - <merge-requests-filters v-if="showMergeRequestFilters" /> - <blobs-filters v-if="showBlobFilters" /> - <projects-filters v-if="showProjectsFilters" /> - <notes-filters v-if="showNotesFilters" /> - <commits-filters v-if="showCommitsFilters" /> - <milestones-filters v-if="showMilestonesFilters" /> - <wiki-blobs-filters v-if="showWikiBlobsFilters" /> - </div> - <small-screen-drawer-navigation class="gl-lg-display-none"> - <scope-legacy-navigation /> - <issues-filters v-if="showIssuesFilters" /> - <merge-requests-filters v-if="showMergeRequestFilters" /> - <blobs-filters v-if="showBlobFilters" /> - <projects-filters v-if="showProjectsFilters" /> - <notes-filters v-if="showNotesFilters" /> - <commits-filters v-if="showCommitsFilters" /> - <milestones-filters v-if="showMilestonesFilters" /> - <wiki-blobs-filters v-if="showWikiBlobsFilters" /> - </small-screen-drawer-navigation> - </section> </template> diff --git a/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue b/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue index b0e84beabc4..914ff99075b 100644 --- a/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue +++ b/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue @@ -21,7 +21,7 @@ export default { tooltip: s__('GlobalSearch|Include search results from archived projects'), }, computed: { - ...mapState(['urlQuery', 'useSidebarNavigation']), + ...mapState(['urlQuery']), selectedFilter: { get() { return [parseBoolean(this.urlQuery?.include_archived)]; @@ -48,7 +48,7 @@ export default { <template> <gl-form-checkbox-group v-model="selectedFilter"> - <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }"> + <h5 class="gl-mt-0 gl-mb-5 gl-font-sm"> {{ $options.archivedFilterData.headerLabel }} </h5> <gl-form-checkbox diff --git a/app/assets/javascripts/search/sidebar/components/blobs_filters.vue b/app/assets/javascripts/search/sidebar/components/blobs_filters.vue index 0ed2c24efba..e282bacae31 100644 --- a/app/assets/javascripts/search/sidebar/components/blobs_filters.vue +++ b/app/assets/javascripts/search/sidebar/components/blobs_filters.vue @@ -1,8 +1,4 @@ <script> -// eslint-disable-next-line no-restricted-imports -import { mapGetters, mapState } from 'vuex'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { HR_DEFAULT_CLASSES } from '../constants'; import LanguageFilter from './language_filter/index.vue'; import ArchivedFilter from './archived_filter/index.vue'; import FiltersTemplate from './filters_template.vue'; @@ -14,24 +10,12 @@ export default { FiltersTemplate, ArchivedFilter, }, - mixins: [glFeatureFlagsMixin()], - computed: { - ...mapGetters(['currentScope']), - ...mapState(['useSidebarNavigation', 'searchType']), - showDivider() { - return !this.useSidebarNavigation; - }, - hrClasses() { - return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block']; - }, - }, }; </script> <template> <filters-template> <language-filter class="gl-mb-5" /> - <hr v-if="showDivider" :class="hrClasses" /> <archived-filter class="gl-mb-5" /> </filters-template> </template> diff --git a/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue b/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue index 176614be6da..4e91158fa36 100644 --- a/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue +++ b/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue @@ -1,6 +1,4 @@ <script> -// eslint-disable-next-line no-restricted-imports -import { mapState } from 'vuex'; import RadioFilter from '../radio_filter.vue'; import { confidentialFilterData } from './data'; @@ -9,9 +7,6 @@ export default { components: { RadioFilter, }, - computed: { - ...mapState(['useSidebarNavigation']), - }, confidentialFilterData, }; </script> diff --git a/app/assets/javascripts/search/sidebar/components/filters_template.vue b/app/assets/javascripts/search/sidebar/components/filters_template.vue index 0f68bf92048..a3aa392d7fc 100644 --- a/app/assets/javascripts/search/sidebar/components/filters_template.vue +++ b/app/assets/javascripts/search/sidebar/components/filters_template.vue @@ -5,7 +5,6 @@ import { mapActions, mapState, mapGetters } from 'vuex'; import Tracking from '~/tracking'; import { - HR_DEFAULT_CLASSES, TRACKING_ACTION_CLICK, TRACKING_LABEL_APPLY, TRACKING_LABEL_RESET, @@ -19,7 +18,7 @@ export default { GlForm, }, computed: { - ...mapState(['sidebarDirty', 'useSidebarNavigation']), + ...mapState(['sidebarDirty']), ...mapGetters(['currentScope']), }, methods: { @@ -37,15 +36,12 @@ export default { this.resetQuery(); }, }, - HR_DEFAULT_CLASSES, }; </script> <template> <gl-form class="issue-filters gl-px-5 gl-pt-0" @submit.prevent="applyQueryWithTracking"> - <hr v-if="!useSidebarNavigation" :class="$options.HR_DEFAULT_CLASSES" /> <slot></slot> - <hr v-if="!useSidebarNavigation" :class="$options.HR_DEFAULT_CLASSES" /> <div class="gl-display-flex gl-align-items-center gl-mt-4"> <gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty"> {{ __('Apply') }} diff --git a/app/assets/javascripts/search/sidebar/components/issues_filters.vue b/app/assets/javascripts/search/sidebar/components/issues_filters.vue index a77fb34cdba..7136959ee0b 100644 --- a/app/assets/javascripts/search/sidebar/components/issues_filters.vue +++ b/app/assets/javascripts/search/sidebar/components/issues_filters.vue @@ -2,7 +2,7 @@ // eslint-disable-next-line no-restricted-imports import { mapGetters, mapState } from 'vuex'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { HR_DEFAULT_CLASSES, SEARCH_TYPE_ADVANCED } from '../constants'; +import { SEARCH_TYPE_ADVANCED } from '../constants'; import { confidentialFilterData } from './confidentiality_filter/data'; import { statusFilterData } from './status_filter/data'; import ConfidentialityFilter from './confidentiality_filter/index.vue'; @@ -26,7 +26,7 @@ export default { mixins: [glFeatureFlagsMixin()], computed: { ...mapGetters(['currentScope']), - ...mapState(['useSidebarNavigation', 'searchType']), + ...mapState(['searchType']), showConfidentialityFilter() { return Object.values(confidentialFilterData.scopes).includes(this.currentScope); }, @@ -43,12 +43,6 @@ export default { showArchivedFilter() { return archivedFilterData.scopes.includes(this.currentScope); }, - showDivider() { - return !this.useSidebarNavigation; - }, - hrClasses() { - return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block']; - }, }, }; </script> @@ -56,11 +50,8 @@ export default { <template> <filters-template> <status-filter v-if="showStatusFilter" class="gl-mb-5" /> - <hr v-if="showConfidentialityFilter && showDivider" :class="hrClasses" /> <confidentiality-filter v-if="showConfidentialityFilter" class="gl-mb-5" /> - <hr v-if="showLabelFilter && showDivider" :class="hrClasses" /> <label-filter v-if="showLabelFilter" class="gl-mb-5" /> - <hr v-if="showArchivedFilter && showDivider" :class="hrClasses" /> <archived-filter v-if="showArchivedFilter" class="gl-mb-5" /> </filters-template> </template> diff --git a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue index 97583730958..a53f519161b 100644 --- a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue +++ b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue @@ -55,7 +55,7 @@ export default { }, i18n: I18N, computed: { - ...mapState(['useSidebarNavigation', 'searchLabelString', 'query', 'urlQuery', 'aggregations']), + ...mapState(['searchLabelString', 'query', 'urlQuery', 'aggregations']), ...mapGetters([ 'filteredLabels', 'filteredUnselectedLabels', @@ -179,11 +179,7 @@ export default { <template> <div class="gl-pb-0 gl-md-pt-0 label-filter gl-relative"> - <h5 - class="gl-my-0" - data-testid="label-filter-title" - :class="{ 'gl-font-sm': useSidebarNavigation }" - > + <h5 class="gl-my-0 gl-font-sm" data-testid="label-filter-title"> {{ $options.labelFilterData.header }} </h5> <div class="gl-my-5"> @@ -246,12 +242,7 @@ export default { v-if="isFocused" v-outside="closeDropdown" data-testid="header-search-dropdown-menu" - class="header-search-dropdown-menu gl-overflow-y-auto gl-absolute gl-w-full gl-bg-white gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-200 gl-shadow-x0-y2-b4-s0 gl-mt-3 gl-z-index-2" - :class="{ - 'gl-max-w-none!': useSidebarNavigation, - 'gl-min-w-full!': useSidebarNavigation, - 'gl-w-full!': useSidebarNavigation, - }" + class="header-search-dropdown-menu gl-overflow-y-auto gl-absolute gl-bg-white gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-200 gl-shadow-x0-y2-b4-s0 gl-mt-3 gl-z-index-2 gl-w-full! gl-min-w-full! gl-max-w-none!" > <div class="header-search-dropdown-content gl-py-2"> <dropdown-keyboard-navigation diff --git a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue index 784207cc702..4a9975641c6 100644 --- a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue +++ b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue @@ -27,7 +27,7 @@ export default { loadError: s__('GlobalSearch|Aggregations load error.'), }, computed: { - ...mapState(['aggregations', 'useSidebarNavigation']), + ...mapState(['aggregations']), ...mapGetters(['languageAggregationBuckets']), hasBuckets() { return this.languageAggregationBuckets.length > 0; @@ -75,7 +75,7 @@ export default { <template> <div v-if="hasBuckets" class="language-filter-checkbox"> - <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }"> + <h5 class="gl-mt-0 gl-mb-5 gl-font-sm"> {{ $options.languageFilterData.header }} </h5> <div diff --git a/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue b/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue index f86906ebd26..18074db7603 100644 --- a/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue +++ b/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue @@ -1,7 +1,6 @@ <script> // eslint-disable-next-line no-restricted-imports -import { mapGetters, mapState } from 'vuex'; -import { HR_DEFAULT_CLASSES } from '../constants'; +import { mapGetters } from 'vuex'; import { statusFilterData } from './status_filter/data'; import StatusFilter from './status_filter/index.vue'; import FiltersTemplate from './filters_template.vue'; @@ -17,19 +16,12 @@ export default { }, computed: { ...mapGetters(['currentScope']), - ...mapState(['useSidebarNavigation', 'searchType']), showArchivedFilter() { return archivedFilterData.scopes.includes(this.currentScope); }, showStatusFilter() { return Object.values(statusFilterData.scopes).includes(this.currentScope); }, - showDivider() { - return !this.useSidebarNavigation; - }, - hrClasses() { - return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block']; - }, }, }; </script> @@ -37,7 +29,6 @@ export default { <template> <filters-template> <status-filter v-if="showStatusFilter" class="gl-mb-5" /> - <hr v-if="showArchivedFilter && showDivider" :class="hrClasses" /> <archived-filter v-if="showArchivedFilter" class="gl-mb-5" /> </filters-template> </template> diff --git a/app/assets/javascripts/search/sidebar/components/radio_filter.vue b/app/assets/javascripts/search/sidebar/components/radio_filter.vue index a1eb5ccecd8..d67844b93a7 100644 --- a/app/assets/javascripts/search/sidebar/components/radio_filter.vue +++ b/app/assets/javascripts/search/sidebar/components/radio_filter.vue @@ -17,7 +17,7 @@ export default { }, }, computed: { - ...mapState(['query', 'useSidebarNavigation']), + ...mapState(['query']), ...mapGetters(['currentScope']), ANY() { return this.filterData.filters.ANY; @@ -57,7 +57,7 @@ export default { <template> <div> - <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }"> + <h5 class="gl-mt-0 gl-mb-5 gl-font-sm"> {{ filterData.header }} </h5> <gl-form-radio-group v-model="selectedFilter"> diff --git a/app/assets/javascripts/search/sidebar/components/scope_legacy_navigation.vue b/app/assets/javascripts/search/sidebar/components/scope_legacy_navigation.vue deleted file mode 100644 index a4c1119736f..00000000000 --- a/app/assets/javascripts/search/sidebar/components/scope_legacy_navigation.vue +++ /dev/null @@ -1,85 +0,0 @@ -<script> -import { GlNav, GlNavItem, GlIcon } from '@gitlab/ui'; -// eslint-disable-next-line no-restricted-imports -import { mapActions, mapState } from 'vuex'; -import { s__ } from '~/locale'; -import Tracking from '~/tracking'; -import { formatSearchResultCount, addCountOverLimit } from '~/search/store/utils'; -import { NAV_LINK_DEFAULT_CLASSES, NAV_LINK_COUNT_DEFAULT_CLASSES } from '../constants'; -import { slugifyWithUnderscore } from '../../../lib/utils/text_utility'; - -export default { - name: 'ScopeLegacyNavigation', - i18n: { - countOverLimitLabel: s__('GlobalSearch|Result count is over limit.'), - }, - components: { - GlNav, - GlNavItem, - GlIcon, - }, - mixins: [Tracking.mixin()], - computed: { - ...mapState(['navigation', 'urlQuery']), - }, - created() { - if (this.urlQuery?.search) { - this.fetchSidebarCount(); - } - }, - methods: { - ...mapActions(['fetchSidebarCount']), - showFormatedCount(countString) { - return formatSearchResultCount(countString); - }, - isCountOverLimit(countString) { - return Boolean(addCountOverLimit(countString)); - }, - handleClick(scope) { - this.track('click_menu_item', { label: `vertical_navigation_${scope}` }); - }, - linkClasses(isHighlighted) { - return [...this.$options.NAV_LINK_DEFAULT_CLASSES, { 'gl-font-weight-bold': isHighlighted }]; - }, - countClasses(isHighlighted) { - return [ - ...this.$options.NAV_LINK_COUNT_DEFAULT_CLASSES, - isHighlighted ? 'gl-text-gray-900' : 'gl-text-gray-500', - ]; - }, - qaSelectorValue(item) { - return `${slugifyWithUnderscore(item.label)}_tab`; - }, - }, - NAV_LINK_DEFAULT_CLASSES, - NAV_LINK_COUNT_DEFAULT_CLASSES, -}; -</script> - -<template> - <nav data-testid="search-filter" class="gl-border-none"> - <gl-nav vertical pills> - <gl-nav-item - v-for="(item, scope) in navigation" - :key="scope" - :link-classes="linkClasses(item.active)" - class="gl-mb-1" - :href="item.link" - :active="item.active" - :data-qa-selector="qaSelectorValue(item)" - :data-testid="qaSelectorValue(item)" - @click="handleClick(scope)" - ><span data-testid="label">{{ item.label }}</span - ><span v-if="item.count" data-testid="count" :class="countClasses(item.active)"> - {{ showFormatedCount(item.count) - }}<gl-icon - v-if="isCountOverLimit(item.count)" - name="plus" - :aria-label="$options.i18n.countOverLimitLabel" - :size="8" - /> - </span> - </gl-nav-item> - </gl-nav> - </nav> -</template> diff --git a/app/assets/javascripts/search/sidebar/components/small_screen_drawer_navigation.vue b/app/assets/javascripts/search/sidebar/components/small_screen_drawer_navigation.vue deleted file mode 100644 index e966b8d877e..00000000000 --- a/app/assets/javascripts/search/sidebar/components/small_screen_drawer_navigation.vue +++ /dev/null @@ -1,61 +0,0 @@ -<script> -import { GlDrawer } from '@gitlab/ui'; -import { getContentWrapperHeight } from '~/lib/utils/dom_utils'; -import { DRAWER_Z_INDEX } from '~/lib/utils/constants'; -import DomElementListener from '~/vue_shared/components/dom_element_listener.vue'; -import { s__ } from '~/locale'; - -export default { - name: 'SmallScreenDrawerNavigation', - components: { - GlDrawer, - DomElementListener, - }, - i18n: { - smallScreenFiltersDrawerHeader: s__('GlobalSearch|Filters'), - }, - data() { - return { - openSmallScreenFilters: false, - }; - }, - computed: { - getDrawerHeaderHeight() { - if (!this.openSmallScreenFilters) return '0'; - return getContentWrapperHeight(); - }, - }, - methods: { - closeSmallScreenFilters() { - this.openSmallScreenFilters = false; - }, - toggleSmallScreenFilters() { - this.openSmallScreenFilters = !this.openSmallScreenFilters; - }, - }, - DRAWER_Z_INDEX, -}; -</script> -<template> - <dom-element-listener selector="#js-open-mobile-filters" @click="toggleSmallScreenFilters"> - <gl-drawer - :header-height="getDrawerHeaderHeight" - :z-index="$options.DRAWER_Z_INDEX" - variant="sidebar" - class="small-screen-drawer-navigation" - :open="openSmallScreenFilters" - @close="closeSmallScreenFilters" - > - <template #title> - <h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24"> - {{ $options.i18n.smallScreenFiltersDrawerHeader }} - </h2> - </template> - <template #default> - <div> - <slot></slot> - </div> - </template> - </gl-drawer> - </dom-element-listener> -</template> diff --git a/app/assets/javascripts/search/sidebar/components/status_filter/index.vue b/app/assets/javascripts/search/sidebar/components/status_filter/index.vue index a5f717dcf06..cbc1a26f1ae 100644 --- a/app/assets/javascripts/search/sidebar/components/status_filter/index.vue +++ b/app/assets/javascripts/search/sidebar/components/status_filter/index.vue @@ -1,5 +1,4 @@ <script> -import { HR_DEFAULT_CLASSES } from '../../constants'; import RadioFilter from '../radio_filter.vue'; import { statusFilterData } from './data'; @@ -9,7 +8,6 @@ export default { RadioFilter, }, statusFilterData, - HR_DEFAULT_CLASSES, }; </script> diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js index 1559155a941..95906c840d7 100644 --- a/app/assets/javascripts/search/sidebar/constants/index.js +++ b/app/assets/javascripts/search/sidebar/constants/index.js @@ -18,8 +18,6 @@ export const NAV_LINK_DEFAULT_CLASSES = [ 'gl-justify-content-space-between', ]; export const NAV_LINK_COUNT_DEFAULT_CLASSES = ['gl-font-sm', 'gl-font-weight-normal']; -export const HR_DEFAULT_CLASSES = ['hr-x', 'gl-border-gray-100']; -export const ONLY_SHOW_MD = ['gl-display-none', 'gl-md-display-block']; export const TRACKING_ACTION_CLICK = 'search:filters:click'; export const TRACKING_LABEL_APPLY = 'Apply Filters'; diff --git a/app/assets/javascripts/search/store/state.js b/app/assets/javascripts/search/store/state.js index b4cd2af65ba..9c38a230343 100644 --- a/app/assets/javascripts/search/store/state.js +++ b/app/assets/javascripts/search/store/state.js @@ -1,7 +1,7 @@ import { cloneDeep } from 'lodash'; import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from './constants'; -const createState = ({ query, navigation, useSidebarNavigation, searchType }) => ({ +const createState = ({ query, navigation, searchType }) => ({ urlQuery: cloneDeep(query), query, groups: [], @@ -14,7 +14,6 @@ const createState = ({ query, navigation, useSidebarNavigation, searchType }) => }, sidebarDirty: false, navigation, - useSidebarNavigation, aggregations: { error: false, fetching: false, diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue index b8618e09761..8f95dd1dcec 100644 --- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue +++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue @@ -39,6 +39,7 @@ export default { i18n: { skipToMainContent: __('Skip to main content'), primaryNavigation: s__('Navigation|Primary navigation'), + adminArea: s__('Navigation|Admin Area'), }, inject: ['showTrialStatusWidget'], props: { @@ -220,6 +221,16 @@ export default { </div> <div class="gl-p-3"> <help-center ref="helpCenter" :sidebar-data="sidebarData" /> + <gl-button + v-if="sidebarData.is_admin" + class="gl-fixed gl-right-0 gl-mr-3 gl-mt-2" + data-testid="sidebar-admin-link" + :href="sidebarData.admin_url" + icon="admin" + size="small" + > + {{ $options.i18n.adminArea }} + </gl-button> </div> </div> </nav> diff --git a/app/assets/stylesheets/page_bundles/search.scss b/app/assets/stylesheets/page_bundles/search.scss index a3a62b44e98..b145d046fa4 100644 --- a/app/assets/stylesheets/page_bundles/search.scss +++ b/app/assets/stylesheets/page_bundles/search.scss @@ -31,26 +31,6 @@ $language-filter-max-height: 20rem; } } -.search-sidebar { - @include media-breakpoint-down(lg) { - max-width: 100%; - } - - @include media-breakpoint-down(xl) { - min-width: $search-sidebar-min-width; - max-width: $search-sidebar-min-width; - } - - @include media-breakpoint-up(xl) { - min-width: $search-sidebar-max-width; - max-width: $search-sidebar-max-width; - } - - .language-filter-max-height { - max-height: $language-filter-max-height; - } -} - .issue-filters { .label-filter { list-style: none; diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss index db20034419a..9d4f6cabcd9 100644 --- a/app/assets/stylesheets/themes/theme_helper.scss +++ b/app/assets/stylesheets/themes/theme_helper.scss @@ -235,16 +235,6 @@ } } - .search-sidebar { - .nav-link { - &.active, - &:hover { - background-color: rgba($gray-50, 0.8); - color: $gray-900; - } - } - } - // Sidebar .nav-sidebar li.active > a { color: $gray-900; diff --git a/app/helpers/organizations/organization_helper.rb b/app/helpers/organizations/organization_helper.rb index 61eb9b5c35f..533d5409ab7 100644 --- a/app/helpers/organizations/organization_helper.rb +++ b/app/helpers/organizations/organization_helper.rb @@ -44,8 +44,9 @@ module Organizations def organization_user_app_data(organization) { - organization_gid: organization.to_global_id - } + organization_gid: organization.to_global_id, + paths: organizations_users_paths + }.to_json end def home_organization_setting_app_data @@ -65,5 +66,12 @@ module Organizations new_project_path: new_project_path } end + + # See UsersHelper#admin_users_paths for inspiration to this method + def organizations_users_paths + { + admin_user: admin_user_path(:id) + } + end end end diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb index f983812ad22..1670e628ffe 100644 --- a/app/helpers/sidebars_helper.rb +++ b/app/helpers/sidebars_helper.rb @@ -72,8 +72,10 @@ module SidebarsHelper def super_sidebar_logged_in_context(user, group:, project:, panel:, panel_type:) # rubocop:disable Metrics/AbcSize super_sidebar_logged_out_context(panel: panel, panel_type: panel_type).merge({ is_logged_in: true, + is_admin: user.can_admin_all_resources?, name: user.name, username: user.username, + admin_url: admin_root_url, avatar_url: user.avatar_url, has_link_to_profile: current_user_menu?(:profile), link_to_profile: user_path(user), diff --git a/app/validators/json_schema_validator.rb b/app/validators/json_schema_validator.rb index 2ef011df73e..b6b21f13546 100644 --- a/app/validators/json_schema_validator.rb +++ b/app/validators/json_schema_validator.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # # JsonSchemaValidator # @@ -24,11 +25,19 @@ class JsonSchemaValidator < ActiveModel::EachValidator end def validate_each(record, attribute, value) - value = value.to_h.stringify_keys if options[:hash_conversion] == true + value = value.to_h.deep_stringify_keys if options[:hash_conversion] == true value = Gitlab::Json.parse(value.to_s) if options[:parse_json] == true && !value.nil? - unless valid_schema?(value) - record.errors.add(attribute, _("must be a valid json schema")) + if options[:detail_errors] + validator.validate(value).each do |error| + message = format( + _("the '%{data_pointer}' must be a valid '%{type}'"), + data_pointer: error['data_pointer'], type: error['type'] + ) + record.errors.add(attribute, message) + end + else + record.errors.add(attribute, _("must be a valid json schema")) unless valid_schema?(value) end end diff --git a/app/views/organizations/organizations/users.html.haml b/app/views/organizations/organizations/users.html.haml index 5fb9d786e0b..0ed15d8e4d0 100644 --- a/app/views/organizations/organizations/users.html.haml +++ b/app/views/organizations/organizations/users.html.haml @@ -1,4 +1,4 @@ - page_title _('Users') -#js-organizations-users{ data: organization_user_app_data(@organization) } +#js-organizations-users{ data: { app_data: organization_user_app_data(@organization) } } diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml index 4fda5379876..55d7a4ed041 100644 --- a/app/views/search/show.html.haml +++ b/app/views/search/show.html.haml @@ -8,7 +8,6 @@ = hidden_field_tag :project_id, params[:project_id] - group_attributes = @group&.attributes&.slice('id', 'name')&.merge(full_name: @group&.full_name) - project_attributes = @project&.attributes&.slice('id', 'namespace_id', 'name')&.merge(name_with_namespace: @project&.name_with_namespace) -- search_bar_classes = !show_super_sidebar? ? 'search-sidebar gl-display-flex gl-flex-direction-column gl-mr-4' : '' - if @search_results && !(@search_results.respond_to?(:failed?) && @search_results.failed?) - if @search_service_presenter.without_count? @@ -22,6 +21,6 @@ #js-search-topbar{ data: { "group-initial-json": group_attributes.to_json, "project-initial-json": project_attributes.to_json, "default-branch-name": @project&.default_branch } } .results.gl-lg-display-flex.gl-mt-0 - #js-search-sidebar{ class: search_bar_classes, data: { navigation_json: search_navigation_json, search_type: search_service.search_type } } + #js-search-sidebar{ data: { navigation_json: search_navigation_json, search_type: search_service.search_type } } - if @search_term = render 'search/results' diff --git a/config/feature_flags/development/ci_catalog_create_metadata.yml b/config/feature_flags/development/ci_catalog_create_metadata.yml index a73f499554d..205e7e0c414 100644 --- a/config/feature_flags/development/ci_catalog_create_metadata.yml +++ b/config/feature_flags/development/ci_catalog_create_metadata.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/430120 milestone: '16.6' type: development group: group::pipeline authoring -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/code_tasks.yml b/config/feature_flags/development/code_tasks.yml deleted file mode 100644 index fec0e8326f3..00000000000 --- a/config/feature_flags/development/code_tasks.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: code_tasks -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135717 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/430962 -milestone: '16.6' -type: development -group: group::code creation -default_enabled: false diff --git a/db/click_house/migrate/20230724064832_create_contribution_analytics_events.rb b/db/click_house/migrate/20230724064832_create_contribution_analytics_events.rb index 2606ae3adc9..992d266fda7 100644 --- a/db/click_house/migrate/20230724064832_create_contribution_analytics_events.rb +++ b/db/click_house/migrate/20230724064832_create_contribution_analytics_events.rb @@ -13,7 +13,7 @@ class CreateContributionAnalyticsEvents < ClickHouse::Migration created_at Date DEFAULT toDate(now()), updated_at DateTime64(6, 'UTC') DEFAULT now() ) - ENGINE = MergeTree + ENGINE = ReplacingMergeTree ORDER BY (path, created_at, author_id, id) PARTITION BY toYear(created_at); SQL diff --git a/doc/administration/backup_restore/backup_gitlab.md b/doc/administration/backup_restore/backup_gitlab.md index 12d63232068..9cb01910816 100644 --- a/doc/administration/backup_restore/backup_gitlab.md +++ b/doc/administration/backup_restore/backup_gitlab.md @@ -362,13 +362,13 @@ Caveats: - The resultant filenames will still end in `.gz`. - The default decompression command, used during restore, is `gzip -cd`. Therefore if you override the compression command to use a format that cannot be decompressed by `gzip -cd`, you must override the decompression command during restore. -### Default compression: Gzip with fastest method +##### Default compression: Gzip with fastest method ```shell gitlab-backup create ``` -### Gzip with slowest method +##### Gzip with slowest method ```shell gitlab-backup create COMPRESS_CMD="gzip -c --best" @@ -380,7 +380,7 @@ If `gzip` was used for backup, then restore does not require any options: gitlab-backup restore ``` -### No compression +##### No compression If your backup destination has built-in automatic compression, then you may wish to skip compression. @@ -396,7 +396,7 @@ And on restore: gitlab-backup restore DECOMPRESS_CMD=tee ``` -### Replace Gzip +##### Replace Gzip This is an example of how to use a compression tool which you installed manually: diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index ea7d59669a6..6edf78aeec2 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -29682,7 +29682,6 @@ Representation of mergeability check identifier. | <a id="mergeabilitycheckidentifierneed_rebase"></a>`NEED_REBASE` | Mergeability check identifier is need_rebase. | | <a id="mergeabilitycheckidentifiernot_approved"></a>`NOT_APPROVED` | Mergeability check identifier is not_approved. | | <a id="mergeabilitycheckidentifiernot_open"></a>`NOT_OPEN` | Mergeability check identifier is not_open. | -| <a id="mergeabilitycheckidentifierpolicies_denied"></a>`POLICIES_DENIED` | Mergeability check identifier is policies_denied. | | <a id="mergeabilitycheckidentifierstatus_checks_must_pass"></a>`STATUS_CHECKS_MUST_PASS` | Mergeability check identifier is status_checks_must_pass. | ### `MergeabilityCheckStatus` diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index f89bf54b979..a5515160b76 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -2404,6 +2404,37 @@ image: - [Override the entrypoint of an image](../docker/using_docker_images.md#override-the-entrypoint-of-an-image). +#### `image:docker` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/27919) in GitLab 16.7. Requires GitLab Runner 16.7 or later. + +Use `image:docker` to pass options to the Docker executor of a GitLab Runner. + +**Keyword type**: Job keyword. You can use it only as part of a job or in the +[`default` section](#default). + +**Possible inputs**: + +A hash of options for the Docker executor, which can include: + +- `platform`: Selects the architecture of the image to pull. When not specified, + the default is the same platform as the host runner. + +**Example of `image:docker`**: + +```yaml +arm-sql-job: + script: echo "Run sql tests" + image: + name: super/sql:experimental + docker: + platform: arm64/v8 +``` + +**Additional details**: + +- `image:docker:platform` maps to the [`docker pull --platform` option](https://docs.docker.com/engine/reference/commandline/pull/#options). + #### `image:pull_policy` > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/21619) in GitLab 15.1 [with a flag](../../administration/feature_flags.md) named `ci_docker_image_pull_policy`. Disabled by default. @@ -4220,6 +4251,38 @@ In this example, GitLab launches two containers for the job: - [Run your CI/CD jobs in Docker containers](../docker/using_docker_images.md). - [Use Docker to build Docker images](../docker/using_docker_build.md). +#### `services:docker` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/27919) in GitLab 16.7. Requires GitLab Runner 16.7 or later. + +Use `services:docker` to pass options to the Docker executor of a GitLab Runner. + +**Keyword type**: Job keyword. You can use it only as part of a job or in the +[`default` section](#default). + +**Possible inputs**: + +A hash of options for the Docker executor, which can include: + +- `platform`: Selects the architecture of the image to pull. When not specified, + the default is the same platform as the host runner. + +**Example of `service:docker`**: + +```yaml +arm-sql-job: + script: echo "Run sql tests in service container" + image: ruby:2.6 + services: + - name: super/sql:experimental + docker: + platform: arm64/v8 +``` + +**Additional details**: + +- `services:docker:platform` maps to the [`docker pull --platform` option](https://docs.docker.com/engine/reference/commandline/pull/#options). + #### `service:pull_policy` > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/21619) in GitLab 15.1 [with a flag](../../administration/feature_flags.md) named `ci_docker_image_pull_policy`. Disabled by default. diff --git a/doc/tutorials/website_project_with_analytics/index.md b/doc/tutorials/website_project_with_analytics/index.md index 90e85d0f88c..156ebad11d3 100644 --- a/doc/tutorials/website_project_with_analytics/index.md +++ b/doc/tutorials/website_project_with_analytics/index.md @@ -70,7 +70,7 @@ To invite a user to the `My website` project: 1. Select **Invite**. The invited user should now be a member of the project. -You can [view, filter, and search for members](../../user/project/members/index.md#filter-and-sort-members) of your project. +You can [view, filter, and search for members](../../user/project/members/index.md#filter-and-sort-project-members) of your project. ## Create project labels diff --git a/doc/update/index.md b/doc/update/index.md index 169805ed79e..284b538f086 100644 --- a/doc/update/index.md +++ b/doc/update/index.md @@ -195,7 +195,7 @@ When upgrading: - GitLab 16: [`16.0.x`](versions/gitlab_16_changes.md#1600) (only instances with [lots of users](versions/gitlab_16_changes.md#long-running-user-type-data-change) or [large pipeline variables history](versions/gitlab_16_changes.md#1610)) > - [`16.1`](versions/gitlab_16_changes.md#1610)(instances with NPM packages in their package registry) > + [`16.1`](versions/gitlab_16_changes.md#1610) (instances with NPM packages in their package registry) > [`16.2.x`](versions/gitlab_16_changes.md#1620) (only instances with [large pipeline variables history](versions/gitlab_16_changes.md#1630)) > [`16.3`](versions/gitlab_16_changes.md#1630) > [latest `16.Y.Z`](https://gitlab.com/gitlab-org/gitlab/-/releases). diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index 683ba6ad19b..40fa32a93db 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -961,7 +961,7 @@ this information is removed from the resulting merged file. ## Versioning and release process -Check the [Release Process documentation](https://gitlab.com/gitlab-org/security-products/release/blob/master/docs/release_process.md). +Check the [Release Process documentation](../../../development/sec/analyzer_development_guide.md#versioning-and-release-process). ## Contributing to the vulnerability database diff --git a/doc/user/okrs.md b/doc/user/okrs.md index ca5882da22a..3dadc406de2 100644 --- a/doc/user/okrs.md +++ b/doc/user/okrs.md @@ -54,10 +54,6 @@ To learn how to create better OKRs and how we use them at GitLab, see the ## Create an objective -Prerequisites: - -- You must have at least the Guest role for the project. - To create an objective: 1. On the left sidebar, select **Search or go to** and find your project. @@ -529,11 +525,11 @@ Prerequisite: To link an item to an objective or key result: 1. In the **Linked items** section of an objective or key result, - select the **Add** button. + select **Add**. 1. Select the relationship between the two items. Either: - - **relates to** - - **blocks** - - **is blocked by** + - **Relates to** + - **Blocks** + - **Is blocked by** 1. Enter the search text of the item. 1. When you have added all the items to be linked, select **Add** below the search box. diff --git a/doc/user/project/members/img/project_members_filter_direct_v14_4.png b/doc/user/project/members/img/project_members_filter_direct_v14_4.png Binary files differdeleted file mode 100644 index 79cee06bc30..00000000000 --- a/doc/user/project/members/img/project_members_filter_direct_v14_4.png +++ /dev/null diff --git a/doc/user/project/members/img/project_members_filter_inherited_v14_4.png b/doc/user/project/members/img/project_members_filter_inherited_v14_4.png Binary files differdeleted file mode 100644 index ce2a0ebf088..00000000000 --- a/doc/user/project/members/img/project_members_filter_inherited_v14_4.png +++ /dev/null diff --git a/doc/user/project/members/img/project_members_search_v14_4.png b/doc/user/project/members/img/project_members_search_v14_4.png Binary files differdeleted file mode 100644 index 8c52c5788d4..00000000000 --- a/doc/user/project/members/img/project_members_search_v14_4.png +++ /dev/null diff --git a/doc/user/project/members/img/project_members_sort_v14_4.png b/doc/user/project/members/img/project_members_sort_v14_4.png Binary files differdeleted file mode 100644 index 20834b9307e..00000000000 --- a/doc/user/project/members/img/project_members_sort_v14_4.png +++ /dev/null diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md index 6df33a4fb06..a008bfdebae 100644 --- a/doc/user/project/members/index.md +++ b/doc/user/project/members/index.md @@ -189,7 +189,7 @@ To add a group to a project: From that date onward, the group can no longer access the project. 1. Select **Invite**. -The members of the group are not displayed on the **Members** tab. +The members of the invited group are not displayed on the **Members** tab. Private groups are masked from unauthorized users. The **Members** tab shows: @@ -210,7 +210,7 @@ If the importing member's role in the target project is: - Maintainer, then members with the Owner role in the source project are imported with the Maintainer role. - Owner, then members with the Owner role in the source project are imported with the Owner role. -To import users: +To import a project's members: 1. On the left sidebar, select **Search or go to** and find your project. 1. Select **Manage > Members**. @@ -218,7 +218,8 @@ To import users: 1. Select the project. You can view only the projects for which you're a maintainer. 1. Select **Import project members**. -After the success message displays, refresh the page to view the new members. +If the import is successful, a success message is displayed. +To view the imported members, refresh the page. ## Remove a member from a project @@ -264,7 +265,7 @@ To avoid this problem, GitLab administrators can: - Remove the malicious user account. - Change the password for the malicious user account. -## Filter and sort members +## Filter and sort project members > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/21727) in GitLab 12.6. > - [Improved](https://gitlab.com/groups/gitlab-org/-/epics/4901) in GitLab 13.9. @@ -279,8 +280,6 @@ You can filter and sort members in a project. 1. In the **Filter members** box, select `Membership` `=` `Inherited`. 1. Press <kbd>Enter</kbd>. -![Project members filter inherited](img/project_members_filter_inherited_v14_4.png) - ### Display direct members 1. On the left sidebar, select **Search or go to** and find your project. @@ -288,19 +287,31 @@ You can filter and sort members in a project. 1. In the **Filter members** box, select `Membership` `=` `Direct`. 1. Press <kbd>Enter</kbd>. -![Project members filter direct](img/project_members_filter_direct_v14_4.png) +### Search for members in a project -### Search +To search for a project member: -You can search for members by name, username, or email. +1. On the left sidebar, select **Search or go to** and find your project. +1. Select **Manage > Members**. +1. In the search box, enter the member's name, username, or email. +1. Press <kbd>Enter</kbd>. -![Project members search](img/project_members_search_v14_4.png) +### Sort members in a project -### Sort +You can sort members in ascending or descending order by: -You can sort members by **Account**, **Access granted**, **Max role**, or **Last sign-in** in ascending or descending order. +- **Account** name +- **Access granted** date +- **Max role** the members have in the group +- **User created** date +- **Last activity** date +- **Last sign-in** date -![Project members sort](img/project_members_sort_v14_4.png) +To sort members: + +1. On the left sidebar, select **Search or go to** and find your project. +1. Select **Manage > Members**. +1. At the top of the member list, from the dropdown list, select the item you want to sort by. ## Request access to a project @@ -319,8 +330,13 @@ Project maintainers cannot approve Owner role access requests. If a project does not have any direct owners or maintainers, the notification is sent to the most recently active owners of the project's group. -If you change your mind before your request is approved, select -**Withdraw Access Request**. +### Withdraw an access request to a project + +You can withdraw an access request to a project before the request is approved. +To withdraw the access request: + +1. On the left sidebar, select **Search or go to** and find the project you requested access to. +1. Next to the project name, select **Withdraw Access Request**. ## Prevent users from requesting access to a project diff --git a/lib/api/entities/ci/job_request/image.rb b/lib/api/entities/ci/job_request/image.rb index 92d68269265..08b0075a657 100644 --- a/lib/api/entities/ci/job_request/image.rb +++ b/lib/api/entities/ci/job_request/image.rb @@ -8,6 +8,7 @@ module API expose :name, :entrypoint expose :ports, using: Entities::Ci::JobRequest::Port + expose :executor_opts expose :pull_policy end end diff --git a/lib/api/entities/ci/job_request/service.rb b/lib/api/entities/ci/job_request/service.rb index 128591058fe..ae726a6b888 100644 --- a/lib/api/entities/ci/job_request/service.rb +++ b/lib/api/entities/ci/job_request/service.rb @@ -8,6 +8,7 @@ module API expose :name, :entrypoint expose :ports, using: Entities::Ci::JobRequest::Port + expose :executor_opts expose :pull_policy expose :alias, :command expose :variables diff --git a/lib/gitlab/ci/build/image.rb b/lib/gitlab/ci/build/image.rb index 84f8eae8deb..660d7701a8f 100644 --- a/lib/gitlab/ci/build/image.rb +++ b/lib/gitlab/ci/build/image.rb @@ -4,7 +4,7 @@ module Gitlab module Ci module Build class Image - attr_reader :alias, :command, :entrypoint, :name, :ports, :variables, :pull_policy + attr_reader :alias, :command, :entrypoint, :name, :ports, :variables, :executor_opts, :pull_policy class << self def from_image(job) @@ -28,6 +28,7 @@ module Gitlab when String @name = image @ports = [] + @executor_opts = {} when Hash @alias = image[:alias] @command = image[:command] @@ -35,6 +36,7 @@ module Gitlab @name = image[:name] @ports = build_ports(image).select(&:valid?) @variables = build_variables(image) + @executor_opts = image.fetch(:executor_opts, {}) @pull_policy = image[:pull_policy] end end diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb index 84e31ca1fc6..58ab488d833 100644 --- a/lib/gitlab/ci/config/entry/image.rb +++ b/lib/gitlab/ci/config/entry/image.rb @@ -13,21 +13,6 @@ module Gitlab validations do validates :config, allowed_keys: IMAGEABLE_ALLOWED_KEYS end - - def value - if string? - { name: @config } - elsif hash? - { - name: @config[:name], - entrypoint: @config[:entrypoint], - ports: (ports_value if ports_defined?), - pull_policy: pull_policy_value - }.compact - else - {} - end - end end end end diff --git a/lib/gitlab/ci/config/entry/imageable.rb b/lib/gitlab/ci/config/entry/imageable.rb index 1aecfee9ab9..53b810b3037 100644 --- a/lib/gitlab/ci/config/entry/imageable.rb +++ b/lib/gitlab/ci/config/entry/imageable.rb @@ -12,7 +12,9 @@ module Gitlab include ::Gitlab::Config::Entry::Attributable include ::Gitlab::Config::Entry::Configurable - IMAGEABLE_ALLOWED_KEYS = %i[name entrypoint ports pull_policy].freeze + EXECUTOR_OPTS_KEYS = %i[docker].freeze + + IMAGEABLE_ALLOWED_KEYS = EXECUTOR_OPTS_KEYS + %i[name entrypoint ports pull_policy].freeze included do include ::Gitlab::Config::Entry::Validatable @@ -23,9 +25,15 @@ module Gitlab validates :name, type: String, presence: true validates :entrypoint, array_of_strings: true, allow_nil: true + validates :executor_opts, json_schema: { + base_directory: "lib/gitlab/ci/config/entry/schemas/imageable", + detail_errors: true, + filename: "executor_opts", + hash_conversion: true + }, allow_nil: true end - attributes :ports, :pull_policy + attributes :docker, :ports, :pull_policy entry :ports, Entry::Ports, description: 'Ports used to expose the image/service' @@ -49,6 +57,28 @@ module Gitlab def skip_config_hash_validation? true end + + def executor_opts + return unless config.is_a?(Hash) + + config.slice(*EXECUTOR_OPTS_KEYS).compact.presence + end + + def value + if string? + { name: config } + elsif hash? + { + name: config[:name], + entrypoint: config[:entrypoint], + executor_opts: executor_opts, + ports: (ports_value if ports_defined?), + pull_policy: pull_policy_value + }.compact + else + {} + end + end end end end diff --git a/lib/gitlab/ci/config/entry/schemas/imageable/executor_opts.json b/lib/gitlab/ci/config/entry/schemas/imageable/executor_opts.json new file mode 100644 index 00000000000..a31374650e6 --- /dev/null +++ b/lib/gitlab/ci/config/entry/schemas/imageable/executor_opts.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Describe `image:` and `service:` options like `docker:`", + "type": "object", + "properties": { + "docker": { + "type": "object", + "properties": { + "platform": { + "type": "string", + "minLength": 1, + "maxLength": 64 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb index 4b3a9990df4..1482ec3dd2b 100644 --- a/lib/gitlab/ci/config/entry/service.rb +++ b/lib/gitlab/ci/config/entry/service.rb @@ -34,14 +34,13 @@ module Gitlab end def value - if string? - { name: @config } - elsif hash? - @config.merge( - pull_policy: pull_policy_value + if hash? + super.merge( + command: @config[:command], + alias: @config[:alias] ).compact else - {} + super end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1ed622246ed..ab7cca65467 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -58328,6 +58328,9 @@ msgstr "" msgid "test case" msgstr "" +msgid "the '%{data_pointer}' must be a valid '%{type}'" +msgstr "" + msgid "the correct format." msgstr "" diff --git a/qa/qa/page/search/results.rb b/qa/qa/page/search/results.rb index 1f30cb3b756..5d856875e0b 100644 --- a/qa/qa/page/search/results.rb +++ b/qa/qa/page/search/results.rb @@ -4,11 +4,6 @@ module QA module Page module Search class Results < QA::Page::Base - view 'app/assets/javascripts/search/sidebar/components/scope_legacy_navigation.vue' do - element :code_tab, ':data-qa-selector="qaSelectorValue(item)"' # rubocop:disable QA/ElementWithPattern - element :projects_tab, ':data-qa-selector="qaSelectorValue(item)"' # rubocop:disable QA/ElementWithPattern - end - view 'app/views/search/results/_blob_data.html.haml' do element :result_item_content element :file_title_content diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/services.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/services.yml index 6761a603a0a..6307ff31cf9 100644 --- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/services.yml +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/services.yml @@ -36,3 +36,31 @@ empty_pull_policy: services: - name: postgres:11.6 pull_policy: [] + +invalid_image_platform: + script: echo "Specifying platform." + image: + name: alpine:latest + docker: + platform: ["arm64"] # The expected value is a string, not an array + +invalid_image_executor_opts: + script: echo "Specifying platform." + image: + name: alpine:latest + docker: + unknown_key: test + +invalid_service_executor_opts: + script: echo "Specifying platform." + services: + - name: mysql:5.7 + docker: + unknown_key: test + +invalid_service_platform: + script: echo "Specifying platform." + services: + - name: mysql:5.7 + docker: + platform: ["arm64"] # The expected value is a string, not an array diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/services.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/services.yml index 8a0f59d1dfd..aebf928613f 100644 --- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/services.yml +++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/services.yml @@ -29,3 +29,17 @@ pull_policy_array: services: - name: postgres:11.6 pull_policy: [always, if-not-present] + +image_platform_string: + script: echo "Specifying platform." + services: + - name: mysql:5.7 + docker: + platform: arm64 + +services_platform_string: + script: echo "Specifying platform." + image: + name: alpine:latest + docker: + platform: arm64 diff --git a/spec/frontend/emoji/index_spec.js b/spec/frontend/emoji/index_spec.js index 7d6a45fbf30..577b7bc726e 100644 --- a/spec/frontend/emoji/index_spec.js +++ b/spec/frontend/emoji/index_spec.js @@ -925,7 +925,7 @@ describe('emoji', () => { window.gon = {}; }); - it('returns empty object', async () => { + it('returns empty emoji data', async () => { const result = await loadCustomEmojiWithNames(); expect(result).toEqual({ emojis: {}, names: [] }); @@ -937,7 +937,28 @@ describe('emoji', () => { delete document.body.dataset.groupFullPath; }); - it('returns empty object', async () => { + it('returns empty emoji data', async () => { + const result = await loadCustomEmojiWithNames(); + + expect(result).toEqual({ emojis: {}, names: [] }); + }); + }); + + describe('when GraphQL request returns null data', () => { + beforeEach(() => { + mockClient = createMockClient([ + [ + customEmojiQuery, + jest.fn().mockResolvedValue({ + data: { + group: null, + }, + }), + ], + ]); + }); + + it('returns empty emoji data', async () => { const result = await loadCustomEmojiWithNames(); expect(result).toEqual({ emojis: {}, names: [] }); @@ -945,7 +966,7 @@ describe('emoji', () => { }); describe('when in a group with flag enabled', () => { - it('returns empty object', async () => { + it('returns emoji data', async () => { const result = await loadCustomEmojiWithNames(); expect(result).toEqual({ diff --git a/spec/frontend/organizations/users/components/app_spec.js b/spec/frontend/organizations/users/components/app_spec.js index b30fd984099..e7ed712c309 100644 --- a/spec/frontend/organizations/users/components/app_spec.js +++ b/spec/frontend/organizations/users/components/app_spec.js @@ -6,7 +6,8 @@ import waitForPromises from 'helpers/wait_for_promises'; import { createAlert } from '~/alert'; import organizationUsersQuery from '~/organizations/users/graphql/organization_users.query.graphql'; import OrganizationsUsersApp from '~/organizations/users/components/app.vue'; -import { MOCK_ORGANIZATION_GID, MOCK_USERS } from '../mock_data'; +import OrganizationsUsersView from '~/organizations/users/components/users_view.vue'; +import { MOCK_ORGANIZATION_GID, MOCK_USERS, MOCK_USERS_FORMATTED } from '../mock_data'; jest.mock('~/alert'); @@ -40,31 +41,26 @@ describe('OrganizationsUsersApp', () => { mockApollo = null; }); - const findOrganizationUsersLoading = () => wrapper.findByText('Loading'); - const findOrganizationUsers = () => wrapper.findByTestId('organization-users'); + const findOrganizationUsersView = () => wrapper.findComponent(OrganizationsUsersView); describe.each` - description | mockResolver | loading | userData | error - ${'when API call is loading'} | ${loadingResolver} | ${true} | ${[]} | ${false} - ${'when API returns successful with results'} | ${successfulResolver(MOCK_USERS)} | ${false} | ${MOCK_USERS} | ${false} - ${'when API returns successful without results'} | ${successfulResolver([])} | ${false} | ${[]} | ${false} - ${'when API returns error'} | ${errorResolver} | ${false} | ${[]} | ${true} + description | mockResolver | loading | userData | error + ${'when API call is loading'} | ${loadingResolver} | ${true} | ${[]} | ${false} + ${'when API returns successful with results'} | ${successfulResolver(MOCK_USERS)} | ${false} | ${MOCK_USERS_FORMATTED} | ${false} + ${'when API returns successful without results'} | ${successfulResolver([])} | ${false} | ${[]} | ${false} + ${'when API returns error'} | ${errorResolver} | ${false} | ${[]} | ${true} `('$description', ({ mockResolver, loading, userData, error }) => { beforeEach(async () => { createComponent(mockResolver); await waitForPromises(); }); - it(`does ${ - loading ? '' : 'not ' - }render the organization users view with loading placeholder`, () => { - expect(findOrganizationUsersLoading().exists()).toBe(loading); + it(`renders OrganizationUsersView with loading prop set to ${loading}`, () => { + expect(findOrganizationUsersView().props('loading')).toBe(loading); }); - it(`renders the organization users view with ${ - userData.length ? 'correct' : 'empty' - } users array raw data`, () => { - expect(JSON.parse(findOrganizationUsers().text())).toStrictEqual(userData); + it('renders OrganizationUsersView with correct users prop', () => { + expect(findOrganizationUsersView().props('users')).toStrictEqual(userData); }); it(`does ${error ? '' : 'not '}render an error message`, () => { diff --git a/spec/frontend/organizations/users/components/users_view_spec.js b/spec/frontend/organizations/users/components/users_view_spec.js new file mode 100644 index 00000000000..5f47e18edd8 --- /dev/null +++ b/spec/frontend/organizations/users/components/users_view_spec.js @@ -0,0 +1,44 @@ +import { GlLoadingIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import UsersView from '~/organizations/users/components/users_view.vue'; +import UsersTable from '~/vue_shared/components/users_table/users_table.vue'; +import { MOCK_PATHS, MOCK_USERS_FORMATTED } from '../mock_data'; + +describe('UsersView', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(UsersView, { + propsData: { + loading: false, + users: MOCK_USERS_FORMATTED, + ...props, + }, + provide: { + paths: MOCK_PATHS, + }, + }); + }; + + const findGlLoading = () => wrapper.findComponent(GlLoadingIcon); + const findUsersTable = () => wrapper.findComponent(UsersTable); + + describe.each` + description | loading | usersData + ${'when loading'} | ${true} | ${[]} + ${'when not loading and has users'} | ${false} | ${MOCK_USERS_FORMATTED} + ${'when not loading and has no users'} | ${false} | ${[]} + `('$description', ({ loading, usersData }) => { + beforeEach(() => { + createComponent({ loading, users: usersData }); + }); + + it(`does ${loading ? '' : 'not '}render loading icon`, () => { + expect(findGlLoading().exists()).toBe(loading); + }); + + it(`does ${!loading ? '' : 'not '}render users table`, () => { + expect(findUsersTable().exists()).toBe(!loading); + }); + }); +}); diff --git a/spec/frontend/organizations/users/mock_data.js b/spec/frontend/organizations/users/mock_data.js index 4f159c70c2c..b6ca00bed79 100644 --- a/spec/frontend/organizations/users/mock_data.js +++ b/spec/frontend/organizations/users/mock_data.js @@ -1,15 +1,31 @@ +const createUser = (id) => { + return { + id: `gid://gitlab/User/${id}`, + username: `test_user_${id}`, + avatarUrl: `/path/test_user_${id}`, + name: `Test User ${id}`, + publicEmail: `test_user_${id}@gitlab.com`, + createdAt: Date.now(), + lastActivityOn: Date.now(), + }; +}; + export const MOCK_ORGANIZATION_GID = 'gid://gitlab/Organizations::Organization/1'; +export const MOCK_PATHS = { + adminUser: '/admin/users/:id', +}; + export const MOCK_USERS = [ { badges: [], id: 'gid://gitlab/Organizations::OrganizationUser/3', - user: { id: 'gid://gitlab/User/3' }, + user: createUser(3), }, { badges: [], id: 'gid://gitlab/Organizations::OrganizationUser/2', - user: { id: 'gid://gitlab/User/2' }, + user: createUser(2), }, { badges: [ @@ -17,6 +33,10 @@ export const MOCK_USERS = [ { text: "It's you!", variant: 'muted' }, ], id: 'gid://gitlab/Organizations::OrganizationUser/1', - user: { id: 'gid://gitlab/User/1' }, + user: createUser(1), }, ]; + +export const MOCK_USERS_FORMATTED = MOCK_USERS.map(({ badges, user }) => { + return { ...user, badges, email: user.publicEmail }; +}); diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js index c2d88493d71..9413927ac26 100644 --- a/spec/frontend/search/sidebar/components/app_spec.js +++ b/spec/frontend/search/sidebar/components/app_spec.js @@ -18,8 +18,6 @@ import NotesFilters from '~/search/sidebar/components/notes_filters.vue'; import CommitsFilters from '~/search/sidebar/components/commits_filters.vue'; import MilestonesFilters from '~/search/sidebar/components/milestones_filters.vue'; import WikiBlobsFilters from '~/search/sidebar/components/wiki_blobs_filters.vue'; -import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue'; -import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue'; import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue'; import DomElementListener from '~/vue_shared/components/dom_element_listener.vue'; @@ -62,8 +60,6 @@ describe('GlobalSearchSidebar', () => { const findCommitsFilters = () => wrapper.findComponent(CommitsFilters); const findMilestonesFilters = () => wrapper.findComponent(MilestonesFilters); const findWikiBlobsFilters = () => wrapper.findComponent(WikiBlobsFilters); - const findScopeLegacyNavigation = () => wrapper.findComponent(ScopeLegacyNavigation); - const findSmallScreenDrawerNavigation = () => wrapper.findComponent(SmallScreenDrawerNavigation); const findScopeSidebarNavigation = () => wrapper.findComponent(ScopeSidebarNavigation); const findDomElementListener = () => wrapper.findComponent(DomElementListener); @@ -129,46 +125,27 @@ describe('GlobalSearchSidebar', () => { }); }); - describe.each` - currentScope | sidebarNavShown | legacyNavShown - ${'issues'} | ${false} | ${true} - ${'test'} | ${false} | ${true} - ${'issues'} | ${true} | ${false} - ${'test'} | ${true} | ${false} - `( - 'renders navigation for scope $currentScope', - ({ currentScope, sidebarNavShown, legacyNavShown }) => { - beforeEach(() => { - getterSpies.currentScope = jest.fn(() => currentScope); - createComponent({ useSidebarNavigation: sidebarNavShown }); - }); - - it(`renders navigation correctly with legacyNavShown ${legacyNavShown}`, () => { - expect(findScopeLegacyNavigation().exists()).toBe(legacyNavShown); - expect(findSmallScreenDrawerNavigation().exists()).toBe(legacyNavShown); - }); - - it(`renders navigation correctly with sidebarNavShown ${sidebarNavShown}`, () => { - expect(findScopeSidebarNavigation().exists()).toBe(sidebarNavShown); - }); - }, - ); - }); + describe.each(['issues', 'test'])('for scope %p', (currentScope) => { + beforeEach(() => { + getterSpies.currentScope = jest.fn(() => currentScope); + createComponent(); + }); - describe('when useSidebarNavigation=true', () => { - beforeEach(() => { - createComponent({ useSidebarNavigation: true }); + it(`renders navigation correctly`, () => { + expect(findScopeSidebarNavigation().exists()).toBe(true); + }); }); + }); - it('toggles super sidebar when button is clicked', () => { - const elListener = findDomElementListener(); + it('toggles super sidebar when button is clicked', () => { + createComponent(); + const elListener = findDomElementListener(); - expect(toggleSuperSidebarCollapsed).not.toHaveBeenCalled(); + expect(toggleSuperSidebarCollapsed).not.toHaveBeenCalled(); - elListener.vm.$emit('click'); + elListener.vm.$emit('click'); - expect(toggleSuperSidebarCollapsed).toHaveBeenCalledTimes(1); - expect(elListener.props('selector')).toBe('#js-open-mobile-filters'); - }); + expect(toggleSuperSidebarCollapsed).toHaveBeenCalledTimes(1); + expect(elListener.props('selector')).toBe('#js-open-mobile-filters'); }); }); diff --git a/spec/frontend/search/sidebar/components/blobs_filters_spec.js b/spec/frontend/search/sidebar/components/blobs_filters_spec.js index 245ddb8f8bb..3f1feae8527 100644 --- a/spec/frontend/search/sidebar/components/blobs_filters_spec.js +++ b/spec/frontend/search/sidebar/components/blobs_filters_spec.js @@ -17,13 +17,11 @@ describe('GlobalSearch BlobsFilters', () => { currentScope: () => 'blobs', }; - const createComponent = ({ initialState = {} } = {}) => { + const createComponent = () => { const store = new Vuex.Store({ state: { urlQuery: MOCK_QUERY, - useSidebarNavigation: false, searchType: SEARCH_TYPE_ADVANCED, - ...initialState, }, getters: defaultGetters, }); @@ -35,10 +33,9 @@ describe('GlobalSearch BlobsFilters', () => { const findLanguageFilter = () => wrapper.findComponent(LanguageFilter); const findArchivedFilter = () => wrapper.findComponent(ArchivedFilter); - const findDividers = () => wrapper.findAll('hr'); beforeEach(() => { - createComponent({}); + createComponent(); }); it('renders LanguageFilter', () => { @@ -48,31 +45,4 @@ describe('GlobalSearch BlobsFilters', () => { it('renders ArchivedFilter', () => { expect(findArchivedFilter().exists()).toBe(true); }); - - it('renders divider correctly', () => { - expect(findDividers()).toHaveLength(1); - }); - - describe('Renders correctly in new nav', () => { - beforeEach(() => { - createComponent({ - initialState: { - searchType: SEARCH_TYPE_ADVANCED, - useSidebarNavigation: true, - }, - }); - }); - - it('renders correctly LanguageFilter', () => { - expect(findLanguageFilter().exists()).toBe(true); - }); - - it('renders correctly ArchivedFilter', () => { - expect(findArchivedFilter().exists()).toBe(true); - }); - - it("doesn't render dividers", () => { - expect(findDividers()).toHaveLength(0); - }); - }); }); diff --git a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js index 6444ec10466..fedbd407b0b 100644 --- a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js +++ b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js @@ -22,23 +22,11 @@ describe('ConfidentialityFilter', () => { const findRadioFilter = () => wrapper.findComponent(RadioFilter); - describe('old sidebar', () => { - beforeEach(() => { - createComponent({ useSidebarNavigation: false }); - }); - - it('renders the component', () => { - expect(findRadioFilter().exists()).toBe(true); - }); + beforeEach(() => { + createComponent(); }); - describe('new sidebar', () => { - beforeEach(() => { - createComponent({ useSidebarNavigation: true }); - }); - - it('renders the component', () => { - expect(findRadioFilter().exists()).toBe(true); - }); + it('renders the component', () => { + expect(findRadioFilter().exists()).toBe(true); }); }); diff --git a/spec/frontend/search/sidebar/components/filters_template_spec.js b/spec/frontend/search/sidebar/components/filters_template_spec.js index f1a807c5ceb..18144e25ac3 100644 --- a/spec/frontend/search/sidebar/components/filters_template_spec.js +++ b/spec/frontend/search/sidebar/components/filters_template_spec.js @@ -52,7 +52,6 @@ describe('GlobalSearchSidebarLanguageFilter', () => { }; const findForm = () => wrapper.findComponent(GlForm); - const findDividers = () => wrapper.findAll('hr'); const findApplyButton = () => wrapper.findComponent(GlButton); const findResetButton = () => wrapper.findComponent(GlLink); const findSlotContent = () => wrapper.findByText('Filters Content'); @@ -66,10 +65,6 @@ describe('GlobalSearchSidebarLanguageFilter', () => { expect(findForm().exists()).toBe(true); }); - it('renders dividers', () => { - expect(findDividers()).toHaveLength(2); - }); - it('renders slot content', () => { expect(findSlotContent().exists()).toBe(true); }); diff --git a/spec/frontend/search/sidebar/components/issues_filters_spec.js b/spec/frontend/search/sidebar/components/issues_filters_spec.js index 860c5c147a6..acdfa41b5f8 100644 --- a/spec/frontend/search/sidebar/components/issues_filters_spec.js +++ b/spec/frontend/search/sidebar/components/issues_filters_spec.js @@ -23,7 +23,6 @@ describe('GlobalSearch IssuesFilters', () => { const store = new Vuex.Store({ state: { urlQuery: MOCK_QUERY, - useSidebarNavigation: false, searchType: SEARCH_TYPE_ADVANCED, ...initialState, }, @@ -44,7 +43,6 @@ describe('GlobalSearch IssuesFilters', () => { const findConfidentialityFilter = () => wrapper.findComponent(ConfidentialityFilter); const findLabelFilter = () => wrapper.findComponent(LabelFilter); const findArchivedFilter = () => wrapper.findComponent(ArchivedFilter); - const findDividers = () => wrapper.findAll('hr'); describe.each` description | searchIssueLabelAggregation @@ -72,15 +70,6 @@ describe('GlobalSearch IssuesFilters', () => { it(`renders correctly LabelFilter when searchIssueLabelAggregation is ${searchIssueLabelAggregation}`, () => { expect(findLabelFilter().exists()).toBe(searchIssueLabelAggregation); }); - - it('renders divider correctly', () => { - // two dividers can't be disabled - let dividersCount = 2; - if (searchIssueLabelAggregation) { - dividersCount += 1; - } - expect(findDividers()).toHaveLength(dividersCount); - }); }); describe('Renders correctly with basic search', () => { @@ -102,41 +91,6 @@ describe('GlobalSearch IssuesFilters', () => { it("doesn't render ArchivedFilter", () => { expect(findArchivedFilter().exists()).toBe(true); }); - - it('renders 1 divider', () => { - expect(findDividers()).toHaveLength(2); - }); - }); - - describe('Renders correctly in new nav', () => { - beforeEach(() => { - createComponent({ - initialState: { - searchType: SEARCH_TYPE_ADVANCED, - useSidebarNavigation: true, - }, - searchIssueLabelAggregation: true, - }); - }); - it('renders StatusFilter', () => { - expect(findStatusFilter().exists()).toBe(true); - }); - - it('renders ConfidentialityFilter', () => { - expect(findConfidentialityFilter().exists()).toBe(true); - }); - - it('renders LabelFilter', () => { - expect(findLabelFilter().exists()).toBe(true); - }); - - it('renders ArchivedFilter', () => { - expect(findArchivedFilter().exists()).toBe(true); - }); - - it("doesn't render dividers", () => { - expect(findDividers()).toHaveLength(0); - }); }); describe('Renders correctly with wrong scope', () => { @@ -159,9 +113,5 @@ describe('GlobalSearch IssuesFilters', () => { it("doesn't render ArchivedFilter", () => { expect(findArchivedFilter().exists()).toBe(false); }); - - it("doesn't render dividers", () => { - expect(findDividers()).toHaveLength(0); - }); }); }); diff --git a/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js b/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js index b02228a418f..8cd3cb45a20 100644 --- a/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js +++ b/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js @@ -21,7 +21,6 @@ describe('GlobalSearch MergeRequestsFilters', () => { const store = new Vuex.Store({ state: { urlQuery: MOCK_QUERY, - useSidebarNavigation: false, searchType: SEARCH_TYPE_ADVANCED, ...initialState, }, @@ -35,7 +34,6 @@ describe('GlobalSearch MergeRequestsFilters', () => { const findStatusFilter = () => wrapper.findComponent(StatusFilter); const findArchivedFilter = () => wrapper.findComponent(ArchivedFilter); - const findDividers = () => wrapper.findAll('hr'); describe('Renders correctly with Archived Filter', () => { beforeEach(() => { @@ -46,8 +44,8 @@ describe('GlobalSearch MergeRequestsFilters', () => { expect(findStatusFilter().exists()).toBe(true); }); - it('renders divider correctly', () => { - expect(findDividers()).toHaveLength(1); + it('renders ArchivedFilter', () => { + expect(findArchivedFilter().exists()).toBe(true); }); }); @@ -60,33 +58,9 @@ describe('GlobalSearch MergeRequestsFilters', () => { expect(findStatusFilter().exists()).toBe(true); }); - it('renders render ArchivedFilter', () => { - expect(findArchivedFilter().exists()).toBe(true); - }); - - it('renders 1 divider', () => { - expect(findDividers()).toHaveLength(1); - }); - }); - - describe('Renders correctly in new nav', () => { - beforeEach(() => { - createComponent({ - searchType: SEARCH_TYPE_ADVANCED, - useSidebarNavigation: true, - }); - }); - it('renders StatusFilter', () => { - expect(findStatusFilter().exists()).toBe(true); - }); - it('renders ArchivedFilter', () => { expect(findArchivedFilter().exists()).toBe(true); }); - - it("doesn't render divider", () => { - expect(findDividers()).toHaveLength(0); - }); }); describe('Renders correctly with wrong scope', () => { @@ -101,9 +75,5 @@ describe('GlobalSearch MergeRequestsFilters', () => { it("doesn't render ArchivedFilter", () => { expect(findArchivedFilter().exists()).toBe(false); }); - - it("doesn't render dividers", () => { - expect(findDividers()).toHaveLength(0); - }); }); }); diff --git a/spec/frontend/search/sidebar/components/scope_legacy_navigation_spec.js b/spec/frontend/search/sidebar/components/scope_legacy_navigation_spec.js deleted file mode 100644 index 63d8b34fcf0..00000000000 --- a/spec/frontend/search/sidebar/components/scope_legacy_navigation_spec.js +++ /dev/null @@ -1,145 +0,0 @@ -import { GlNav, GlNavItem, GlIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import { MOCK_QUERY, MOCK_NAVIGATION } from 'jest/search/mock_data'; -import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue'; - -Vue.use(Vuex); - -const MOCK_NAVIGATION_ENTRIES = Object.entries(MOCK_NAVIGATION); - -describe('ScopeLegacyNavigation', () => { - let wrapper; - - const actionSpies = { - fetchSidebarCount: jest.fn(), - }; - - const getterSpies = { - currentScope: jest.fn(() => 'issues'), - }; - - const createComponent = (initialState) => { - const store = new Vuex.Store({ - state: { - urlQuery: MOCK_QUERY, - navigation: MOCK_NAVIGATION, - ...initialState, - }, - actions: actionSpies, - getters: getterSpies, - }); - - wrapper = shallowMount(ScopeLegacyNavigation, { - store, - }); - }; - - const findNavElement = () => wrapper.find('nav'); - const findGlNav = () => wrapper.findComponent(GlNav); - const findGlNavItems = () => wrapper.findAllComponents(GlNavItem); - const findGlNavItemActive = () => wrapper.find('[active=true]'); - const findGlNavItemActiveLabel = () => findGlNavItemActive().find('[data-testid="label"]'); - const findGlNavItemActiveCount = () => findGlNavItemActive().find('[data-testid="count"]'); - - describe('scope navigation', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders section', () => { - expect(findNavElement().exists()).toBe(true); - }); - - it('renders nav component', () => { - expect(findGlNav().exists()).toBe(true); - }); - - it('renders all nav item components', () => { - expect(findGlNavItems()).toHaveLength(MOCK_NAVIGATION_ENTRIES.length); - }); - - it('has all proper links', () => { - const linkAtPosition = 3; - const { link } = MOCK_NAVIGATION_ENTRIES[linkAtPosition][1]; - - expect(findGlNavItems().at(linkAtPosition).attributes('href')).toBe(link); - }); - }); - - describe('scope navigation sets proper state with url scope set', () => { - beforeEach(() => { - createComponent(); - }); - - it('has correct active item', () => { - expect(findGlNavItemActive().exists()).toBe(true); - expect(findGlNavItemActiveLabel().text()).toBe('Issues'); - }); - - it('has correct active item count', () => { - expect(findGlNavItemActiveCount().text()).toBe('2.4K'); - }); - - it('does not have plus sign after count text', () => { - expect(findGlNavItemActive().findComponent(GlIcon).exists()).toBe(false); - }); - - it('has count is highlighted correctly', () => { - expect(findGlNavItemActiveCount().classes('gl-text-gray-900')).toBe(true); - }); - }); - - describe('scope navigation sets proper state with NO url scope set', () => { - beforeEach(() => { - getterSpies.currentScope = jest.fn(() => 'projects'); - createComponent({ - urlQuery: {}, - navigation: { - ...MOCK_NAVIGATION, - projects: { - ...MOCK_NAVIGATION.projects, - active: true, - }, - issues: { - ...MOCK_NAVIGATION.issues, - active: false, - }, - }, - }); - }); - - it('has correct active item', () => { - expect(findGlNavItemActive().exists()).toBe(true); - expect(findGlNavItemActiveLabel().text()).toBe('Projects'); - }); - - it('has correct active item count', () => { - expect(findGlNavItemActiveCount().text()).toBe('10K'); - }); - - it('has correct active item count and over limit sign', () => { - expect(findGlNavItemActive().findComponent(GlIcon).exists()).toBe(true); - }); - }); - - describe.each` - searchTherm | hasBeenCalled - ${null} | ${0} - ${'test'} | ${1} - `('fetchSidebarCount', ({ searchTherm, hasBeenCalled }) => { - beforeEach(() => { - createComponent({ - urlQuery: { - search: searchTherm, - }, - }); - }); - - it('is only called when search term is set', () => { - expect(actionSpies.fetchSidebarCount).toHaveBeenCalledTimes(hasBeenCalled); - }); - }); -}); diff --git a/spec/frontend/search/sidebar/components/small_screen_drawer_navigation_spec.js b/spec/frontend/search/sidebar/components/small_screen_drawer_navigation_spec.js deleted file mode 100644 index 5ab4afba7f0..00000000000 --- a/spec/frontend/search/sidebar/components/small_screen_drawer_navigation_spec.js +++ /dev/null @@ -1,68 +0,0 @@ -import { nextTick } from 'vue'; -import { GlDrawer } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import DomElementListener from '~/vue_shared/components/dom_element_listener.vue'; -import { DRAWER_Z_INDEX } from '~/lib/utils/constants'; -import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue'; - -describe('ScopeLegacyNavigation', () => { - let wrapper; - let closeSpy; - let toggleSpy; - - const createComponent = () => { - wrapper = shallowMountExtended(SmallScreenDrawerNavigation, { - slots: { - default: '<div data-testid="default-slot-content">test</div>', - }, - }); - }; - - const findGlDrawer = () => wrapper.findComponent(GlDrawer); - const findTitle = () => wrapper.findComponent('h2'); - const findSlot = () => wrapper.findByTestId('default-slot-content'); - const findDomElementListener = () => wrapper.findComponent(DomElementListener); - - describe('small screen navigation', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders drawer', () => { - expect(findGlDrawer().exists()).toBe(true); - expect(findGlDrawer().attributes('zindex')).toBe(DRAWER_Z_INDEX.toString()); - expect(findGlDrawer().attributes('headerheight')).toBe('0'); - }); - - it('renders title', () => { - expect(findTitle().exists()).toBe(true); - }); - - it('renders slots', () => { - expect(findSlot().exists()).toBe(true); - }); - }); - - describe('actions', () => { - beforeEach(() => { - closeSpy = jest.spyOn(SmallScreenDrawerNavigation.methods, 'closeSmallScreenFilters'); - toggleSpy = jest.spyOn(SmallScreenDrawerNavigation.methods, 'toggleSmallScreenFilters'); - createComponent(); - }); - - it('calls onClose', () => { - findGlDrawer().vm.$emit('close'); - expect(closeSpy).toHaveBeenCalled(); - }); - - it('calls toggleSmallScreenFilters', async () => { - expect(findGlDrawer().props('open')).toBe(false); - - findDomElementListener().vm.$emit('click'); - await nextTick(); - - expect(toggleSpy).toHaveBeenCalled(); - expect(findGlDrawer().props('open')).toBe(true); - }); - }); -}); diff --git a/spec/frontend/search/sidebar/components/status_filter_spec.js b/spec/frontend/search/sidebar/components/status_filter_spec.js index c230341c172..719932a79ef 100644 --- a/spec/frontend/search/sidebar/components/status_filter_spec.js +++ b/spec/frontend/search/sidebar/components/status_filter_spec.js @@ -22,23 +22,9 @@ describe('StatusFilter', () => { const findRadioFilter = () => wrapper.findComponent(RadioFilter); - describe('old sidebar', () => { - beforeEach(() => { - createComponent({ useSidebarNavigation: false }); - }); - - it('renders the component', () => { - expect(findRadioFilter().exists()).toBe(true); - }); - }); + it('renders the component', () => { + createComponent(); - describe('new sidebar', () => { - beforeEach(() => { - createComponent({ useSidebarNavigation: true }); - }); - - it('renders the component', () => { - expect(findRadioFilter().exists()).toBe(true); - }); + expect(findRadioFilter().exists()).toBe(true); }); }); diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js index fd34e7893a9..31fa7cd3ec4 100644 --- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js +++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js @@ -58,6 +58,7 @@ describe('SuperSidebar component', () => { const findTrialStatusWidget = () => wrapper.findByTestId(trialStatusWidgetStubTestId); const findTrialStatusPopover = () => wrapper.findByTestId(trialStatusPopoverStubTestId); const findSidebarMenu = () => wrapper.findComponent(SidebarMenu); + const findAdminLink = () => wrapper.findByTestId('sidebar-admin-link'); let trackingSpy = null; const createWrapper = ({ @@ -337,4 +338,25 @@ describe('SuperSidebar component', () => { expect(document.addEventListener).toHaveBeenCalledWith('keydown', wrapper.vm.focusTrap); }); }); + + describe('link to Admin area', () => { + describe('when user is admin', () => { + it('renders', () => { + createWrapper({ + sidebarData: { + ...mockSidebarData, + is_admin: true, + }, + }); + expect(findAdminLink().attributes('href')).toBe(mockSidebarData.admin_url); + }); + }); + + describe('when user is not admin', () => { + it('renders', () => { + createWrapper(); + expect(findAdminLink().exists()).toBe(false); + }); + }); + }); }); diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js index ef0c3686a4e..f5e7a7507ea 100644 --- a/spec/frontend/super_sidebar/mock_data.js +++ b/spec/frontend/super_sidebar/mock_data.js @@ -79,6 +79,8 @@ export const contextSwitcherLinks = [ export const sidebarData = { is_logged_in: true, + is_admin: false, + admin_url: '/admin', current_menu_items: [], current_context: {}, current_context_header: 'Your work', diff --git a/spec/graphql/types/root_storage_statistics_type_spec.rb b/spec/graphql/types/root_storage_statistics_type_spec.rb index 00f4092baf4..8ac3b32948f 100644 --- a/spec/graphql/types/root_storage_statistics_type_spec.rb +++ b/spec/graphql/types/root_storage_statistics_type_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe GitlabSchema.types['RootStorageStatistics'] do +RSpec.describe GitlabSchema.types['RootStorageStatistics'], feature_category: :consumables_cost_management do specify { expect(described_class.graphql_name).to eq('RootStorageStatistics') } it 'has the expected fields' do diff --git a/spec/helpers/organizations/organization_helper_spec.rb b/spec/helpers/organizations/organization_helper_spec.rb index 9d55d2a84f8..86a7a88797c 100644 --- a/spec/helpers/organizations/organization_helper_spec.rb +++ b/spec/helpers/organizations/organization_helper_spec.rb @@ -119,10 +119,13 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do end describe '#organization_user_app_data' do - it 'returns expected data object' do - expect(helper.organization_user_app_data(organization)).to eq( + it 'returns expected json' do + expect(Gitlab::Json.parse(helper.organization_user_app_data(organization))).to eq( { - organization_gid: organization.to_global_id + 'organization_gid' => organization.to_global_id.to_s, + 'paths' => { + 'admin_user' => admin_user_path(:id) + } } ) end diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb index c9131ca518f..421b1c178aa 100644 --- a/spec/helpers/sidebars_helper_spec.rb +++ b/spec/helpers/sidebars_helper_spec.rb @@ -131,8 +131,10 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do it 'returns sidebar values from user', :use_clean_rails_memory_store_caching do expect(subject).to include({ is_logged_in: true, + is_admin: false, name: user.name, username: user.username, + admin_url: admin_root_url, avatar_url: user.avatar_url, has_link_to_profile: helper.current_user_menu?(:profile), link_to_profile: user_path(user), @@ -174,6 +176,14 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do }) end + context 'when user is admin' do + before do + allow(user).to receive(:can_admin_all_resources?).and_return(true) + end + + it { is_expected.to include({ is_admin: true }) } + end + describe "shortcut links" do describe "as the anonymous user" do let_it_be(:user) { nil } diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb index b2da9fa8801..e840dddbedd 100644 --- a/spec/helpers/storage_helper_spec.rb +++ b/spec/helpers/storage_helper_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -RSpec.describe StorageHelper do +RSpec.describe StorageHelper, feature_category: :consumables_cost_management do describe "#storage_counter" do it "formats bytes to one decimal place" do expect(helper.storage_counter(1.23.megabytes)).to eq("1.2 MiB") diff --git a/spec/lib/api/entities/ci/job_request/image_spec.rb b/spec/lib/api/entities/ci/job_request/image_spec.rb index 14d4a074fce..666ec31d3d9 100644 --- a/spec/lib/api/entities/ci/job_request/image_spec.rb +++ b/spec/lib/api/entities/ci/job_request/image_spec.rb @@ -4,7 +4,10 @@ require 'spec_helper' RSpec.describe API::Entities::Ci::JobRequest::Image do let(:ports) { [{ number: 80, protocol: 'http', name: 'name' }] } - let(:image) { double(name: 'image_name', entrypoint: ['foo'], ports: ports, pull_policy: ['if-not-present']) } + let(:image) do + double(name: 'image_name', entrypoint: ['foo'], executor_opts: {}, ports: ports, pull_policy: ['if-not-present']) + end + let(:entity) { described_class.new(image) } subject { entity.as_json } @@ -29,6 +32,10 @@ RSpec.describe API::Entities::Ci::JobRequest::Image do end end + it 'returns the executor_opts options' do + expect(subject[:executor_opts]).to eq({}) + end + it 'returns the pull policy' do expect(subject[:pull_policy]).to eq(['if-not-present']) end diff --git a/spec/lib/api/entities/ci/job_request/service_spec.rb b/spec/lib/api/entities/ci/job_request/service_spec.rb index 11350f7c41b..c2331799314 100644 --- a/spec/lib/api/entities/ci/job_request/service_spec.rb +++ b/spec/lib/api/entities/ci/job_request/service_spec.rb @@ -9,6 +9,7 @@ RSpec.describe API::Entities::Ci::JobRequest::Service do ::Gitlab::Ci::Build::Image, name: 'image_name', entrypoint: ['foo'], + executor_opts: {}, ports: ports, pull_policy: ['if-not-present'], alias: 'alias', @@ -25,6 +26,7 @@ RSpec.describe API::Entities::Ci::JobRequest::Service do expect(result).to eq( name: 'image_name', entrypoint: ['foo'], + executor_opts: {}, ports: ports, pull_policy: ['if-not-present'], alias: 'alias', diff --git a/spec/lib/gitlab/ci/build/image_spec.rb b/spec/lib/gitlab/ci/build/image_spec.rb index 4895077a731..f8c0d69be2e 100644 --- a/spec/lib/gitlab/ci/build/image_spec.rb +++ b/spec/lib/gitlab/ci/build/image_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Gitlab::Ci::Build::Image do context 'when image is defined in job' do let(:image_name) { 'image:1.0' } - let(:job) { create(:ci_build, options: { image: image_name } ) } + let(:job) { create(:ci_build, options: { image: image_name }) } context 'when image is defined as string' do it 'fabricates an object of the proper class' do @@ -29,12 +29,14 @@ RSpec.describe Gitlab::Ci::Build::Image do context 'when image is defined as hash' do let(:entrypoint) { '/bin/sh' } let(:pull_policy) { %w[always if-not-present] } + let(:executor_opts) { { docker: { platform: 'arm64' } } } let(:job) do create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint, ports: [80], - pull_policy: pull_policy } } ) + executor_opts: executor_opts, + pull_policy: pull_policy } }) end it 'fabricates an object of the proper class' do @@ -44,6 +46,7 @@ RSpec.describe Gitlab::Ci::Build::Image do it 'populates fabricated object with the proper attributes' do expect(subject.name).to eq(image_name) expect(subject.entrypoint).to eq(entrypoint) + expect(subject.executor_opts).to eq(executor_opts) expect(subject.pull_policy).to eq(pull_policy) end @@ -98,11 +101,12 @@ RSpec.describe Gitlab::Ci::Build::Image do let(:service_entrypoint) { '/bin/sh' } let(:service_alias) { 'db' } let(:service_command) { 'sleep 30' } + let(:executor_opts) { { docker: { platform: 'amd64' } } } let(:pull_policy) { %w[always if-not-present] } let(:job) do create(:ci_build, options: { services: [{ name: service_image_name, entrypoint: service_entrypoint, alias: service_alias, command: service_command, ports: [80], - pull_policy: pull_policy }] }) + executor_opts: executor_opts, pull_policy: pull_policy }] }) end it 'fabricates an non-empty array of objects' do @@ -116,6 +120,7 @@ RSpec.describe Gitlab::Ci::Build::Image do expect(subject.first.entrypoint).to eq(service_entrypoint) expect(subject.first.alias).to eq(service_alias) expect(subject.first.command).to eq(service_command) + expect(subject.first.executor_opts).to eq(executor_opts) expect(subject.first.pull_policy).to eq(pull_policy) port = subject.first.ports.first diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index 17c45ec4c2c..50f3e209ad0 100644 --- a/spec/lib/gitlab/ci/config/entry/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -42,6 +42,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do end end + describe '#executor_opts' do + it "returns nil" do + expect(entry.executor_opts).to be_nil + end + end + describe '#ports' do it "returns image's ports" do expect(entry.ports).to be_nil @@ -88,6 +94,52 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do end end + context 'when configuration specifies docker' do + let(:config) { { name: 'image:1.0', docker: {} } } + + it 'is valid' do + expect(entry).to be_valid + end + + describe '#value' do + it "returns value" do + expect(entry.value).to eq( + name: 'image:1.0', + executor_opts: { + docker: {} + } + ) + end + end + + context "when docker specifies an option" do + let(:config) { { name: 'image:1.0', docker: { platform: 'amd64' } } } + + it 'is valid' do + expect(entry).to be_valid + end + + describe '#value' do + it "returns value" do + expect(entry.value).to eq( + name: 'image:1.0', + executor_opts: { + docker: { platform: 'amd64' } + } + ) + end + end + end + + context "when docker specifies an invalid option" do + let(:config) { { name: 'image:1.0', docker: { platform: 1 } } } + + it 'is not valid' do + expect(entry).not_to be_valid + end + end + end + context 'when configuration has ports' do let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] } let(:config) { { name: 'image:1.0', entrypoint: %w[/bin/sh run], ports: ports } } @@ -146,7 +198,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do describe '#errors' do it 'saves errors' do expect(entry.errors.first) - .to match /config should be a hash or a string/ + .to match(/config should be a hash or a string/) end end @@ -163,7 +215,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do describe '#errors' do it 'saves errors' do expect(entry.errors.first) - .to match /config contains unknown keys: non_existing/ + .to match(/config contains unknown keys: non_existing/) end end diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb index 1f935bebed5..9f9c15145ed 100644 --- a/spec/lib/gitlab/ci/config/entry/service_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb @@ -47,6 +47,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do expect(entry.ports).to be_nil end end + + describe '#executor_opts' do + it "returns service's executor_opts configuration" do + expect(entry.executor_opts).to be_nil + end + end end context 'when configuration is a hash' do @@ -141,6 +147,27 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do end end + context 'when configuration has docker options' do + let(:config) { { name: 'postgresql:9.5', docker: { platform: 'amd64' } } } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it "returns value" do + expect(entry.value).to eq( + name: 'postgresql:9.5', + executor_opts: { + docker: { platform: 'amd64' } + } + ) + end + end + end + context 'when configuration has pull_policy' do let(:config) { { name: 'postgresql:9.5', pull_policy: 'if-not-present' } } diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index f01c1c7d053..e44e01b2ffa 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -1313,6 +1313,46 @@ module Gitlab }) end end + + context 'when image and service have docker options' do + let(:config) do + <<~YAML + test: + script: exit 0 + image: + name: ruby:2.7 + docker: + platform: linux/amd64 + services: + - name: postgres:11.9 + docker: + platform: linux/amd64 + YAML + end + + it { is_expected.to be_valid } + + it "returns with image" do + expect(processor.builds).to contain_exactly({ + stage: "test", + stage_idx: 2, + name: "test", + only: { refs: %w[branches tags] }, + options: { + script: ["exit 0"], + image: { name: "ruby:2.7", + executor_opts: { docker: { platform: 'linux/amd64' } } }, + services: [{ name: "postgres:11.9", + executor_opts: { docker: { platform: 'linux/amd64' } } }] + }, + allow_failure: false, + when: "on_success", + job_variables: [], + root_variables_inheritance: true, + scheduling_type: :stage + }) + end + end end describe 'Variables' do diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb index 2a870a25ea6..3d6d86335eb 100644 --- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb +++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb @@ -272,16 +272,19 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego expect(json_response['job_info']).to include(expected_job_info) expect(json_response['git_info']).to eq(expected_git_info) expect(json_response['image']).to eq( - { 'name' => 'image:1.0', 'entrypoint' => '/bin/sh', 'ports' => [], 'pull_policy' => nil } + { 'name' => 'image:1.0', 'entrypoint' => '/bin/sh', 'ports' => [], 'executor_opts' => {}, + 'pull_policy' => nil } ) expect(json_response['services']).to eq( [ { 'name' => 'postgres', 'entrypoint' => nil, 'alias' => nil, 'command' => nil, 'ports' => [], - 'variables' => nil, 'pull_policy' => nil }, - { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh', 'alias' => 'docker', 'command' => 'sleep 30', - 'ports' => [], 'variables' => [], 'pull_policy' => nil }, + 'variables' => nil, 'executor_opts' => {}, 'pull_policy' => nil }, + { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh', 'alias' => 'docker', + 'command' => 'sleep 30', 'ports' => [], 'variables' => [], 'executor_opts' => {}, + 'pull_policy' => nil }, { 'name' => 'mysql:latest', 'entrypoint' => nil, 'alias' => nil, 'command' => nil, 'ports' => [], - 'variables' => [{ 'key' => 'MYSQL_ROOT_PASSWORD', 'value' => 'root123.' }], 'pull_policy' => nil } + 'variables' => [{ 'key' => 'MYSQL_ROOT_PASSWORD', 'value' => 'root123.' }], 'executor_opts' => {}, + 'pull_policy' => nil } ]) expect(json_response['steps']).to eq(expected_steps) expect(json_response['hooks']).to eq(expected_hooks) @@ -920,6 +923,41 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego end end + context 'when image has docker options' do + let(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) } + + let(:options) do + { + image: { + name: 'ruby', + executor_opts: { + docker: { + platform: 'amd64' + } + } + } + } + end + + it 'returns the image with docker options' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to include( + 'id' => job.id, + 'image' => { 'name' => 'ruby', + 'executor_opts' => { + 'docker' => { + 'platform' => 'amd64' + } + }, + 'pull_policy' => nil, + 'entrypoint' => nil, + 'ports' => [] } + ) + end + end + context 'when image has pull_policy' do let(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) } @@ -938,7 +976,11 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego expect(response).to have_gitlab_http_status(:created) expect(json_response).to include( 'id' => job.id, - 'image' => { 'name' => 'ruby', 'pull_policy' => ['if-not-present'], 'entrypoint' => nil, 'ports' => [] } + 'image' => { 'name' => 'ruby', + 'executor_opts' => {}, + 'pull_policy' => ['if-not-present'], + 'entrypoint' => nil, + 'ports' => [] } ) end end @@ -962,7 +1004,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego expect(json_response).to include( 'id' => job.id, 'services' => [{ 'alias' => nil, 'command' => nil, 'entrypoint' => nil, 'name' => 'postgres:11.9', - 'ports' => [], 'pull_policy' => ['if-not-present'], 'variables' => [] }] + 'ports' => [], 'executor_opts' => {}, 'pull_policy' => ['if-not-present'], + 'variables' => [] }] ) end end diff --git a/spec/requests/api/ci/runner/jobs_request_yamls_spec.rb b/spec/requests/api/ci/runner/jobs_request_yamls_spec.rb new file mode 100644 index 00000000000..f399c3e310e --- /dev/null +++ b/spec/requests/api/ci/runner/jobs_request_yamls_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_category: :continuous_integration do + include StubGitlabCalls + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository, shared_runners_enabled: false) } + let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) } + + let(:user_agent) { 'gitlab-runner 9.0.0 (9-0-stable; go1.7.4; linux/amd64)' } + + before_all do + project.add_maintainer(user) + end + + Dir[Rails.root.join("spec/requests/api/ci/runner/yamls/*.yml")].each do |yml_file| + context "for #{File.basename(yml_file)}" do + let(:yaml_content) { YAML.load_file(yml_file) } + let(:gitlab_ci_yml) { yaml_content.fetch("gitlab_ci") } + let(:request_response) { yaml_content.fetch("request_response") } + + it 'runs a job' do + stub_ci_pipeline_yaml_file(YAML.dump(gitlab_ci_yml)) + + pipeline_response = create_pipeline! + expect(pipeline_response).to be_success, pipeline_response.message + expect(pipeline_response.payload).to be_created_successfully + expect(pipeline_response.payload.builds).to be_one + + build = pipeline_response.payload.builds.first + + process_pipeline!(pipeline_response.payload) + expect(build.reload).to be_pending + + request_job(runner.token) + expect(response).to have_gitlab_http_status(:created) + expect(response.headers['Content-Type']).to eq('application/json') + expect(json_response).to include('id' => build.id, 'token' => build.token) + expect(json_response).to include(request_response) + end + end + end + + def create_pipeline! + params = { ref: 'master', + before: '00000000', + after: project.commit.id, + commits: [{ message: 'some commit' }] } + + Ci::CreatePipelineService.new(project, user, params).execute(:push) + end + + def process_pipeline!(pipeline) + PipelineProcessWorker.new.perform(pipeline.id) + end + + def request_job(token, **params) + new_params = params.merge(token: token) + post api('/jobs/request'), params: new_params.to_json, + headers: { 'User-Agent' => user_agent, 'Content-Type': 'application/json' } + end +end diff --git a/spec/requests/api/ci/runner/yamls/README.md b/spec/requests/api/ci/runner/yamls/README.md new file mode 100644 index 00000000000..db8ef51ff9c --- /dev/null +++ b/spec/requests/api/ci/runner/yamls/README.md @@ -0,0 +1,15 @@ +# .gitlab-ci.yml end-to-end tests + +The purpose of this folder is to provide a single job `.gitlab-ci.yml` +that will be validated against end-to-end response that is send to runner. + +This allows to easily test end-to-end all CI job transformation that +and impact on how such job is rendered to be executed by the GitLab Runner. + +```yaml +gitlab_ci: + # .gitlab-ci.yml to stub + +request_response: + # exact payload that is checked as returned by `/api/v4/jobs/request` +``` diff --git a/spec/requests/api/ci/runner/yamls/image-basic.yml b/spec/requests/api/ci/runner/yamls/image-basic.yml new file mode 100644 index 00000000000..0c01dbc6e8b --- /dev/null +++ b/spec/requests/api/ci/runner/yamls/image-basic.yml @@ -0,0 +1,19 @@ +gitlab_ci: + rspec: + image: alpine:latest + script: echo Hello World + +request_response: + image: + name: alpine:latest + entrypoint: null + executor_opts: {} + ports: [] + pull_policy: null + steps: + - name: script + script: ["echo Hello World"] + timeout: 3600 + when: on_success + allow_failure: false + services: [] diff --git a/spec/requests/api/ci/runner/yamls/image-executor_opts-platform.yml b/spec/requests/api/ci/runner/yamls/image-executor_opts-platform.yml new file mode 100644 index 00000000000..62e301f2e9a --- /dev/null +++ b/spec/requests/api/ci/runner/yamls/image-executor_opts-platform.yml @@ -0,0 +1,25 @@ +gitlab_ci: + rspec: + image: + name: alpine:latest + docker: + platform: amd64 + script: echo Hello World + +request_response: + image: + name: alpine:latest + entrypoint: null + executor_opts: + docker: + platform: amd64 + ports: [] + pull_policy: null + steps: + - name: script + script: ["echo Hello World"] + timeout: 3600 + when: on_success + allow_failure: false + services: [] + diff --git a/spec/requests/api/ci/runner/yamls/service-basic.yml b/spec/requests/api/ci/runner/yamls/service-basic.yml new file mode 100644 index 00000000000..5438837c496 --- /dev/null +++ b/spec/requests/api/ci/runner/yamls/service-basic.yml @@ -0,0 +1,23 @@ +gitlab_ci: + rspec: + services: + - docker:dind + script: echo Hello World + +request_response: + image: null + steps: + - name: script + script: ["echo Hello World"] + timeout: 3600 + when: on_success + allow_failure: false + services: + - name: docker:dind + alias: null + command: null + entrypoint: null + executor_opts: {} + ports: [] + pull_policy: null + variables: [] diff --git a/spec/requests/api/ci/runner/yamls/service-executor_opts-platform.yml b/spec/requests/api/ci/runner/yamls/service-executor_opts-platform.yml new file mode 100644 index 00000000000..6483d749c45 --- /dev/null +++ b/spec/requests/api/ci/runner/yamls/service-executor_opts-platform.yml @@ -0,0 +1,27 @@ +gitlab_ci: + rspec: + services: + - name: docker:dind + docker: + platform: amd64 + script: echo Hello World + +request_response: + image: null + steps: + - name: script + script: ["echo Hello World"] + timeout: 3600 + when: on_success + allow_failure: false + services: + - name: docker:dind + alias: null + command: null + entrypoint: null + executor_opts: + docker: + platform: amd64 + ports: [] + pull_policy: null + variables: [] diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index da23f81e86e..fc6823352cd 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -2709,7 +2709,6 @@ - './ee/spec/services/merge_requests/build_service_spec.rb' - './ee/spec/services/merge_requests/mergeability/check_approved_service_spec.rb' - './ee/spec/services/merge_requests/mergeability/check_blocked_by_other_mrs_service_spec.rb' -- './ee/spec/services/merge_requests/mergeability/check_denied_policies_service_spec.rb' - './ee/spec/services/merge_requests/merge_service_spec.rb' - './ee/spec/services/merge_requests/merge_to_ref_service_spec.rb' - './ee/spec/services/merge_requests/push_options_handler_service_spec.rb' diff --git a/spec/validators/json_schema_validator_spec.rb b/spec/validators/json_schema_validator_spec.rb index 01caf4ab0bd..a12e7fbd160 100644 --- a/spec/validators/json_schema_validator_spec.rb +++ b/spec/validators/json_schema_validator_spec.rb @@ -58,5 +58,32 @@ RSpec.describe JsonSchemaValidator do end end end + + context "for executor_opts schema" do + context 'when detail_errors is true' do + let(:validator) { described_class.new(attributes: [:data], detail_errors: true, filename: "build_report_result_data") } + + context 'when data is valid' do + it 'returns no errors' do + subject + + expect(build_report_result.errors).to be_empty + end + end + + context 'when data is invalid' do + it 'returns json schema is invalid' do + build_report_result.data = { invalid: 'data' } + + subject + + expect(build_report_result.errors.size).to eq(1) + expect(build_report_result.errors.full_messages).to match_array( + ["Data the '/invalid' must be a valid 'schema'"] + ) + end + end + end + end end end |