Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-01-21 00:12:56 +00:00
parent 7636511718
commit 881c946990
38 changed files with 810 additions and 72 deletions

View File

@ -1,13 +1,15 @@
<!-- Actionable insights must recommend an action that needs to take place. An actionable insight both defines the insight and clearly calls out action or next step required to improve based on the result of the research observation or data. Actionable insights are tracked over time and will include follow-up. Please follow the tasks outlined in this issue for best results. Learn more in the handbook here: https://about.gitlab.com/handbook/engineering/ux/ux-research-training/research-insights/#actionable-insights -->
<!-- Actionable insights must recommend an action that needs to take place. An actionable insight both defines the insight and clearly calls out action or next step required to improve based on the result of the research observation or data. Actionable insights are tracked over time and will include follow-up. Please follow the tasks outlined in this issue for best results. Learn more in the handbook here: https://about.gitlab.com/handbook/engineering/ux/ux-research-training/research-insights/#actionable-insights
This issue template is for an actionable insight that requires further exploration.-->
### Insight
<!-- Describe the insight itself: often the problem, finding, or observation. -->
<!-- Describe the insight itself: often the problem, finding, or observation.-->
### Supporting evidence
<!-- Describe why the problem is happening, or more details behind the finding or observation. Try to include quotes or specific data collected. Feel free to link the Actionable insight from Dovetail here if applicable instead of retyping details. -->
### Action
<!--Describe the next step or action that needs to take place as a result of the research. The action should be clearly defined, achievable, and directly tied back to the insight. Make sure to use directive terminology, such as: conduct, explore, redesign, etc. -->
<!--Since this is an actionable insight that requires further exploration, ensure the action is algned to that. Describe the next step or action that needs to take place as a result of the research. The action should be clearly defined, achievable, and directly tied back to the insight. Make sure to use directive terminology, such as: conduct, explore, redesign, etc. -->
### Resources
<!--Add resources as links below or as related issues. -->
@ -26,5 +28,5 @@
/confidential
/label ~"Actionable Insight"
/label ~"Actionable Insight::Exploration needed"

View File

@ -0,0 +1,32 @@
<!-- Actionable insights must recommend an action that needs to take place. An actionable insight both defines the insight and clearly calls out action or next step required to improve based on the result of the research observation or data. Actionable insights are tracked over time and will include follow-up. Please follow the tasks outlined in this issue for best results. Learn more in the handbook here: https://about.gitlab.com/handbook/engineering/ux/ux-research-training/research-insights/#actionable-insights
This issue template is for an actionable insight that requires a change in the product.-->
### Insight
<!-- Describe the insight itself: often the problem, finding, or observation.-->
### Supporting evidence
<!-- Describe why the problem is happening, or more details behind the finding or observation. Try to include quotes or specific data collected. Feel free to link the Actionable insight from Dovetail here if applicable instead of retyping details. -->
### Action
<!--Since this is an actionable insight that requires a change in the product, ensure the action is algned to that. Describe the next step or action that needs to take place as a result of the research. The action should be clearly defined, achievable, and directly tied back to the insight. Make sure to use directive terminology, such as: change, update, add/remove, etc. -->
### Resources
<!--Add resources as links below or as related issues. -->
- :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
<!--Fill out these tasks in order to consider an Actionable Insight complete. Actionable Insights are created as confidential by default, but can be made non-confidential if the insight does not include information about competitors from a Competitor Evaluation or any other confidential information. -->
- [ ] 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"

View File

@ -0,0 +1,3 @@
import { initAdminRunnerShow } from '~/runner/admin_runner_show';
initAdminRunnerShow();

View File

@ -0,0 +1,74 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import RunnerEditButton from '../components/runner_edit_button.vue';
import RunnerHeader from '../components/runner_header.vue';
import RunnerDetails from '../components/runner_details.vue';
import { I18N_FETCH_ERROR } from '../constants';
import getRunnerQuery from '../graphql/get_runner.query.graphql';
import { captureException } from '../sentry_utils';
export default {
name: 'AdminRunnerShowApp',
components: {
RunnerEditButton,
RunnerHeader,
RunnerDetails,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
runnerId: {
type: String,
required: true,
},
},
data() {
return {
runner: null,
};
},
apollo: {
runner: {
query: getRunnerQuery,
variables() {
return {
id: convertToGraphQLId(TYPE_CI_RUNNER, this.runnerId),
};
},
error(error) {
createAlert({ message: I18N_FETCH_ERROR });
this.reportToSentry(error);
},
},
},
computed: {
canUpdate() {
return this.runner.userPermissions?.updateRunner;
},
},
errorCaptured(error) {
this.reportToSentry(error);
},
methods: {
reportToSentry(error) {
captureException({ error, component: this.$options.name });
},
},
};
</script>
<template>
<div>
<runner-header v-if="runner" :runner="runner">
<template #actions>
<runner-edit-button v-if="canUpdate && runner.editAdminUrl" :href="runner.editAdminUrl" />
</template>
</runner-header>
<runner-details :runner="runner" />
</div>
</template>

