Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
7636511718
commit
881c946990
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import { initAdminRunnerShow } from '~/runner/admin_runner_show';
|
||||
|
||||
initAdminRunnerShow();
|
|
@ -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>
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -11,4 +11,11 @@ fragment RunnerDetailsShared on CiRunner {
|
|||
tagList
|
||||
createdAt
|
||||
status(legacyMode: null)
|
||||
contactedAt
|
||||
version
|
||||
editAdminUrl
|
||||
userPermissions {
|
||||
updateRunner
|
||||
deleteRunner
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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} }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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). |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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})")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue