From 680d17e351a1ee756c3cd78bd62bbfbf5c588979 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 7 Feb 2024 06:06:55 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../style/inline_disable_annotation.yml | 1 - .../components/pipelines_artifacts.vue | 1 + .../groups_and_projects/components/app.vue | 30 +++++-- .../groups_and_projects/index.js | 6 ++ .../shared/components/groups_view.vue | 22 ++--- .../shared/components/new_group_button.vue | 32 +++++++ .../shared/components/new_project_button.vue | 44 ++++++++++ .../shared/components/projects_view.vue | 20 ++--- .../javascripts/organizations/show/index.js | 6 ++ app/helpers/jira_connect_helper.rb | 24 ++++-- .../organizations/organization_helper.rb | 9 +- doc/administration/gitaly/troubleshooting.md | 25 ++++++ .../jira_cloud_app_troubleshooting.md | 3 +- locale/gitlab.pot | 3 + package.json | 2 +- qa/Gemfile | 2 +- qa/Gemfile.lock | 4 +- qa/qa/page/merge_request/show.rb | 6 +- spec/deprecation_warnings.rb | 8 +- .../components/app_spec.js | 25 ++++++ .../shared/components/groups_view_spec.js | 62 +++++++------- .../components/new_group_button_spec.js | 80 +++++++++++++++++ .../components/new_project_button_spec.js | 77 +++++++++++++++++ .../shared/components/projects_view_spec.js | 62 +++++++------- spec/helpers/jira_connect_helper_spec.rb | 16 ++++ .../organizations/organization_helper_spec.rb | 85 ++++++++++++++++++- yarn.lock | 8 +- 27 files changed, 535 insertions(+), 128 deletions(-) create mode 100644 app/assets/javascripts/organizations/shared/components/new_group_button.vue create mode 100644 app/assets/javascripts/organizations/shared/components/new_project_button.vue create mode 100644 spec/frontend/organizations/shared/components/new_group_button_spec.js create mode 100644 spec/frontend/organizations/shared/components/new_project_button_spec.js diff --git a/.rubocop_todo/style/inline_disable_annotation.yml b/.rubocop_todo/style/inline_disable_annotation.yml index c8b4dfedc41..92d1328106c 100644 --- a/.rubocop_todo/style/inline_disable_annotation.yml +++ b/.rubocop_todo/style/inline_disable_annotation.yml @@ -2780,7 +2780,6 @@ Style/InlineDisableAnnotation: - 'spec/controllers/projects/releases_controller_spec.rb' - 'spec/controllers/projects/runners_controller_spec.rb' - 'spec/db/docs_spec.rb' - - 'spec/deprecation_warnings.rb' - 'spec/factories/design_management/designs.rb' - 'spec/factories/events.rb' - 'spec/factories/go_module_commits.rb' diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue b/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue index a45387ca676..ce1fa2adec4 100644 --- a/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue +++ b/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue @@ -68,6 +68,7 @@ export default { placement="right" text-sr-only :items="items" + data-testid="artifacts-dropdown" /> diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue index f837845ab63..3dcfe1c8bb9 100644 --- a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue +++ b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue @@ -13,17 +13,19 @@ import { FILTERED_SEARCH_TERM, TOKEN_EMPTY_SEARCH_TERM, } from '~/vue_shared/components/filtered_search_bar/constants'; -import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '../../constants'; -import GroupsView from '../../shared/components/groups_view.vue'; -import ProjectsView from '../../shared/components/projects_view.vue'; -import { onPageChange } from '../../shared/utils'; +import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '~/organizations/constants'; +import GroupsView from '~/organizations/shared/components/groups_view.vue'; +import ProjectsView from '~/organizations/shared/components/projects_view.vue'; +import NewGroupButton from '~/organizations/shared/components/new_group_button.vue'; +import NewProjectButton from '~/organizations/shared/components/new_project_button.vue'; +import { onPageChange } from '~/organizations/shared/utils'; import { QUERY_PARAM_END_CURSOR, QUERY_PARAM_START_CURSOR, SORT_DIRECTION_ASC, SORT_DIRECTION_DESC, SORT_ITEM_NAME, -} from '../../shared/constants'; +} from '~/organizations/shared/constants'; import { DISPLAY_LISTBOX_ITEMS, SORT_ITEMS, FILTERED_SEARCH_TERM_KEY } from '../constants'; export default { @@ -32,7 +34,13 @@ export default { searchInputPlaceholder: s__('Organization|Search or filter list'), displayListboxHeaderText: __('Display'), }, - components: { FilteredSearchBar, GlCollapsibleListbox, GlSorting }, + components: { + FilteredSearchBar, + GlCollapsibleListbox, + GlSorting, + NewGroupButton, + NewProjectButton, + }, filteredSearch: { tokens: [], namespace: 'organization_groups_and_projects', @@ -156,7 +164,15 @@ export default { diff --git a/app/assets/javascripts/organizations/shared/components/new_group_button.vue b/app/assets/javascripts/organizations/shared/components/new_group_button.vue new file mode 100644 index 00000000000..8254735192a --- /dev/null +++ b/app/assets/javascripts/organizations/shared/components/new_group_button.vue @@ -0,0 +1,32 @@ + + + diff --git a/app/assets/javascripts/organizations/shared/components/new_project_button.vue b/app/assets/javascripts/organizations/shared/components/new_project_button.vue new file mode 100644 index 00000000000..c7d39e8e31f --- /dev/null +++ b/app/assets/javascripts/organizations/shared/components/new_project_button.vue @@ -0,0 +1,44 @@ + + + diff --git a/app/assets/javascripts/organizations/shared/components/projects_view.vue b/app/assets/javascripts/organizations/shared/components/projects_view.vue index 20faa6e82d5..f6d0d70e24e 100644 --- a/app/assets/javascripts/organizations/shared/components/projects_view.vue +++ b/app/assets/javascripts/organizations/shared/components/projects_view.vue @@ -7,6 +7,7 @@ import { createAlert } from '~/alert'; import { SORT_ITEM_NAME, SORT_DIRECTION_ASC } from '../constants'; import projectsQuery from '../graphql/queries/projects.query.graphql'; import { formatProjects } from '../utils'; +import NewProjectButton from './new_project_button.vue'; export default { i18n: { @@ -18,7 +19,6 @@ export default { description: s__( 'GroupsEmptyState|Projects are where you can store your code, access issues, wiki, and other features of GitLab.', ), - primaryButtonText: __('New project'), }, prev: __('Prev'), next: __('Next'), @@ -28,13 +28,11 @@ export default { GlLoadingIcon, GlEmptyState, GlKeysetPagination, + NewProjectButton, }, inject: { organizationGid: {}, projectsEmptyStateSvgPath: {}, - newProjectPath: { - default: null, - }, }, props: { shouldShowEmptyStateButtons: { @@ -149,14 +147,6 @@ export default { description: this.$options.i18n.emptyState.description, }; - if (this.shouldShowEmptyStateButtons && this.newProjectPath) { - return { - ...baseProps, - primaryButtonLink: this.newProjectPath, - primaryButtonText: this.$options.i18n.emptyState.primaryButtonText, - }; - } - return baseProps; }, }, @@ -191,5 +181,9 @@ export default { /> - + + + diff --git a/app/assets/javascripts/organizations/show/index.js b/app/assets/javascripts/organizations/show/index.js index 0d927eeea8a..940161ef091 100644 --- a/app/assets/javascripts/organizations/show/index.js +++ b/app/assets/javascripts/organizations/show/index.js @@ -35,6 +35,9 @@ export const initOrganizationsShow = () => { newGroupPath, newProjectPath, associationCounts, + canCreateProject, + canCreateGroup, + hasGroups, } = convertObjectPropsToCamelCase(JSON.parse(appData)); Vue.use(VueRouter); @@ -54,6 +57,9 @@ export const initOrganizationsShow = () => { groupsEmptyStateSvgPath, newGroupPath, newProjectPath, + canCreateProject, + canCreateGroup, + hasGroups, }, render(createElement) { return createElement(App, { diff --git a/app/helpers/jira_connect_helper.rb b/app/helpers/jira_connect_helper.rb index 2309dfc2a2b..fbfc038222b 100644 --- a/app/helpers/jira_connect_helper.rb +++ b/app/helpers/jira_connect_helper.rb @@ -19,14 +19,6 @@ module JiraConnectHelper def jira_connect_oauth_data(installation) oauth_instance_url = installation.oauth_authorization_url - oauth_authorize_path = oauth_authorization_path( - client_id: Gitlab::CurrentSettings.jira_connect_application_key, - response_type: 'code', - scope: 'api', - redirect_uri: jira_connect_oauth_callbacks_url, - state: oauth_state - ) - { oauth_authorize_url: Gitlab::Utils.append_path(oauth_instance_url, oauth_authorize_path), oauth_token_path: oauth_token_path, @@ -55,4 +47,20 @@ module JiraConnectHelper unlink_path: jira_connect_subscription_path(subscription) } end + + def relative_url_root + Gitlab.config.gitlab.relative_url_root.presence + end + + def oauth_authorize_path + oauth_authorize_path = oauth_authorization_path( + client_id: Gitlab::CurrentSettings.jira_connect_application_key, + response_type: 'code', + scope: 'api', + redirect_uri: jira_connect_oauth_callbacks_url, + state: oauth_state + ) + + oauth_authorize_path.delete_prefix(relative_url_root) + end end diff --git a/app/helpers/organizations/organization_helper.rb b/app/helpers/organizations/organization_helper.rb index e18cd409d73..ba386bca2a0 100644 --- a/app/helpers/organizations/organization_helper.rb +++ b/app/helpers/organizations/organization_helper.rb @@ -77,7 +77,10 @@ module Organizations projects_empty_state_svg_path: image_path('illustrations/empty-state/empty-projects-md.svg'), groups_empty_state_svg_path: image_path('illustrations/empty-state/empty-groups-md.svg'), new_group_path: new_group_path, - new_project_path: new_project_path + new_project_path: new_project_path, + can_create_group: can?(current_user, :create_group, organization), + can_create_project: current_user&.can_create_project?, + has_groups: has_groups?(organization) } end @@ -95,5 +98,9 @@ module Organizations admin_user: admin_user_path(:id) } end + + def has_groups?(organization) + organization.groups.exists? + end end end diff --git a/doc/administration/gitaly/troubleshooting.md b/doc/administration/gitaly/troubleshooting.md index 893f8500804..683dd49672e 100644 --- a/doc/administration/gitaly/troubleshooting.md +++ b/doc/administration/gitaly/troubleshooting.md @@ -557,3 +557,28 @@ If you find that `fapolicyd` is denying execution, consider the following: ``` 1. Restart the service. + +## `Pre-receive hook declined` error when pushing to RHEL instance with `fapolicyd` enabled + +When pushing to an RHEL-based instance with `fapolicyd` enabled, you might get a `Pre-receive hook declined` error. This error can occur because `fapolicyd` can block the execution +of the Gitaly binary. To resolve this problem, either: + +- Disable `fapolicyd`. +- Create an `fapolicyd` rule to permit execution of Gitaly binaries with `fapolicyd` enabled. + +To create a rule to allow Gitaly binary execution: + +1. Create a file at `/etc/fapolicyd/rules.d/89-gitlab.rules`. +1. Enter the following into the file: + + ```plaintext + allow perm=any all : ftype=application/x-executable dir=/var/opt/gitlab/gitaly/ + ``` + +1. Restart the service: + + ```shell + systemctl restart fapolicyd + ``` + +The new rule takes effect after the daemon restarts. diff --git a/doc/administration/settings/jira_cloud_app_troubleshooting.md b/doc/administration/settings/jira_cloud_app_troubleshooting.md index fa72e5a9296..829cf77d7ee 100644 --- a/doc/administration/settings/jira_cloud_app_troubleshooting.md +++ b/doc/administration/settings/jira_cloud_app_troubleshooting.md @@ -127,7 +127,8 @@ GitLab Support with: 1. Your GitLab self-managed instance URL. 1. Your GitLab.com username. 1. If possible, the `X-Request-Id` response header for the failed `GET` request to `https://gitlab.com/-/jira_connect/installations`. -1. Optional. [A HAR file that captured the problem](https://support.zendesk.com/hc/en-us/articles/4408828867098-Generating-a-HAR-file-for-troubleshooting). +1. Optional. [A HAR file that captured the problem](https://support.zendesk.com/hc/en-us/articles/4408828867098-Generating-a-HAR-file-for-troubleshooting) that you have + processed with the [harcleaner](https://gitlab.com/gitlab-com/support/toolbox/harcleaner) utility. The GitLab Support team can then look up why this is failing in the GitLab.com server logs. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index dc50605423e..8b844f552f8 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -34514,6 +34514,9 @@ msgstr "" msgid "Organization|Perform advanced options such as deleting the organization." msgstr "" +msgid "Organization|Projects are hosted/created in groups. Before creating a project, you must create a group." +msgstr "" + msgid "Organization|Public - The organization can be accessed without any authentication." msgstr "" diff --git a/package.json b/package.json index 1048e1c491e..80952cc8af8 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "@gitlab/svgs": "3.83.0", "@gitlab/ui": "^74.0.0", "@gitlab/visual-review-tools": "1.7.3", - "@gitlab/web-ide": "^0.0.1-dev-20240201215504", + "@gitlab/web-ide": "^0.0.1-dev-20240206230318", "@mattiasbuelens/web-streams-adapter": "^0.1.0", "@rails/actioncable": "7.0.8", "@rails/ujs": "7.0.8", diff --git a/qa/Gemfile b/qa/Gemfile index 2451f49be0a..33dccf8e0fa 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -37,7 +37,7 @@ gem 'chemlab', '~> 0.11', '>= 0.11.1' gem 'chemlab-library-www-gitlab-com', '~> 0.1', '>= 0.1.1' # dependencies for jenkins client -gem 'nokogiri', '~> 1.16' +gem 'nokogiri', '~> 1.16', '>= 1.16.2' gem 'deprecation_toolkit', '~> 2.2.0', require: false diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 1f99fdb765c..982e62154bd 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -213,7 +213,7 @@ GEM multi_json (1.15.0) multi_xml (0.6.0) netrc (0.11.0) - nokogiri (1.16.0) + nokogiri (1.16.2) mini_portile2 (~> 2.8.2) racc (~> 1.4) octokit (8.1.0) @@ -362,7 +362,7 @@ DEPENDENCIES gitlab_quality-test_tooling (~> 1.11.0) influxdb-client (~> 3.0) knapsack (~> 4.0) - nokogiri (~> 1.16) + nokogiri (~> 1.16, >= 1.16.2) octokit (~> 8.1.0) parallel (~> 1.24) parallel_tests (~> 4.4) diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index 8c6c4d29f62..99ccdd1d916 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -96,6 +96,10 @@ module QA element 'pipeline-container' end + view 'app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue' do + element 'artifacts-dropdown' + end + view 'app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue' do element 'apply-suggestion-dropdown' element 'commit-message-field' @@ -499,7 +503,7 @@ module QA def click_artifacts_dropdown_button wait_for_requests - within_element('pipeline-container') do + within_element('artifacts-dropdown') do click_element('base-dropdown-toggle') end end diff --git a/spec/deprecation_warnings.rb b/spec/deprecation_warnings.rb index abdd13ee8e7..3ebca87f3e1 100644 --- a/spec/deprecation_warnings.rb +++ b/spec/deprecation_warnings.rb @@ -7,13 +7,11 @@ return if Gitlab::Utils.to_boolean(ENV['SILENCE_DEPRECATIONS'], default: false) # to developers to ease upgrading to newer Ruby versions. Warning[:deprecated] = true -# rubocop:disable Layout/LineLength +# rubocop:disable Layout/LineLength -- Avoid multiline (x modifier) Regexp to keep it readable case RUBY_VERSION[/\d+\.\d+/, 0] -when '3.2' - warn "#{__FILE__}:#{__LINE__}: warning: Ignored warnings for Ruby < 3.2 are no longer necessary." -else +when '3.1' require 'warning' - # Ignore Ruby warnings until Ruby 3.2. + # These warnings only happen in Ruby 3.1 and are gone in Ruby 3.2. # ... ruby/3.1.3/lib/ruby/gems/3.1.0/gems/rspec-parameterized-table_syntax-1.0.0/lib/rspec/parameterized/table_syntax.rb:38: warning: Refinement#include is deprecated and will be removed in Ruby 3.2 Warning.ignore(%r{rspec-parameterized-table_syntax-1\.0\.0/lib/rspec/parameterized/table_syntax\.rb:\d+: warning: Refinement#include is deprecated}) diff --git a/spec/frontend/organizations/groups_and_projects/components/app_spec.js b/spec/frontend/organizations/groups_and_projects/components/app_spec.js index ebc84c9efb0..1f6a153a9e1 100644 --- a/spec/frontend/organizations/groups_and_projects/components/app_spec.js +++ b/spec/frontend/organizations/groups_and_projects/components/app_spec.js @@ -2,6 +2,8 @@ import { GlCollapsibleListbox, GlSorting } from '@gitlab/ui'; import App from '~/organizations/groups_and_projects/components/app.vue'; import GroupsView from '~/organizations/shared/components/groups_view.vue'; import ProjectsView from '~/organizations/shared/components/projects_view.vue'; +import NewGroupButton from '~/organizations/shared/components/new_group_button.vue'; +import NewProjectButton from '~/organizations/shared/components/new_project_button.vue'; import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '~/organizations/constants'; import { SORT_ITEMS } from '~/organizations/groups_and_projects/constants'; import { @@ -35,10 +37,19 @@ describe('GroupsAndProjectsApp', () => { }); }; + const findPageTitle = () => wrapper.findByText('Groups and projects'); const findFilteredSearchBar = () => wrapper.findComponent(FilteredSearchBar); const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); const findSort = () => wrapper.findComponent(GlSorting); const findProjectsView = () => wrapper.findComponent(ProjectsView); + const findNewGroupButton = () => wrapper.findComponent(NewGroupButton); + const findNewProjectButton = () => wrapper.findComponent(NewProjectButton); + + it('renders page title as Groups and projects', () => { + createComponent(); + + expect(findPageTitle().exists()).toBe(true); + }); describe.each` display | expectedComponent | expectedDisplayListboxSelectedProp @@ -101,6 +112,20 @@ describe('GroupsAndProjectsApp', () => { }); }); + describe('actions', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders NewProjectButton', () => { + expect(findNewProjectButton().exists()).toBe(true); + }); + + it('renders NewGroupButton with correct props', () => { + expect(findNewGroupButton().props()).toStrictEqual({ category: 'secondary' }); + }); + }); + it('renders sort dropdown with sort items and correct props', () => { createComponent(); diff --git a/spec/frontend/organizations/shared/components/groups_view_spec.js b/spec/frontend/organizations/shared/components/groups_view_spec.js index 55d2c36f625..b97dcf59aac 100644 --- a/spec/frontend/organizations/shared/components/groups_view_spec.js +++ b/spec/frontend/organizations/shared/components/groups_view_spec.js @@ -3,6 +3,7 @@ import Vue from 'vue'; import { GlEmptyState, GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui'; import GroupsView from '~/organizations/shared/components/groups_view.vue'; import { SORT_DIRECTION_ASC, SORT_ITEM_NAME } from '~/organizations/shared/constants'; +import NewGroupButton from '~/organizations/shared/components/new_group_button.vue'; import { formatGroups } from '~/organizations/shared/utils'; import groupsQuery from '~/organizations/shared/graphql/queries/groups.query.graphql'; import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue'; @@ -67,6 +68,7 @@ describe('GroupsView', () => { }; const findPagination = () => wrapper.findComponent(GlKeysetPagination); + const findNewGroupButton = () => wrapper.findComponent(NewGroupButton); afterEach(() => { mockApollo = null; @@ -81,51 +83,47 @@ describe('GroupsView', () => { }); describe('when API call is successful', () => { - describe('when there are no groups', () => { - const emptyHandler = jest.fn().mockResolvedValue({ - data: { - organization: { - id: defaultProvide.organizationGid, - groups: { - nodes: [], - pageInfo: pageInfoEmpty, + describe.each` + shouldShowEmptyStateButtons + ${false} + ${true} + `( + 'when there are no groups and `shouldShowEmptyStateButtons` is `$shouldShowEmptyStateButtons`', + ({ shouldShowEmptyStateButtons }) => { + const emptyHandler = jest.fn().mockResolvedValue({ + data: { + organization: { + id: defaultProvide.organizationGid, + groups: { + nodes: [], + pageInfo: pageInfoEmpty, + }, }, }, - }, - }); - - it('renders empty state without buttons by default', async () => { - createComponent({ handler: emptyHandler }); - - await waitForPromises(); - - expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({ - title: "You don't have any groups yet.", - description: - 'A group is a collection of several projects. If you organize your projects under a group, it works like a folder.', - svgHeight: 144, - svgPath: defaultProvide.groupsEmptyStateSvgPath, - primaryButtonLink: null, - primaryButtonText: null, }); - }); - describe('when `shouldShowEmptyStateButtons` is `true` and `groupsEmptyStateSvgPath` is set', () => { - it('renders empty state with buttons', async () => { + it(`renders empty state ${ + shouldShowEmptyStateButtons ? 'with' : 'without' + } buttons`, async () => { createComponent({ handler: emptyHandler, - propsData: { shouldShowEmptyStateButtons: true }, + propsData: { shouldShowEmptyStateButtons }, }); await waitForPromises(); expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({ - primaryButtonLink: defaultProvide.newGroupPath, - primaryButtonText: 'New group', + title: "You don't have any groups yet.", + description: + 'A group is a collection of several projects. If you organize your projects under a group, it works like a folder.', + svgHeight: 144, + svgPath: defaultProvide.groupsEmptyStateSvgPath, }); + + expect(findNewGroupButton().exists()).toBe(shouldShowEmptyStateButtons); }); - }); - }); + }, + ); describe('when there are groups', () => { beforeEach(() => { diff --git a/spec/frontend/organizations/shared/components/new_group_button_spec.js b/spec/frontend/organizations/shared/components/new_group_button_spec.js new file mode 100644 index 00000000000..0ca628478a5 --- /dev/null +++ b/spec/frontend/organizations/shared/components/new_group_button_spec.js @@ -0,0 +1,80 @@ +import { GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import NewGroupButton from '~/organizations/shared/components/new_group_button.vue'; + +describe('NewGroupButton', () => { + let wrapper; + + const defaultProvide = { + canCreateGroup: false, + newGroupPath: '', + }; + + const defaultProps = { + category: 'primary', + }; + + function createComponent({ provide = {}, props = {} } = {}) { + wrapper = shallowMount(NewGroupButton, { + provide: { + ...defaultProvide, + ...provide, + }, + propsData: { + ...defaultProps, + ...props, + }, + }); + } + + const findGlButton = () => wrapper.findComponent(GlButton); + + describe.each` + canCreateGroup | newGroupPath + ${false} | ${null} + ${false} | ${'/asdf'} + ${true} | ${null} + `( + 'when `canCreateGroup` is $canCreateGroup and `newGroupPath` is $newGroupPath', + ({ canCreateGroup, newGroupPath }) => { + beforeEach(() => { + createComponent({ provide: { canCreateGroup, newGroupPath } }); + }); + + it('renders nothing', () => { + expect(wrapper.html()).toBe(''); + }); + }, + ); + + describe('when `canCreateGroup` is true and `newGroupPath` is /asdf', () => { + const newGroupPath = '/asdf'; + + describe('with no category', () => { + beforeEach(() => { + createComponent({ + provide: { canCreateGroup: true, newGroupPath }, + props: { category: undefined }, + }); + }); + + it('renders GlButton correctly', () => { + expect(findGlButton().attributes('href')).toBe(newGroupPath); + expect(findGlButton().props('category')).toBe(defaultProps.category); + }); + }); + + describe('with set category', () => { + const category = 'secondary'; + + beforeEach(() => { + createComponent({ provide: { canCreateGroup: true, newGroupPath }, props: { category } }); + }); + + it('renders GlButton correctly', () => { + expect(findGlButton().attributes('href')).toBe(newGroupPath); + expect(findGlButton().props('category')).toBe(category); + }); + }); + }); +}); diff --git a/spec/frontend/organizations/shared/components/new_project_button_spec.js b/spec/frontend/organizations/shared/components/new_project_button_spec.js new file mode 100644 index 00000000000..31f6933d0c7 --- /dev/null +++ b/spec/frontend/organizations/shared/components/new_project_button_spec.js @@ -0,0 +1,77 @@ +import { GlButton } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import NewProjectButton from '~/organizations/shared/components/new_project_button.vue'; + +describe('NewProjectButton', () => { + let wrapper; + + const defaultProvide = { + canCreateProject: false, + newProjectPath: '', + hasGroups: false, + }; + + function createComponent({ provide = {} } = {}) { + wrapper = shallowMountExtended(NewProjectButton, { + provide: { + ...defaultProvide, + ...provide, + }, + }); + } + + const findTooltipContainer = () => wrapper.findByTestId('new-project-button-tooltip-container'); + const findGlButton = () => wrapper.findComponent(GlButton); + + describe.each` + canCreateProject | newProjectPath + ${false} | ${null} + ${false} | ${'/asdf'} + ${true} | ${null} + `( + 'when `canCreateProject` is $canCreateProject and `newProjectPath` is $newProjectPath', + ({ canCreateProject, newProjectPath }) => { + beforeEach(() => { + createComponent({ provide: { canCreateProject, newProjectPath } }); + }); + + it('renders nothing', () => { + expect(wrapper.html()).toBe(''); + }); + }, + ); + + describe('when `canCreateProject` is true and `newProjectPath` is /asdf', () => { + const newProjectPath = '/asdf'; + + beforeEach(() => { + createComponent({ provide: { canCreateProject: true, newProjectPath } }); + }); + + it('renders GlButton correctly', () => { + expect(findGlButton().attributes('href')).toBe(newProjectPath); + }); + }); + + describe.each` + hasGroups | disabled | tooltip + ${false} | ${'true'} | ${'Projects are hosted/created in groups. Before creating a project, you must create a group.'} + ${true} | ${undefined} | ${undefined} + `( + 'when `canCreateProject` is true , `newProjectPath` is /asdf, and hasGroups is $hasGroups', + ({ hasGroups, disabled, tooltip }) => { + beforeEach(() => { + createComponent({ + provide: { canCreateProject: true, newProjectPath: '/asdf', hasGroups }, + }); + }); + + it(`renders GlButton as ${disabled ? 'disabled' : 'not disabled'} with ${ + tooltip ? 'tooltip' : 'no tooltip' + }`, () => { + expect(findGlButton().attributes('disabled')).toBe(disabled); + expect(findTooltipContainer().attributes('title')).toBe(tooltip); + }); + }, + ); +}); diff --git a/spec/frontend/organizations/shared/components/projects_view_spec.js b/spec/frontend/organizations/shared/components/projects_view_spec.js index c605b777afc..83376a34df6 100644 --- a/spec/frontend/organizations/shared/components/projects_view_spec.js +++ b/spec/frontend/organizations/shared/components/projects_view_spec.js @@ -2,6 +2,7 @@ import VueApollo from 'vue-apollo'; import Vue from 'vue'; import { GlLoadingIcon, GlEmptyState, GlKeysetPagination } from '@gitlab/ui'; import ProjectsView from '~/organizations/shared/components/projects_view.vue'; +import NewProjectButton from '~/organizations/shared/components/new_project_button.vue'; import projectsQuery from '~/organizations/shared/graphql/queries/projects.query.graphql'; import { formatProjects } from '~/organizations/shared/utils'; import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue'; @@ -66,6 +67,7 @@ describe('ProjectsView', () => { const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findProjectsList = () => wrapper.findComponent(ProjectsList); + const findNewProjectButton = () => wrapper.findComponent(NewProjectButton); afterEach(() => { mockApollo = null; @@ -80,51 +82,47 @@ describe('ProjectsView', () => { }); describe('when API call is successful', () => { - describe('when there are no projects', () => { - const emptyHandler = jest.fn().mockResolvedValue({ - data: { - organization: { - id: defaultProvide.organizationGid, - projects: { - nodes: [], - pageInfo: pageInfoEmpty, + describe.each` + shouldShowEmptyStateButtons + ${false} + ${true} + `( + 'when there are no projects and `shouldShowEmptyStateButtons` is `$shouldShowEmptyStateButtons`', + ({ shouldShowEmptyStateButtons }) => { + const emptyHandler = jest.fn().mockResolvedValue({ + data: { + organization: { + id: defaultProvide.organizationGid, + projects: { + nodes: [], + pageInfo: pageInfoEmpty, + }, }, }, - }, - }); - - it('renders empty state without buttons by default', async () => { - createComponent({ handler: emptyHandler }); - - await waitForPromises(); - - expect(findEmptyState().props()).toMatchObject({ - title: "You don't have any projects yet.", - description: - 'Projects are where you can store your code, access issues, wiki, and other features of GitLab.', - svgHeight: 144, - svgPath: defaultProvide.projectsEmptyStateSvgPath, - primaryButtonLink: null, - primaryButtonText: null, }); - }); - describe('when `shouldShowEmptyStateButtons` is `true` and `projectsEmptyStateSvgPath` is set', () => { - it('renders empty state with buttons', async () => { + it(`renders empty state ${ + shouldShowEmptyStateButtons ? 'with' : 'without' + } buttons`, async () => { createComponent({ handler: emptyHandler, - propsData: { shouldShowEmptyStateButtons: true }, + propsData: { shouldShowEmptyStateButtons }, }); await waitForPromises(); expect(findEmptyState().props()).toMatchObject({ - primaryButtonLink: defaultProvide.newProjectPath, - primaryButtonText: 'New project', + title: "You don't have any projects yet.", + description: + 'Projects are where you can store your code, access issues, wiki, and other features of GitLab.', + svgHeight: 144, + svgPath: defaultProvide.projectsEmptyStateSvgPath, }); + + expect(findNewProjectButton().exists()).toBe(shouldShowEmptyStateButtons); }); - }); - }); + }, + ); describe('when there are projects', () => { beforeEach(() => { diff --git a/spec/helpers/jira_connect_helper_spec.rb b/spec/helpers/jira_connect_helper_spec.rb index b7c25320a0e..2f2530f89f1 100644 --- a/spec/helpers/jira_connect_helper_spec.rb +++ b/spec/helpers/jira_connect_helper_spec.rb @@ -72,6 +72,22 @@ RSpec.describe JiraConnectHelper, feature_category: :integrations do oauth_token_path: '/oauth/token' ) end + + context 'with relative_url_root' do + let_it_be(:installation) { create(:jira_connect_installation, instance_url: 'https://gitlab.example.com/gitlab') } + + before do + stub_config_setting(relative_url_root: '/gitlab') + allow(Rails.application.routes).to receive(:default_url_options).and_return(script_name: '/gitlab') + end + + it 'points urls to the self-managed instance' do + expect(parsed_oauth_metadata).to include( + oauth_authorize_url: start_with('https://gitlab.example.com/gitlab/oauth/authorize?'), + oauth_token_path: '/gitlab/oauth/token' + ) + end + end end end diff --git a/spec/helpers/organizations/organization_helper_spec.rb b/spec/helpers/organizations/organization_helper_spec.rb index 1ec95e4503a..4404ebbc927 100644 --- a/spec/helpers/organizations/organization_helper_spec.rb +++ b/spec/helpers/organizations/organization_helper_spec.rb @@ -3,6 +3,9 @@ require 'spec_helper' RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do + include Devise::Test::ControllerHelpers + + let_it_be(:user) { build_stubbed(:user) } let_it_be(:organization_detail) { build_stubbed(:organization_detail, description_html: 'description') } let_it_be(:organization) { organization_detail.organization } let_it_be(:organization_gid) { 'gid://gitlab/Organizations::Organization/1' } @@ -27,6 +30,31 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do allow(helper).to receive(:image_path).with(groups_empty_state_svg_path).and_return(groups_empty_state_svg_path) allow(helper).to receive(:image_path).with(projects_empty_state_svg_path).and_return(projects_empty_state_svg_path) allow(helper).to receive(:preview_markdown_organizations_path).and_return(preview_markdown_organizations_path) + allow(helper).to receive(:current_user).and_return(user) + end + + shared_examples 'includes that the user can create a group' do |method| + it 'returns expected json' do + expect( + Gitlab::Json.parse(helper.send(method, organization)) + ).to include('can_create_group' => true) + end + end + + shared_examples 'includes that the user can create a project' do |method| + it 'returns expected json' do + expect( + Gitlab::Json.parse(helper.send(method, organization)) + ).to include('can_create_project' => true) + end + end + + shared_examples 'includes that the organization has groups' do |method| + it 'returns expected json' do + expect( + Gitlab::Json.parse(helper.send(method, organization)) + ).to include('has_groups' => true) + end end describe '#organization_layout_nav' do @@ -68,13 +96,38 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do .and_return(groups_and_projects_organization_path) end - it 'returns expected json' do + context 'when the user can create a group' do + before do + allow(helper).to receive(:can?).with(user, :create_group, organization).and_return(true) + end + + include_examples 'includes that the user can create a group', 'organization_show_app_data' + end + + context 'when the user can create a project' do + before do + allow(user).to receive(:can_create_project?).and_return(true) + end + + include_examples 'includes that the user can create a project', 'organization_show_app_data' + end + + context 'when the organization has groups' do + before do + allow(helper).to receive(:has_groups?).and_return(true) + end + + include_examples 'includes that the organization has groups', 'organization_show_app_data' + end + + it "includes all other non-conditional data" do expect(organization).to receive(:avatar_url).with(size: 128).and_return('avatar.jpg') + expect( Gitlab::Json.parse( helper.organization_show_app_data(organization) ) - ).to eq( + ).to include( { 'organization_gid' => organization_gid, 'organization' => { @@ -99,12 +152,36 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do end describe '#organization_groups_and_projects_app_data' do - it 'returns expected json' do + context 'when the user can create a group' do + before do + allow(helper).to receive(:can?).with(user, :create_group, organization).and_return(true) + end + + include_examples 'includes that the user can create a group', 'organization_groups_and_projects_app_data' + end + + context 'when the user can create a project' do + before do + allow(user).to receive(:can_create_project?).and_return(true) + end + + include_examples 'includes that the user can create a project', 'organization_groups_and_projects_app_data' + end + + context 'when the organization has groups' do + before do + allow(helper).to receive(:has_groups?).and_return(true) + end + + include_examples 'includes that the organization has groups', 'organization_groups_and_projects_app_data' + end + + it "includes all other non-conditional data" do expect( Gitlab::Json.parse( helper.organization_groups_and_projects_app_data(organization) ) - ).to eq( + ).to include( { 'organization_gid' => organization_gid, 'new_group_path' => new_group_path, diff --git a/yarn.lock b/yarn.lock index f6c9bbbc26f..9d0460a9ac9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1339,10 +1339,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.7.3.tgz#9ea641146436da388ffbad25d7f2abe0df52c235" integrity sha512-NMV++7Ew1FSBDN1xiZaauU9tfeSfgDHcOLpn+8bGpP+O5orUPm2Eu66R5eC5gkjBPaXosNAxNWtriee+aFk4+g== -"@gitlab/web-ide@^0.0.1-dev-20240201215504": - version "0.0.1-dev-20240201215504" - resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20240201215504.tgz#e1c7fd4c2be48e956a6c54dd752541a7ccb5bf3b" - integrity sha512-nw5BApPI6l6F4CKiLM0RRbXvM1QACxFZc15ZJh4/R+8FaZ3pVQ1CXc/CVvLwrjJVphO5rfm98A7FV7Cjn1zwhA== +"@gitlab/web-ide@^0.0.1-dev-20240206230318": + version "0.0.1-dev-20240206230318" + resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20240206230318.tgz#3dd52302fa521550f1f3b8f98e6041ef35ac1ff0" + integrity sha512-/R1+NA99j2Ns5/WwP1jx6jRkEt0QLdcZ05Fvylrci1NSIXa7andWyIweCY2NpERBJrwXXFZ2MBKf8hEULI/roQ== "@graphql-eslint/eslint-plugin@3.20.1": version "3.20.1"