From 881c9469903d5520cd1d51b47f620856f30c253e Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 21 Jan 2022 00:12:56 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- ...ctionable Insight - Exploration needed.md} | 10 +- .../Actionable Insight - Product change.md | 32 ++++++ .../pages/admin/runners/show/index.js | 3 + .../admin_runner_show_app.vue | 74 ++++++++++++ .../runner/admin_runner_show/index.js | 32 ++++++ .../components/cells/runner_actions_cell.vue | 13 +-- .../runner/components/runner_detail.vue | 50 +++++++++ .../runner/components/runner_details.vue | 92 +++++++++++++++ .../runner/components/runner_edit_button.vue | 26 +++++ .../runner/components/runner_header.vue | 52 ++++++--- .../runner/components/runner_tags.vue | 4 +- .../runner_details_shared.fragment.graphql | 7 ++ .../runner_instructions.vue | 2 +- app/graphql/types/ci/runner_type.rb | 20 ++++ app/views/admin/runners/show.html.haml | 2 +- .../ci/runner/_how_to_setup_runner.html.haml | 2 +- .../merge_requests/_mr_title.html.haml | 2 +- doc/administration/auth/ldap/index.md | 4 +- .../auth/ldap/ldap_synchronization.md | 4 + doc/api/graphql/reference/index.md | 1 + doc/development/testing_guide/review_apps.md | 2 + .../templates/linux/install.sh | 6 +- .../templates/osx/install.sh | 6 +- .../templates/windows/install.ps1 | 8 +- locale/gitlab.pot | 15 ++- spec/features/admin/admin_runners_spec.rb | 38 ++++++- .../user_views_open_merge_request_spec.rb | 26 +++++ .../admin_runner_edit_app_spec.js | 5 +- .../admin_runner_show_app_spec.js | 97 ++++++++++++++++ .../cells/runner_actions_cell_spec.js | 3 +- .../runner/components/runner_details_spec.js | 106 ++++++++++++++++++ .../components/runner_edit_button_spec.js | 41 +++++++ .../runner/components/runner_header_spec.js | 44 ++++++-- .../runner/components/runner_list_spec.js | 3 +- .../components/runner_update_form_spec.js | 14 ++- .../runner_instructions_spec.js | 4 +- spec/graphql/types/ci/runner_type_spec.rb | 1 + spec/requests/api/graphql/ci/runner_spec.rb | 31 ++++- 38 files changed, 810 insertions(+), 72 deletions(-) rename .gitlab/issue_templates/{Actionable Insight.md => Actionable Insight - Exploration needed.md} (78%) create mode 100644 .gitlab/issue_templates/Actionable Insight - Product change.md create mode 100644 app/assets/javascripts/pages/admin/runners/show/index.js create mode 100644 app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue create mode 100644 app/assets/javascripts/runner/admin_runner_show/index.js create mode 100644 app/assets/javascripts/runner/components/runner_detail.vue create mode 100644 app/assets/javascripts/runner/components/runner_details.vue create mode 100644 app/assets/javascripts/runner/components/runner_edit_button.vue create mode 100644 spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js create mode 100644 spec/frontend/runner/components/runner_details_spec.js create mode 100644 spec/frontend/runner/components/runner_edit_button_spec.js diff --git a/.gitlab/issue_templates/Actionable Insight.md b/.gitlab/issue_templates/Actionable Insight - Exploration needed.md similarity index 78% rename from .gitlab/issue_templates/Actionable Insight.md rename to .gitlab/issue_templates/Actionable Insight - Exploration needed.md index f4724d66a1b..76316fc626d 100644 --- a/.gitlab/issue_templates/Actionable Insight.md +++ b/.gitlab/issue_templates/Actionable Insight - Exploration needed.md @@ -1,13 +1,15 @@ - + ### Insight - + ### Supporting evidence ### Action - + ### Resources @@ -26,5 +28,5 @@ /confidential -/label ~"Actionable Insight" +/label ~"Actionable Insight::Exploration needed" diff --git a/.gitlab/issue_templates/Actionable Insight - Product change.md b/.gitlab/issue_templates/Actionable Insight - Product change.md new file mode 100644 index 00000000000..7277cb7e9f2 --- /dev/null +++ b/.gitlab/issue_templates/Actionable Insight - Product change.md @@ -0,0 +1,32 @@ + + +### Insight + + +### Supporting evidence + + +### Action + + +### Resources + + +- :dove: [Dovetail project](Paste URL for Dovetail project here) +- :mag: [Research issue](Paste URL for research issue here) +- :footprints: [Follow-up issue or epic](Paste URL for follow-up issue or epic here) + +### Tasks + +- [ ] Assign this issue to the appropriate Product Manager, Product Designer, or UX Researcher. +- [ ] Add the appropriate `Group` (such as `~"group::source code"`) label to the issue. This helps identify and track actionable insights at the group level. +- [ ] Link this issue back to the original research issue in the GitLab UX Research project and the Dovetail project. +- [ ] Adjust confidentiality of this issue if applicable + + + +/confidential +/label ~"Actionable Insight::Product change" + diff --git a/app/assets/javascripts/pages/admin/runners/show/index.js b/app/assets/javascripts/pages/admin/runners/show/index.js new file mode 100644 index 00000000000..f76f3a2430d --- /dev/null +++ b/app/assets/javascripts/pages/admin/runners/show/index.js @@ -0,0 +1,3 @@ +import { initAdminRunnerShow } from '~/runner/admin_runner_show'; + +initAdminRunnerShow(); diff --git a/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue b/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue new file mode 100644 index 00000000000..a7ae0213661 --- /dev/null +++ b/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue @@ -0,0 +1,74 @@ + + diff --git a/app/assets/javascripts/runner/admin_runner_show/index.js b/app/assets/javascripts/runner/admin_runner_show/index.js new file mode 100644 index 00000000000..a781898cf8d --- /dev/null +++ b/app/assets/javascripts/runner/admin_runner_show/index.js @@ -0,0 +1,32 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import AdminRunnerShowApp from './admin_runner_show_app.vue'; + +Vue.use(VueApollo); + +export const initAdminRunnerShow = (selector = '#js-admin-runner-show') => { + const el = document.querySelector(selector); + + if (!el) { + return null; + } + + const { runnerId } = el.dataset; + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + return new Vue({ + el, + apolloProvider, + render(h) { + return h(AdminRunnerShowApp, { + props: { + runnerId, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue index 0934508c87f..19f6598c62c 100644 --- a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue +++ b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue @@ -6,9 +6,9 @@ import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphq import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql'; import { captureException } from '~/runner/sentry_utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import RunnerEditButton from '../runner_edit_button.vue'; import RunnerDeleteModal from '../runner_delete_modal.vue'; -const I18N_EDIT = __('Edit'); const I18N_PAUSE = __('Pause'); const I18N_RESUME = __('Resume'); const I18N_DELETE = s__('Runners|Delete runner'); @@ -19,6 +19,7 @@ export default { components: { GlButton, GlButtonGroup, + RunnerEditButton, RunnerDeleteModal, }, directives: { @@ -147,7 +148,6 @@ export default { captureException({ error, component: this.$options.name }); }, }, - I18N_EDIT, I18N_DELETE, }; @@ -161,14 +161,7 @@ export default { See https://gitlab.com/gitlab-org/gitlab/-/issues/334802 --> - + +import { __ } from '~/locale'; + +/** + * Usage: + * + * With a `value` prop: + * + * + * + * Or a `value` slot: + * + * + * + * + * + */ +export default { + props: { + label: { + type: String, + required: true, + }, + value: { + type: String, + default: null, + required: false, + }, + emptyValue: { + type: String, + default: __('None'), + required: false, + }, + }, +}; + + + diff --git a/app/assets/javascripts/runner/components/runner_details.vue b/app/assets/javascripts/runner/components/runner_details.vue new file mode 100644 index 00000000000..56d1109f0f5 --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_details.vue @@ -0,0 +1,92 @@ + + + diff --git a/app/assets/javascripts/runner/components/runner_edit_button.vue b/app/assets/javascripts/runner/components/runner_edit_button.vue new file mode 100644 index 00000000000..b115be09e69 --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_edit_button.vue @@ -0,0 +1,26 @@ + + + diff --git a/app/assets/javascripts/runner/components/runner_header.vue b/app/assets/javascripts/runner/components/runner_header.vue index 09f58df7bd0..295162e954a 100644 --- a/app/assets/javascripts/runner/components/runner_header.vue +++ b/app/assets/javascripts/runner/components/runner_header.vue @@ -1,19 +1,23 @@ diff --git a/app/assets/javascripts/runner/components/runner_tags.vue b/app/assets/javascripts/runner/components/runner_tags.vue index 8da5e33076f..797d2a35b2c 100644 --- a/app/assets/javascripts/runner/components/runner_tags.vue +++ b/app/assets/javascripts/runner/components/runner_tags.vue @@ -20,7 +20,7 @@ export default { }; diff --git a/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql index 8e968343b9b..60771af51f4 100644 --- a/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql +++ b/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql @@ -11,4 +11,11 @@ fragment RunnerDetailsShared on CiRunner { tagList createdAt status(legacyMode: null) + contactedAt + version + editAdminUrl + userPermissions { + updateRunner + deleteRunner + } } diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue index d886a67fff7..5d144c0d699 100644 --- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue +++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue @@ -13,7 +13,7 @@ export default { }, modalId: 'runner-instructions-modal', i18n: { - buttonText: s__('Runners|Show Runner installation instructions'), + buttonText: s__('Runners|Show runner installation instructions'), }, data() { return { diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb index 4fe65734911..ee2230eb24b 100644 --- a/app/graphql/types/ci/runner_type.rb +++ b/app/graphql/types/ci/runner_type.rb @@ -63,6 +63,8 @@ module Types description: 'Executor last advertised by the runner.', method: :executor_name, feature_flag: :graphql_ci_runner_executor + field :groups, ::Types::GroupType.connection_type, null: true, + description: 'Groups the runner is associated with. For group runners only.' def job_count # We limit to 1 above the JOB_COUNT_LIMIT to indicate that more items exist after JOB_COUNT_LIMIT @@ -92,6 +94,24 @@ module Types end end end + + def groups + BatchLoader::GraphQL.for(runner.id).batch(key: :runner_groups) do |runner_ids, loader, args| + runner_and_namespace_ids = + ::Ci::RunnerNamespace + .where(runner_id: runner_ids) + .pluck(:runner_id, :namespace_id) + + group_ids_by_runner_id = runner_and_namespace_ids.group_by(&:first).transform_values { |v| v.pluck(1) } + group_ids = runner_and_namespace_ids.pluck(1).uniq + + groups = Group.where(id: group_ids).index_by(&:id) + + runner_ids.each do |runner_id| + loader.call(runner_id, group_ids_by_runner_id[runner_id]&.map { |group_id| groups[group_id] }) + end + end + end # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 7b4390ae463..5c4a7026f50 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -5,4 +5,4 @@ - page_title title - add_to_breadcrumbs _('Runners'), admin_runners_path --# Empty view in development behind feature flag runner_read_only_admin_view +#js-admin-runner-show{ data: {runner_id: @runner.id} } diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml index c872ee481ad..f0a9936112b 100644 --- a/app/views/ci/runner/_how_to_setup_runner.html.haml +++ b/app/views/ci/runner/_how_to_setup_runner.html.haml @@ -1,6 +1,6 @@ - link = link_to _("Install GitLab Runner and ensure it's running."), 'https://docs.gitlab.com/runner/install/', target: '_blank', rel: 'noopener noreferrer' .gl-mb-3 - %h5= _("Set up a %{type} Runner for a project") % { type: type } + %h5= _("Set up a %{type} runner for a project") % { type: type } %ol %li = link.html_safe diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index 9d5d1de1005..f2a271da771 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -2,7 +2,7 @@ - can_update_merge_request = can?(current_user, :update_merge_request, @merge_request) - can_reopen_merge_request = can?(current_user, :reopen_merge_request, @merge_request) - are_close_and_open_buttons_hidden = merge_request_button_hidden?(@merge_request, true) && merge_request_button_hidden?(@merge_request, false) -- cache_key = [@project, @merge_request, can_update_merge_request, can_reopen_merge_request, are_close_and_open_buttons_hidden] +- cache_key = [@project, @merge_request, can_update_merge_request, can_reopen_merge_request, are_close_and_open_buttons_hidden, current_user&.preferred_language] = cache(cache_key, expires_in: 1.day) do - if @merge_request.closed_or_merged_without_fork? diff --git a/doc/administration/auth/ldap/index.md b/doc/administration/auth/ldap/index.md index b773281b216..722bc25746b 100644 --- a/doc/administration/auth/ldap/index.md +++ b/doc/administration/auth/ldap/index.md @@ -468,7 +468,7 @@ If initially your LDAP configuration looked like: password: '123' ``` -1. Edit `/etc/gitlab/gitlab.rb` and remove the settings for `user_bn` and `password`. +1. Edit `/etc/gitlab/gitlab.rb` and remove the settings for `user_dn` and `password`. 1. [Reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. @@ -502,7 +502,7 @@ If initially your LDAP configuration looked like: password: '123' ``` -1. Edit `config/gitlab.yaml` and remove the settings for `user_bn` and `password`. +1. Edit `config/gitlab.yaml` and remove the settings for `user_dn` and `password`. 1. [Restart GitLab](../../restart_gitlab.md#installations-from-source) for the changes to take effect. diff --git a/doc/administration/auth/ldap/ldap_synchronization.md b/doc/administration/auth/ldap/ldap_synchronization.md index 3329ea8e9cc..0d6454fe108 100644 --- a/doc/administration/auth/ldap/ldap_synchronization.md +++ b/doc/administration/auth/ldap/ldap_synchronization.md @@ -347,3 +347,7 @@ These metrics are meant to provide a baseline and performance may vary based on any number of factors. This benchmark was extreme and most instances don't have near this many users or groups. Disk speed, database performance, network and LDAP server response time affects these metrics. + +## Troubleshooting + +See our [administrator guide to troubleshooting LDAP](ldap-troubleshooting.md). diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index bb069e2ad66..7c1f16a20dd 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -9022,6 +9022,7 @@ Represents the total number of issues and their weights for a particular day. | `description` | [`String`](#string) | Description of the runner. | | `editAdminUrl` | [`String`](#string) | Admin form URL of the runner. Only available for administrators. | | `executorName` | [`String`](#string) | Executor last advertised by the runner. Available only when feature flag `graphql_ci_runner_executor` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. | +| `groups` | [`GroupConnection`](#groupconnection) | Groups the runner is associated with. For group runners only. (see [Connections](#connections)) | | `id` | [`CiRunnerID!`](#cirunnerid) | ID of the runner. | | `ipAddress` | [`String`](#string) | IP address of the runner. | | `jobCount` | [`Int`](#int) | Number of jobs processed by the runner (limited to 1000, plus one to indicate that more items exist). | diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md index aef83109b9b..5ae6b9e5a59 100644 --- a/doc/development/testing_guide/review_apps.md +++ b/doc/development/testing_guide/review_apps.md @@ -30,6 +30,8 @@ You can also manually start the `review-qa-all`: it runs the full QA suite. After the end-to-end test runs have finished, [Allure reports](https://github.com/allure-framework/allure2) are generated and published by the `allure-report-qa-smoke`, `allure-report-qa-reliable`, and `allure-report-qa-all` jobs. A comment with links to the reports are added to the merge request. +Errors can be found in the `gitlab-review-apps` Sentry project and [filterable by Review App URL](https://sentry.gitlab.net/gitlab/gitlab-review-apps/?query=url%3A%22https%3A%2F%2Fgitlab-review-require-ve-u92nn2.gitlab-review.app%2F%22). + ## Performance Metrics On every [pipeline](https://gitlab.com/gitlab-org/gitlab/pipelines/125315730) in the `qa` stage, the diff --git a/lib/gitlab/ci/runner_instructions/templates/linux/install.sh b/lib/gitlab/ci/runner_instructions/templates/linux/install.sh index 6c8a0796d23..2c55144e82d 100644 --- a/lib/gitlab/ci/runner_instructions/templates/linux/install.sh +++ b/lib/gitlab/ci/runner_instructions/templates/linux/install.sh @@ -1,12 +1,12 @@ # Download the binary for your system sudo curl -L --output /usr/local/bin/gitlab-runner ${GITLAB_CI_RUNNER_DOWNLOAD_LOCATION} -# Give it permissions to execute +# Give it permission to execute sudo chmod +x /usr/local/bin/gitlab-runner -# Create a GitLab CI user +# Create a GitLab user sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash -# Install and run as service +# Install and run as a service sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner sudo gitlab-runner start diff --git a/lib/gitlab/ci/runner_instructions/templates/osx/install.sh b/lib/gitlab/ci/runner_instructions/templates/osx/install.sh index de4ee3e52fc..76c893bacfc 100644 --- a/lib/gitlab/ci/runner_instructions/templates/osx/install.sh +++ b/lib/gitlab/ci/runner_instructions/templates/osx/install.sh @@ -1,11 +1,11 @@ # Download the binary for your system sudo curl --output /usr/local/bin/gitlab-runner ${GITLAB_CI_RUNNER_DOWNLOAD_LOCATION} -# Give it permissions to execute +# Give it permission to execute sudo chmod +x /usr/local/bin/gitlab-runner -# The rest of commands execute as the user who will run the Runner -# Register the Runner (steps below), then run +# The rest of the commands execute as the user who will run the runner +# Register the runner (steps below), then run cd ~ gitlab-runner install gitlab-runner start diff --git a/lib/gitlab/ci/runner_instructions/templates/windows/install.ps1 b/lib/gitlab/ci/runner_instructions/templates/windows/install.ps1 index dc37f88543c..019363fc3f7 100644 --- a/lib/gitlab/ci/runner_instructions/templates/windows/install.ps1 +++ b/lib/gitlab/ci/runner_instructions/templates/windows/install.ps1 @@ -1,13 +1,13 @@ # Run PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/windows-powershell/starting-windows-powershell?view=powershell-7#with-administrative-privileges-run-as-administrator -# Create a folder somewhere in your system ex.: C:\GitLab-Runner +# Create a folder somewhere on your system, for example: C:\GitLab-Runner New-Item -Path 'C:\GitLab-Runner' -ItemType Directory -# Enter the folder +# Change to the folder cd 'C:\GitLab-Runner' -# Dowload binary +# Download binary Invoke-WebRequest -Uri "${GITLAB_CI_RUNNER_DOWNLOAD_LOCATION}" -OutFile "gitlab-runner.exe" -# Register the Runner (steps below), then run +# Register the runner (steps below), then run .\gitlab-runner.exe install .\gitlab-runner.exe start diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7a20c44ccd9..d65c672182b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -30738,6 +30738,9 @@ msgstr "" msgid "Runners|Command to register runner" msgstr "" +msgid "Runners|Configuration" +msgstr "" + msgid "Runners|Copy instructions" msgstr "" @@ -30756,6 +30759,9 @@ msgstr "" msgid "Runners|Description" msgstr "" +msgid "Runners|Details" +msgstr "" + msgid "Runners|Download and install binary" msgstr "" @@ -30906,13 +30912,16 @@ msgstr "" msgid "Runners|Runners in this group: %{groupRunnersCount}" msgstr "" +msgid "Runners|Runs untagged jobs" +msgstr "" + msgid "Runners|Shared runners are available to every project in a GitLab instance. If you want a runner to build only specific projects, restrict the project in the table below. After you restrict a runner to a project, you cannot change it back to a shared runner." msgstr "" -msgid "Runners|Show Runner installation instructions" +msgid "Runners|Show runner installation and registration instructions" msgstr "" -msgid "Runners|Show runner installation and registration instructions" +msgid "Runners|Show runner installation instructions" msgstr "" msgid "Runners|Something went wrong while fetching runner data." @@ -32743,7 +32752,7 @@ msgstr "" msgid "Set up Jira Integration" msgstr "" -msgid "Set up a %{type} Runner for a project" +msgid "Set up a %{type} runner for a project" msgstr "" msgid "Set up a hardware device as a second factor to sign in." diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index ceb91b86876..37aa1433994 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -476,6 +476,42 @@ RSpec.describe "Admin Runners" do end end + describe "Runner show page", :js do + let(:runner) do + create( + :ci_runner, + description: 'runner-foo', + version: '14.0', + ip_address: '127.0.0.1', + tag_list: %w(tag1 tag2) + ) + end + + before do + visit admin_runner_path(runner) + end + + describe 'runner show page breadcrumbs' do + it 'contains the current runner id and token' do + page.within '[data-testid="breadcrumb-links"]' do + expect(page.find('h2')).to have_link("##{runner.id} (#{runner.short_sha})") + end + end + end + + it 'shows runner details' do + aggregate_failures do + expect(page).to have_content 'Description runner-foo' + expect(page).to have_content 'Last contact Never contacted' + expect(page).to have_content 'Version 14.0' + expect(page).to have_content 'IP Address 127.0.0.1' + expect(page).to have_content 'Configuration Runs untagged jobs' + expect(page).to have_content 'Maximum job timeout None' + expect(page).to have_content 'Tags tag1 tag2' + end + end + end + describe "Runner edit page" do let(:runner) { create(:ci_runner) } @@ -487,7 +523,7 @@ RSpec.describe "Admin Runners" do wait_for_requests end - describe 'runner page breadcrumbs' do + describe 'runner edit page breadcrumbs' do it 'contains the current runner id and token' do page.within '[data-testid="breadcrumb-links"]' do expect(page).to have_link("##{runner.id} (#{runner.short_sha})") diff --git a/spec/features/merge_request/user_views_open_merge_request_spec.rb b/spec/features/merge_request/user_views_open_merge_request_spec.rb index b5a973a53c0..a145bcb976b 100644 --- a/spec/features/merge_request/user_views_open_merge_request_spec.rb +++ b/spec/features/merge_request/user_views_open_merge_request_spec.rb @@ -128,4 +128,30 @@ RSpec.describe 'User views an open merge request' do expect(find("[data-testid='ref-name']")[:title]).to eq(source_branch) end end + + context 'when user preferred language has changed', :use_clean_rails_memory_store_fragment_caching do + let(:project) { create(:project, :public, :repository) } + let(:user) { create(:user) } + + before do + project.add_maintainer(user) + sign_in(user) + end + + it 'renders edit button in preferred language' do + visit(merge_request_path(merge_request)) + + page.within('.detail-page-header-actions') do + expect(page).to have_link('Edit') + end + + user.update!(preferred_language: 'de') + + visit(merge_request_path(merge_request)) + + page.within('.detail-page-header-actions') do + expect(page).to have_link('Bearbeiten') + end + end + end end diff --git a/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js index ad0bce5c9af..9a9438417ee 100644 --- a/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js +++ b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js @@ -55,10 +55,11 @@ describe('AdminRunnerEditApp', () => { expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId }); }); - it('displays the runner id', async () => { + it('displays the runner id and creation date', async () => { await createComponentWithApollo({ mountFn: mount }); - expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId} created`); + expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`); + expect(findRunnerHeader().text()).toContain('created'); }); it('displays the runner type and status', async () => { diff --git a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js new file mode 100644 index 00000000000..61a4f7a443c --- /dev/null +++ b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js @@ -0,0 +1,97 @@ +import Vue from 'vue'; +import { mount, shallowMount } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createAlert } from '~/flash'; + +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import RunnerHeader from '~/runner/components/runner_header.vue'; +import RunnerDetails from '~/runner/components/runner_details.vue'; +import getRunnerQuery from '~/runner/graphql/get_runner.query.graphql'; +import AdminRunnerShowApp from '~/runner/admin_runner_show/admin_runner_show_app.vue'; +import { captureException } from '~/runner/sentry_utils'; + +import { runnerData } from '../mock_data'; + +jest.mock('~/flash'); +jest.mock('~/runner/sentry_utils'); + +const mockRunnerGraphqlId = runnerData.data.runner.id; +const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`; + +Vue.use(VueApollo); + +describe('AdminRunnerShowApp', () => { + let wrapper; + let mockRunnerQuery; + + const findRunnerHeader = () => wrapper.findComponent(RunnerHeader); + const findRunnerDetails = () => wrapper.findComponent(RunnerDetails); + + const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => { + wrapper = mountFn(AdminRunnerShowApp, { + apolloProvider: createMockApollo([[getRunnerQuery, mockRunnerQuery]]), + propsData: { + runnerId: mockRunnerId, + ...props, + }, + }); + + return waitForPromises(); + }; + + beforeEach(() => { + mockRunnerQuery = jest.fn().mockResolvedValue(runnerData); + }); + + afterEach(() => { + mockRunnerQuery.mockReset(); + wrapper.destroy(); + }); + + describe('When showing runner details', () => { + beforeEach(async () => { + await createComponent({ mountFn: mount }); + }); + + it('expect GraphQL ID to be requested', async () => { + expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId }); + }); + + it('displays the runner header', async () => { + expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`); + }); + + it('shows basic runner details', async () => { + const expected = `Details + Description Instance runner + Last contact Never contacted + Version 1.0.0 + IP Address 127.0.0.1 + Configuration Runs untagged jobs + Maximum job timeout None + Tags None`.replace(/\s+/g, ' '); + + expect(findRunnerDetails().text()).toMatchInterpolatedText(expected); + }); + }); + + describe('When there is an error', () => { + beforeEach(async () => { + mockRunnerQuery = jest.fn().mockRejectedValueOnce(new Error('Error!')); + await createComponent(); + }); + + it('error is reported to sentry', () => { + expect(captureException).toHaveBeenCalledWith({ + error: new Error('Network error: Error!'), + component: 'AdminRunnerShowApp', + }); + }); + + it('error is shown to the user', () => { + expect(createAlert).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js index 5411214bcf1..89961fdb162 100644 --- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js +++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js @@ -9,6 +9,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { captureException } from '~/runner/sentry_utils'; import RunnerActionCell from '~/runner/components/cells/runner_actions_cell.vue'; +import RunnerEditButton from '~/runner/components/runner_edit_button.vue'; import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue'; import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql'; import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql'; @@ -33,7 +34,7 @@ describe('RunnerTypeCell', () => { const runnerDeleteMutationHandler = jest.fn(); const runnerActionsUpdateMutationHandler = jest.fn(); - const findEditBtn = () => wrapper.findByTestId('edit-runner'); + const findEditBtn = () => wrapper.findComponent(RunnerEditButton); const findToggleActiveBtn = () => wrapper.findByTestId('toggle-active-runner'); const findRunnerDeleteModal = () => wrapper.findComponent(RunnerDeleteModal); const findDeleteBtn = () => wrapper.findByTestId('delete-runner'); diff --git a/spec/frontend/runner/components/runner_details_spec.js b/spec/frontend/runner/components/runner_details_spec.js new file mode 100644 index 00000000000..c9afd37492f --- /dev/null +++ b/spec/frontend/runner/components/runner_details_spec.js @@ -0,0 +1,106 @@ +import { GlSprintf, GlIntersperse } from '@gitlab/ui'; +import { createWrapper, ErrorWrapper } from '@vue/test-utils'; +import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; +import { useFakeDate } from 'helpers/fake_date'; +import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner/constants'; + +import RunnerDetails from '~/runner/components/runner_details.vue'; +import RunnerDetail from '~/runner/components/runner_detail.vue'; + +import { runnerData } from '../mock_data'; + +const mockRunner = runnerData.data.runner; + +describe('RunnerDetails', () => { + let wrapper; + const mockNow = '2021-01-15T12:00:00Z'; + const mockOneHourAgo = '2021-01-15T11:00:00Z'; + + useFakeDate(mockNow); + + /** + * Find the definition (
) that corresponds to this term (
) + * @param {string} dtLabel - Label for this value + * @returns Wrapper + */ + const findDd = (dtLabel) => { + const dt = wrapper.findByText(dtLabel).element; + const dd = dt.nextElementSibling; + if (dt.tagName === 'DT' && dd.tagName === 'DD') { + return createWrapper(dd, {}); + } + return ErrorWrapper(dtLabel); + }; + + const createComponent = ({ runner = {}, mountFn = shallowMountExtended } = {}) => { + wrapper = mountFn(RunnerDetails, { + propsData: { + runner: { + ...mockRunner, + ...runner, + }, + }, + stubs: { + GlIntersperse, + GlSprintf, + TimeAgo, + RunnerDetail, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe.each` + field | runner | expectedValue + ${'Description'} | ${{ description: 'My runner' }} | ${'My runner'} + ${'Description'} | ${{ description: null }} | ${'None'} + ${'Last contact'} | ${{ contactedAt: mockOneHourAgo }} | ${'1 hour ago'} + ${'Last contact'} | ${{ contactedAt: null }} | ${'Never contacted'} + ${'Version'} | ${{ version: '12.3' }} | ${'12.3'} + ${'Version'} | ${{ version: null }} | ${'None'} + ${'IP Address'} | ${{ ipAddress: '127.0.0.1' }} | ${'127.0.0.1'} + ${'IP Address'} | ${{ ipAddress: null }} | ${'None'} + ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: true }} | ${'Protected, Runs untagged jobs'} + ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: false }} | ${'Protected'} + ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: true }} | ${'Runs untagged jobs'} + ${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: false }} | ${'None'} + ${'Maximum job timeout'} | ${{ maximumTimeout: null }} | ${'None'} + ${'Maximum job timeout'} | ${{ maximumTimeout: 0 }} | ${'0 seconds'} + ${'Maximum job timeout'} | ${{ maximumTimeout: 59 }} | ${'59 seconds'} + ${'Maximum job timeout'} | ${{ maximumTimeout: 10 * 60 + 5 }} | ${'10 minutes 5 seconds'} + `('"$field" field', ({ field, runner, expectedValue }) => { + beforeEach(() => { + createComponent({ + runner, + }); + }); + + it(`displays expected value "${expectedValue}"`, () => { + expect(findDd(field).text()).toBe(expectedValue); + }); + }); + + describe('"Tags" field', () => { + it('displays expected value "tag-1 tag-2"', () => { + createComponent({ + runner: { tagList: ['tag-1', 'tag-2'] }, + mountFn: mountExtended, + }); + + expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('tag-1 tag-2'); + }); + + it('displays "None" when runner has no tags', () => { + createComponent({ + runner: { tagList: [] }, + mountFn: mountExtended, + }); + + expect(findDd('Tags').text().replace(/\s+/g, ' ')).toBe('None'); + }); + }); +}); diff --git a/spec/frontend/runner/components/runner_edit_button_spec.js b/spec/frontend/runner/components/runner_edit_button_spec.js new file mode 100644 index 00000000000..428c1ef07e9 --- /dev/null +++ b/spec/frontend/runner/components/runner_edit_button_spec.js @@ -0,0 +1,41 @@ +import { shallowMount, mount } from '@vue/test-utils'; +import RunnerEditButton from '~/runner/components/runner_edit_button.vue'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; + +describe('RunnerEditButton', () => { + let wrapper; + + const getTooltipValue = () => getBinding(wrapper.element, 'gl-tooltip').value; + + const createComponent = ({ attrs = {}, mountFn = shallowMount } = {}) => { + wrapper = mountFn(RunnerEditButton, { + attrs, + directives: { + GlTooltip: createMockDirective(), + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('Displays Edit text', () => { + expect(wrapper.attributes('aria-label')).toBe('Edit'); + }); + + it('Displays Edit tooltip', () => { + expect(getTooltipValue()).toBe('Edit'); + }); + + it('Renders a link and adds an href attribute', () => { + createComponent({ attrs: { href: '/edit' }, mountFn: mount }); + + expect(wrapper.element.tagName).toBe('A'); + expect(wrapper.attributes('href')).toBe('/edit'); + }); +}); diff --git a/spec/frontend/runner/components/runner_header_spec.js b/spec/frontend/runner/components/runner_header_spec.js index 50699df3a44..8799c218b06 100644 --- a/spec/frontend/runner/components/runner_header_spec.js +++ b/spec/frontend/runner/components/runner_header_spec.js @@ -1,5 +1,5 @@ import { GlSprintf } from '@gitlab/ui'; -import { mount, shallowMount } from '@vue/test-utils'; +import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { GROUP_TYPE, STATUS_ONLINE } from '~/runner/constants'; import { TYPE_CI_RUNNER } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; @@ -18,9 +18,10 @@ describe('RunnerHeader', () => { const findRunnerTypeBadge = () => wrapper.findComponent(RunnerTypeBadge); const findRunnerStatusBadge = () => wrapper.findComponent(RunnerStatusBadge); + const findRunnerLockedIcon = () => wrapper.findByTestId('lock-icon'); const findTimeAgo = () => wrapper.findComponent(TimeAgo); - const createComponent = ({ runner = {}, mountFn = shallowMount } = {}) => { + const createComponent = ({ runner = {}, options = {}, mountFn = shallowMountExtended } = {}) => { wrapper = mountFn(RunnerHeader, { propsData: { runner: { @@ -32,6 +33,7 @@ describe('RunnerHeader', () => { GlSprintf, TimeAgo, }, + ...options, }); }; @@ -41,24 +43,24 @@ describe('RunnerHeader', () => { it('displays the runner status', () => { createComponent({ - mountFn: mount, + mountFn: mountExtended, runner: { status: STATUS_ONLINE, }, }); - expect(findRunnerStatusBadge().text()).toContain(`online`); + expect(findRunnerStatusBadge().text()).toContain('online'); }); it('displays the runner type', () => { createComponent({ - mountFn: mount, + mountFn: mountExtended, runner: { runnerType: GROUP_TYPE, }, }); - expect(findRunnerTypeBadge().text()).toContain(`group`); + expect(findRunnerTypeBadge().text()).toContain('group'); }); it('displays the runner id', () => { @@ -68,7 +70,18 @@ describe('RunnerHeader', () => { }, }); - expect(wrapper.text()).toContain(`Runner #99`); + expect(wrapper.text()).toContain('Runner #99'); + }); + + it('displays the runner locked icon', () => { + createComponent({ + runner: { + locked: true, + }, + mountFn: mountExtended, + }); + + expect(findRunnerLockedIcon().exists()).toBe(true); }); it('displays the runner creation time', () => { @@ -78,7 +91,7 @@ describe('RunnerHeader', () => { expect(findTimeAgo().props('time')).toBe(mockRunner.createdAt); }); - it('does not display runner creation time if createdAt missing', () => { + it('does not display runner creation time if "createdAt" is missing', () => { createComponent({ runner: { id: convertToGraphQLId(TYPE_CI_RUNNER, 99), @@ -86,8 +99,21 @@ describe('RunnerHeader', () => { }, }); - expect(wrapper.text()).toContain(`Runner #99`); + expect(wrapper.text()).toContain('Runner #99'); expect(wrapper.text()).not.toMatch(/created .+/); expect(findTimeAgo().exists()).toBe(false); }); + + it('displays actions in a slot', () => { + createComponent({ + options: { + slots: { + actions: '
My Actions
', + }, + mountFn: mountExtended, + }, + }); + + expect(wrapper.findByTestId('actions-content').text()).toBe('My Actions'); + }); }); diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js index e6c08288cdf..13f53c36785 100644 --- a/spec/frontend/runner/components/runner_list_spec.js +++ b/spec/frontend/runner/components/runner_list_spec.js @@ -6,6 +6,7 @@ import { } from 'helpers/vue_test_utils_helper'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import RunnerList from '~/runner/components/runner_list.vue'; +import RunnerEditButton from '~/runner/components/runner_edit_button.vue'; import { runnersData } from '../mock_data'; const mockRunners = runnersData.data.runners.nodes; @@ -90,7 +91,7 @@ describe('RunnerList', () => { // Actions const actions = findCell({ fieldKey: 'actions' }); - expect(actions.findByTestId('edit-runner').exists()).toBe(true); + expect(actions.findComponent(RunnerEditButton).exists()).toBe(true); expect(actions.findByTestId('toggle-active-runner').exists()).toBe(true); }); diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js index 27450ea7ee4..02dc4a797d1 100644 --- a/spec/frontend/runner/components/runner_update_form_spec.js +++ b/spec/frontend/runner/components/runner_update_form_spec.js @@ -121,8 +121,18 @@ describe('RunnerUpdateForm', () => { it('Updates runner with no changes', async () => { await submitFormAndWait(); - // Some fields are not submitted - const { ipAddress, runnerType, createdAt, status, ...submitted } = mockRunner; + // Some read-only fields are not submitted + const { + ipAddress, + runnerType, + createdAt, + status, + editAdminUrl, + contactedAt, + userPermissions, + version, + ...submitted + } = mockRunner; expectToHaveSubmittedRunnerContaining(submitted); }); diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js index 23f8d6afcb5..9a95a838291 100644 --- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js +++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js @@ -22,9 +22,9 @@ describe('RunnerInstructions component', () => { wrapper.destroy(); }); - it('should show the "Show Runner installation instructions" button', () => { + it('should show the "Show runner installation instructions" button', () => { expect(findModalButton().exists()).toBe(true); - expect(findModalButton().text()).toBe('Show Runner installation instructions'); + expect(findModalButton().text()).toBe('Show runner installation instructions'); }); it('should not render the modal once mounted', () => { diff --git a/spec/graphql/types/ci/runner_type_spec.rb b/spec/graphql/types/ci/runner_type_spec.rb index 43d8b585d6b..1d8d3dd88bb 100644 --- a/spec/graphql/types/ci/runner_type_spec.rb +++ b/spec/graphql/types/ci/runner_type_spec.rb @@ -12,6 +12,7 @@ RSpec.describe GitlabSchema.types['CiRunner'] do id description created_at contacted_at maximum_timeout access_level active status version short_sha revision locked run_untagged ip_address runner_type tag_list project_count job_count admin_url edit_admin_url user_permissions executor_name + groups ] expect(described_class).to include_graphql_fields(*expected_fields) diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index 8c919b48849..61f11952ddc 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -223,6 +223,29 @@ RSpec.describe 'Query.runner(id)' do end end + describe 'for group runner request' do + let(:query) do + %( + query { + runner(id: "gid://gitlab/Ci::Runner/#{active_group_runner.id}") { + groups { + nodes { + id + } + } + } + } + ) + end + + it 'retrieves groups field with expected value' do + post_graphql(query, current_user: user) + + runner_data = graphql_data_at(:runner, :groups) + expect(runner_data).to eq 'nodes' => [{ 'id' => group.to_global_id.to_s }] + end + end + describe 'for runner with status' do let_it_be(:stale_runner) { create(:ci_runner, description: 'Stale runner 1', created_at: 3.months.ago) } let_it_be(:never_contacted_instance_runner) { create(:ci_runner, description: 'Missing runner 1', created_at: 1.month.ago, contacted_at: nil) } @@ -326,7 +349,13 @@ RSpec.describe 'Query.runner(id)' do describe 'by regular user' do let(:user) { create(:user) } - it_behaves_like 'retrieval by unauthorized user', :active_instance_runner + context 'on instance runner' do + it_behaves_like 'retrieval by unauthorized user', :active_instance_runner + end + + context 'on group runner' do + it_behaves_like 'retrieval by unauthorized user', :active_group_runner + end end describe 'by non-admin user' do