View File

@ -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,
},
});
},
});
};

View File

@ -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,
};
</script>
@ -161,14 +161,7 @@ export default {
See https://gitlab.com/gitlab-org/gitlab/-/issues/334802
-->
<gl-button
v-if="canUpdate && runner.editAdminUrl"
v-gl-tooltip.hover.viewport="$options.I18N_EDIT"
:href="runner.editAdminUrl"
:aria-label="$options.I18N_EDIT"
icon="pencil"
data-testid="edit-runner"
/>
<runner-edit-button v-if="canUpdate && runner.editAdminUrl" :href="runner.editAdminUrl" />
<gl-button
v-if="canUpdate"
v-gl-tooltip.hover.viewport="toggleActiveTitle"

View File

@ -0,0 +1,50 @@
<script>
import { __ } from '~/locale';
/**
* Usage:
*
* With a `value` prop:
*
* <runner-detail label="Field Name" :value="value" />
*
* Or a `value` slot:
*
* <runner-detail label="Field Name">
* <template #value>
* <strong>{{ value }}</strong>
* </template>
* </runner-detail>
*
*/
export default {
props: {
label: {
type: String,
required: true,
},
value: {
type: String,
default: null,
required: false,
},
emptyValue: {
type: String,
default: __('None'),
required: false,
},
},
};
</script>
<template>
<div class="gl-display-flex gl-pb-4">
<dt class="gl-mr-2">{{ label }}</dt>
<dd class="gl-mb-0">
<template v-if="value || $slots.value">
<slot name="value">{{ value }}</slot>
</template>
<span v-else class="gl-text-gray-500">{{ emptyValue }}</span>
</dd>
</div>
</template>

View File

@ -0,0 +1,92 @@
<script>
import { GlTabs, GlTab, GlIntersperse } from '@gitlab/ui';
import { s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { ACCESS_LEVEL_REF_PROTECTED } from '../constants';
import RunnerDetail from './runner_detail.vue';
import RunnerTags from './runner_tags.vue';
export default {
components: {
GlTabs,
GlTab,
GlIntersperse,
RunnerDetail,
RunnerTags,
TimeAgo,
},
props: {
runner: {
type: Object,
required: false,
default: null,
},
},
computed: {
maximumTimeout() {
const { maximumTimeout } = this.runner;
if (typeof maximumTimeout !== 'number') {
return null;
}
return timeIntervalInWords(maximumTimeout);
},
configTextProtected() {
if (this.runner.accessLevel === ACCESS_LEVEL_REF_PROTECTED) {
return s__('Runners|Protected');
}
return null;
},
configTextUntagged() {
if (this.runner.runUntagged) {
return s__('Runners|Runs untagged jobs');
}
return null;
},
},
ACCESS_LEVEL_REF_PROTECTED,
};
</script>
<template>
<gl-tabs>
<gl-tab>
<template #title>{{ s__('Runners|Details') }}</template>
<div v-if="runner" class="gl-py-4">
<dl>
<runner-detail :label="s__('Runners|Description')" :value="runner.description" />
<runner-detail
:label="s__('Runners|Last contact')"
:empty-value="s__('Runners|Never contacted')"
>
<template #value>
<time-ago v-if="runner.contactedAt" :time="runner.contactedAt" />
</template>
</runner-detail>
<runner-detail :label="s__('Runners|Version')" :value="runner.version" />
<runner-detail :label="s__('Runners|IP Address')" :value="runner.ipAddress" />
<runner-detail :label="s__('Runners|Configuration')">
<template #value>
<gl-intersperse v-if="configTextProtected || configTextUntagged">
<span v-if="configTextProtected">{{ configTextProtected }}</span>
<span v-if="configTextUntagged">{{ configTextUntagged }}</span>
</gl-intersperse>
</template>
</runner-detail>
<runner-detail :label="s__('Runners|Maximum job timeout')" :value="maximumTimeout" />
<runner-detail :label="s__('Runners|Tags')">
<template #value>
<runner-tags
v-if="runner.tagList && runner.tagList.length"
class="gl-vertical-align-middle"
:tag-list="runner.tagList"
size="sm"
/>
</template>
</runner-detail>
</dl>
</div>
</gl-tab>
</gl-tabs>
</template>

View File

@ -0,0 +1,26 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
const I18N_EDIT = __('Edit');
export default {
components: {
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
I18N_EDIT,
};
</script>
<template>
<gl-button
v-gl-tooltip="$options.I18N_EDIT"
v-bind="$attrs"
:aria-label="$options.I18N_EDIT"
icon="pencil"
v-on="$listeners"
/>
</template>

View File

@ -1,19 +1,23 @@
<script>
import { GlSprintf } from '@gitlab/ui';
import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { sprintf } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { I18N_DETAILS_TITLE } from '../constants';
import { I18N_DETAILS_TITLE, I18N_LOCKED_RUNNER_DESCRIPTION } from '../constants';
import RunnerTypeBadge from './runner_type_badge.vue';
import RunnerStatusBadge from './runner_status_badge.vue';
export default {
components: {
GlIcon,
GlSprintf,
TimeAgo,
RunnerTypeBadge,
RunnerStatusBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
runner: {
type: Object,
@ -29,24 +33,36 @@ export default {
return sprintf(I18N_DETAILS_TITLE, { runner_id: id });
},
},
I18N_LOCKED_RUNNER_DESCRIPTION,
};
</script>
<template>
<div class="gl-py-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
<runner-status-badge :runner="runner" />
<runner-type-badge v-if="runner" :type="runner.runnerType" />
<template v-if="runner.createdAt">
<gl-sprintf :message="__('%{runner} created %{timeago}')">
<template #runner>
<strong>{{ heading }}</strong>
</template>
<template #timeago>
<time-ago :time="runner.createdAt" />
</template>
</gl-sprintf>
</template>
<template v-else>
<strong>{{ heading }}</strong>
</template>
<div
class="gl-display-flex gl-align-items-center gl-py-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
>
<div>
<runner-status-badge :runner="runner" />
<runner-type-badge v-if="runner" :type="runner.runnerType" />
<template v-if="runner.createdAt">
<gl-sprintf :message="__('%{runner} created %{timeago}')">
<template #runner>
<strong>{{ heading }}</strong>
<gl-icon
v-if="runner.locked"
v-gl-tooltip="$options.I18N_LOCKED_RUNNER_DESCRIPTION"
name="lock"
:aria-label="$options.I18N_LOCKED_RUNNER_DESCRIPTION"
/>
</template>
<template #timeago>
<time-ago :time="runner.createdAt" />
</template>
</gl-sprintf>
</template>
<template v-else>
<strong>{{ heading }}</strong>
</template>
</div>
<div class="gl-ml-auto"><slot name="actions"></slot></div>
</div>
</template>

View File

@ -20,7 +20,7 @@ export default {
};
</script>
<template>
<div>
<span>
<runner-tag
v-for="tag in tagList"
:key="tag"
@ -28,5 +28,5 @@ export default {
:tag="tag"
:size="size"
/>
</div>
</span>
</template>

View File

@ -11,4 +11,11 @@ fragment RunnerDetailsShared on CiRunner {
tagList
createdAt
status(legacyMode: null)
contactedAt
version
editAdminUrl
userPermissions {
updateRunner
deleteRunner
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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} }

View File

@ -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

View File

@ -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?

View File

@ -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.

View File

@ -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).

View File

@ -9022,6 +9022,7 @@ Represents the total number of issues and their weights for a particular day.
| <a id="cirunnerdescription"></a>`description` | [`String`](#string) | Description of the runner. |
| <a id="cirunnereditadminurl"></a>`editAdminUrl` | [`String`](#string) | Admin form URL of the runner. Only available for administrators. |
| <a id="cirunnerexecutorname"></a>`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. |
| <a id="cirunnergroups"></a>`groups` | [`GroupConnection`](#groupconnection) | Groups the runner is associated with. For group runners only. (see [Connections](#connections)) |
| <a id="cirunnerid"></a>`id` | [`CiRunnerID!`](#cirunnerid) | ID of the runner. |
| <a id="cirunneripaddress"></a>`ipAddress` | [`String`](#string) | IP address of the runner. |
| <a id="cirunnerjobcount"></a>`jobCount` | [`Int`](#int) | Number of jobs processed by the runner (limited to 1000, plus one to indicate that more items exist). |

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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."

View File

@ -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})")

View File

@ -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

View File

@ -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 () => {

View File

@ -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();
});
});
});

View File

@ -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');

View File

@ -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 (<dd>) that corresponds to this term (<dt>)
* @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');
});
});
});

View File

@ -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');
});
});

View File

@ -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: '<div data-testid="actions-content">My Actions</div>',
},
mountFn: mountExtended,
},
});
expect(wrapper.findByTestId('actions-content').text()).toBe('My Actions');
});
});

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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', () => {

View File

@ -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)

View File

@ -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