Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-30 21:09:29 +00:00
parent f06ebebade
commit 8924515456
71 changed files with 769 additions and 192 deletions

View File

@ -293,14 +293,14 @@ coverage-frontend:
- *yarn-install
- run_timed_command "retry yarn run webpack-prod"
qa-frontend-node:12:
extends: .qa-frontend-node
image: ${GITLAB_DEPENDENCY_PROXY}node:12
qa-frontend-node:14:
extends: .qa-frontend-node
image: ${GITLAB_DEPENDENCY_PROXY}node:14
qa-frontend-node:16:
extends: .qa-frontend-node
image: ${GITLAB_DEPENDENCY_PROXY}node:16
qa-frontend-node:latest:
extends:
- .qa-frontend-node

View File

@ -1252,7 +1252,7 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sqlite3 (1.3.13)
sqlite3 (1.4.2)
ssh_data (1.2.0)
ssrf_filter (1.0.7)
stackprof (0.2.15)

View File

@ -12,6 +12,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-security-newsletter-callout',
'.js-approaching-seats-count-threshold',
'.js-storage-enforcement-banner',
'.js-user-over-limit-free-plan-alert',
];
const initCallouts = () => {

View File

@ -4,9 +4,11 @@ import { createAlert } from '~/flash';
import { updateHistory } from '~/lib/utils/url_utility';
import { formatNumber } from '~/locale';
import { fetchPolicies } from '~/lib/graphql';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerBulkDelete from '../components/runner_bulk_delete.vue';
import RunnerList from '../components/runner_list.vue';
import RunnerName from '../components/runner_name.vue';
import RunnerStats from '../components/stat/runner_stats.vue';
@ -53,6 +55,7 @@ export default {
GlLink,
RegistrationDropdown,
RunnerFilteredSearchBar,
RunnerBulkDelete,
RunnerList,
RunnerName,
RunnerStats,
@ -60,6 +63,8 @@ export default {
RunnerTypeTabs,
RunnerActionsCell,
},
mixins: [glFeatureFlagMixin()],
inject: ['localMutations'],
props: {
registrationToken: {
type: String,
@ -180,6 +185,11 @@ export default {
},
];
},
isBulkDeleteEnabled() {
// Feature flag: admin_runners_bulk_delete
// Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/353981
return this.glFeatures.adminRunnersBulkDelete;
},
},
watch: {
search: {
@ -238,6 +248,12 @@ export default {
reportToSentry(error) {
captureException({ error, component: this.$options.name });
},
onChecked({ runner, isChecked }) {
this.localMutations.setRunnerChecked({
runner,
isChecked,
});
},
},
filteredSearchNamespace: ADMIN_FILTERED_SEARCH_NAMESPACE,
INSTANCE_TYPE,
@ -286,7 +302,13 @@ export default {
{{ __('No runners found') }}
</div>
<template v-else>
<runner-list :runners="runners.items" :loading="runnersLoading">
<runner-bulk-delete v-if="isBulkDeleteEnabled" />
<runner-list
:runners="runners.items"
:loading="runnersLoading"
:checkable="isBulkDeleteEnabled"
@checked="onChecked"
>
<template #runner-name="{ runner }">
<gl-link :href="runner.adminUrl">
<runner-name :runner="runner" />

View File

@ -1,9 +1,10 @@
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { visitUrl } from '~/lib/utils/url_utility';
import { updateOutdatedUrl } from '~/runner/runner_search_utils';
import createDefaultClient from '~/lib/graphql';
import { createLocalState } from '../graphql/list/local_state';
import AdminRunnersApp from './admin_runners_app.vue';
Vue.use(GlToast);
@ -27,8 +28,10 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
const { runnerInstallHelpPage, registrationToken } = el.dataset;
const { cacheConfig, typeDefs, localMutations } = createLocalState();
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
defaultClient: createDefaultClient({}, { cacheConfig, typeDefs }),
});
return new Vue({
@ -36,6 +39,7 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
apolloProvider,
provide: {
runnerInstallHelpPage,
localMutations,
},
render(h) {
return h(AdminRunnersApp, {

View File

@ -0,0 +1,111 @@
<script>
import { GlButton, GlModalDirective, GlSprintf } from '@gitlab/ui';
import { n__, sprintf } from '~/locale';
import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql';
export default {
components: {
GlButton,
GlSprintf,
},
directives: {
GlModal: GlModalDirective,
},
inject: ['localMutations'],
data() {
return {
checkedRunnerIds: [],
};
},
apollo: {
checkedRunnerIds: {
query: checkedRunnerIdsQuery,
},
},
computed: {
checkedCount() {
return this.checkedRunnerIds.length || 0;
},
bannerMessage() {
return sprintf(
n__(
'Runners|%{strongStart}%{count}%{strongEnd} runner selected',
'Runners|%{strongStart}%{count}%{strongEnd} runners selected',
this.checkedCount,
),
{
count: this.checkedCount,
},
);
},
modalTitle() {
return n__('Runners|Delete %d runner', 'Runners|Delete %d runners', this.checkedCount);
},
modalHtmlMessage() {
return sprintf(
n__(
'Runners|%{strongStart}%{count}%{strongEnd} runner will be permanently deleted and no longer available for projects or groups in the instance. Are you sure you want to continue?',
'Runners|%{strongStart}%{count}%{strongEnd} runners will be permanently deleted and no longer available for projects or groups in the instance. Are you sure you want to continue?',
this.checkedCount,
),
{
strongStart: '<strong>',
strongEnd: '</strong>',
count: this.checkedCount,
},
false,
);
},
primaryBtnText() {
return n__(
'Runners|Permanently delete %d runner',
'Runners|Permanently delete %d runners',
this.checkedCount,
);
},
},
methods: {
onClearChecked() {
this.localMutations.clearChecked();
},
onClickDelete: ignoreWhilePending(async function onClickDelete() {
const confirmed = await confirmAction(null, {
title: this.modalTitle,
modalHtmlMessage: this.modalHtmlMessage,
primaryBtnVariant: 'danger',
primaryBtnText: this.primaryBtnText,
});
if (confirmed) {
// TODO Call $apollo.mutate with list of runner
// ids in `this.checkedRunnerIds`.
// See https://gitlab.com/gitlab-org/gitlab/-/issues/339525/
}
}),
},
};
</script>
<template>
<div v-if="checkedCount" class="gl-my-4 gl-p-4 gl-border-1 gl-border-solid gl-border-gray-100">
<div class="gl-display-flex gl-align-items-center">
<div>
<gl-sprintf :message="bannerMessage">
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
</gl-sprintf>
</div>
<div class="gl-ml-auto">
<gl-button data-testid="clear-btn" variant="default" @click="onClearChecked">{{
s__('Runners|Clear selection')
}}</gl-button>
<gl-button data-testid="delete-btn" variant="danger" @click="onClickDelete">{{
s__('Runners|Delete selected')
}}</gl-button>
</div>
</div>
</div>
</template>

View File

@ -4,11 +4,22 @@ import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/toolt
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql';
import { formatJobCount, tableField } from '../utils';
import RunnerSummaryCell from './cells/runner_summary_cell.vue';
import RunnerStatusCell from './cells/runner_status_cell.vue';
import RunnerTags from './runner_tags.vue';
const defaultFields = [
tableField({ key: 'status', label: s__('Runners|Status') }),
tableField({ key: 'summary', label: s__('Runners|Runner'), thClasses: ['gl-lg-w-25p'] }),
tableField({ key: 'version', label: __('Version') }),
tableField({ key: 'jobCount', label: __('Jobs') }),
tableField({ key: 'tagList', label: __('Tags'), thClasses: ['gl-lg-w-25p'] }),
tableField({ key: 'contactedAt', label: __('Last contact') }),
tableField({ key: 'actions', label: '' }),
];
export default {
components: {
GlTableLite,
@ -22,7 +33,20 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
apollo: {
checkedRunnerIds: {
query: checkedRunnerIdsQuery,
skip() {
return !this.checkable;
},
},
},
props: {
checkable: {
type: Boolean,
required: false,
default: false,
},
loading: {
type: Boolean,
required: false,
@ -33,6 +57,10 @@ export default {
required: true,
},
},
emits: ['checked'],
data() {
return { checkedRunnerIds: [] };
},
computed: {
tableClass() {
// <gl-table-lite> does not provide a busy state, add
@ -42,6 +70,18 @@ export default {
'gl-opacity-6': this.loading,
};
},
fields() {
if (this.checkable) {
const checkboxField = tableField({
key: 'checkbox',
label: s__('Runners|Checkbox'),
thClasses: ['gl-w-9'],
tdClass: ['gl-text-center'],
});
return [checkboxField, ...defaultFields];
}
return defaultFields;
},
},
methods: {
formatJobCount(jobCount) {
@ -55,16 +95,16 @@ export default {
}
return {};
},
onCheckboxChange(runner, isChecked) {
this.$emit('checked', {
runner,
isChecked,
});
},
isChecked(runner) {
return this.checkedRunnerIds.includes(runner.id);
},
},
fields: [
tableField({ key: 'status', label: s__('Runners|Status') }),
tableField({ key: 'summary', label: s__('Runners|Runner'), thClasses: ['gl-lg-w-25p'] }),
tableField({ key: 'version', label: __('Version') }),
tableField({ key: 'jobCount', label: __('Jobs') }),
tableField({ key: 'tagList', label: __('Tags'), thClasses: ['gl-lg-w-25p'] }),
tableField({ key: 'contactedAt', label: __('Last contact') }),
tableField({ key: 'actions', label: '' }),
],
};
</script>
<template>
@ -73,13 +113,29 @@ export default {
:aria-busy="loading"
:class="tableClass"
:items="runners"
:fields="$options.fields"
:fields="fields"
:tbody-tr-attr="runnerTrAttr"
data-testid="runner-list"
stacked="md"
primary-key="id"
fixed
>
<template #head(checkbox)>
<!--
Checkbox to select all to be added here
See https://gitlab.com/gitlab-org/gitlab/-/issues/339525/
-->
<span></span>
</template>
<template #cell(checkbox)="{ item }">
<input
type="checkbox"
:checked="isChecked(item)"
@change="onCheckboxChange(item, $event.target.checked)"
/>
</template>
<template #cell(status)="{ item }">
<runner-status-cell :runner="item" />
</template>

View File

@ -0,0 +1,3 @@
query getCheckedRunnerIds {
checkedRunnerIds @client
}

View File

@ -0,0 +1,63 @@
import { makeVar } from '@apollo/client/core';
import typeDefs from './typedefs.graphql';
/**
* Local state for checkable runner items.
*
* Usage:
*
* ```
* import { createLocalState } from '~/runner/graphql/list/local_state';
*
* // initialize local state
* const { cacheConfig, typeDefs, localMutations } = createLocalState();
*
* // configure the client
* apolloClient = createApolloClient({}, { cacheConfig, typeDefs });
*
* // modify local state
* localMutations.setRunnerChecked( ... )
* ```
*
* Note: Currently only in use behind a feature flag:
* admin_runners_bulk_delete for the admin list, rollout issue:
* https://gitlab.com/gitlab-org/gitlab/-/issues/353981
*
* @returns {Object} An object to configure an Apollo client:
* contains cacheConfig, typeDefs, localMutations.
*/
export const createLocalState = () => {
const checkedRunnerIdsVar = makeVar({});
const cacheConfig = {
typePolicies: {
Query: {
fields: {
checkedRunnerIds() {
return Object.entries(checkedRunnerIdsVar())
.filter(([, isChecked]) => isChecked)
.map(([key]) => key);
},
},
},
},
};
const localMutations = {
setRunnerChecked({ runner, isChecked }) {
checkedRunnerIdsVar({
...checkedRunnerIdsVar(),
[runner.id]: isChecked,
});
},
clearChecked() {
checkedRunnerIdsVar({});
},
};
return {
cacheConfig,
typeDefs,
localMutations,
};
};

View File

@ -0,0 +1,3 @@
extend type Query {
checkedRunnerIds: [ID!]!
}

View File

@ -24,7 +24,7 @@ export const formatJobCount = (jobCount) => {
* @param {Object} options
* @returns Field object to add to GlTable fields
*/
export const tableField = ({ key, label = '', thClasses = [] }) => {
export const tableField = ({ key, label = '', thClasses = [], ...options }) => {
return {
key,
label,
@ -32,6 +32,7 @@ export const tableField = ({ key, label = '', thClasses = [] }) => {
tdAttr: {
'data-testid': `td-${key}`,
},
...options,
};
};

View File

@ -4,7 +4,6 @@ import { __, s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
@ -52,7 +51,7 @@ export default {
TrainingProviderList,
},
mixins: [glFeatureFlagsMixin()],
inject: ['projectFullPath'],
inject: ['projectFullPath', 'vulnerabilityTrainingDocsPath'],
props: {
augmentedSecurityFeatures: {
type: Array,
@ -127,9 +126,6 @@ export default {
},
},
autoDevopsEnabledAlertStorageKey: AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
securityTraininDocLink: helpPagePath('user/application_security/vulnerabilities/index', {
anchor: 'enable-security-training-for-vulnerabilities',
}),
};
</script>
@ -268,7 +264,7 @@ export default {
{{ $options.i18n.securityTrainingDescription }}
</p>
<p>
<gl-link :href="$options.securityTraininDocLink">{{
<gl-link :href="vulnerabilityTrainingDocsPath">{{
$options.i18n.securityTrainingDoc
}}</gl-link>
</p>

View File

@ -25,6 +25,7 @@ export const initSecurityConfiguration = (el) => {
gitlabCiHistoryPath,
autoDevopsHelpPagePath,
autoDevopsPath,
vulnerabilityTrainingDocsPath,
} = el.dataset;
const { augmentedSecurityFeatures, augmentedComplianceFeatures } = augmentFeatures(
@ -41,6 +42,7 @@ export const initSecurityConfiguration = (el) => {
upgradePath,
autoDevopsHelpPagePath,
autoDevopsPath,
vulnerabilityTrainingDocsPath,
},
render(createElement) {
return createElement(SecurityConfigurationApp, {

View File

@ -25,7 +25,7 @@ export default {
</script>
<template>
<span>
<gl-button ref="popoverTrigger" variant="link" icon="question" :aria-label="__('Help')" />
<gl-button ref="popoverTrigger" variant="link" icon="question-o" :aria-label="__('Help')" />
<gl-popover :target="() => $refs.popoverTrigger.$el" v-bind="options">
<template v-if="options.title" #title>
<span v-safe-html="options.title"></span>

View File

@ -4,6 +4,9 @@ class Admin::RunnersController < Admin::ApplicationController
include RunnerSetupScripts
before_action :runner, except: [:index, :tag_list, :runner_setup_scripts]
before_action only: [:index] do
push_frontend_feature_flag(:admin_runners_bulk_delete, default_enabled: :yaml)
end
feature_category :runner

View File

@ -6,6 +6,10 @@ module Projects
def security_upgrade_path
"https://#{ApplicationHelper.promo_host}/pricing/"
end
def vulnerability_training_docs_path
help_page_path('user/application_security/vulnerabilities/index', anchor: 'enable-security-training-for-vulnerabilities')
end
end
end
end

View File

@ -31,3 +31,5 @@ module Users
end
end
end
Users::GroupCalloutsHelper.prepend_mod

View File

@ -14,7 +14,8 @@ module Users
storage_enforcement_banner_first_enforcement_threshold: 3,
storage_enforcement_banner_second_enforcement_threshold: 4,
storage_enforcement_banner_third_enforcement_threshold: 5,
storage_enforcement_banner_fourth_enforcement_threshold: 6
storage_enforcement_banner_fourth_enforcement_threshold: 6,
preview_user_over_limit_free_plan_alert: 7 # EE-only
}
validates :group, presence: true

View File

@ -4,7 +4,9 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
presents ::Issue, as: :issue
def issue_path
url_builder.build(issue, only_path: true)
return url_builder.build(issue, only_path: true) unless use_work_items_path?
project_work_items_path(issue.project, work_items_path: issue.id)
end
delegator_override :subscribed?
@ -15,6 +17,18 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
def project_emails_disabled?
issue.project.emails_disabled?
end
def web_url
return super unless use_work_items_path?
project_work_items_url(issue.project, work_items_path: issue.id)
end
private
def use_work_items_path?
issue.issue_type == 'task' && issue.project.work_items_feature_flag_enabled?
end
end
IssuePresenter.prepend_mod_with('IssuePresenter')

View File

@ -1,6 +1,8 @@
- add_page_specific_style 'page_bundles/members'
- page_title _('Group members')
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @group
.row.gl-mt-3
.col-lg-12
.gl-display-flex.gl-flex-wrap

View File

@ -7,6 +7,7 @@
= render_if_exists 'shared/thanks_for_purchase_banner', plan_title: plan_title, quantity: params[:purchased_quantity].to_i
= render_if_exists 'shared/qrtly_reconciliation_alert', group: @group
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @group
- if show_invite_banner?(@group)
= content_for :group_invite_members_banner do

View File

@ -20,6 +20,7 @@
= dispensable_render_if_exists "shared/namespace_user_cap_reached_alert"
= dispensable_render_if_exists "shared/new_user_signups_cap_reached_alert"
= yield :page_level_alert
= yield :user_over_limit_free_plan_alert
= yield :group_invite_members_banner
- unless @hide_breadcrumbs
= render "layouts/nav/breadcrumbs"

View File

@ -3,6 +3,7 @@
- escaped_default_branch_name = default_branch_name.shellescape
- @skip_current_level_breadcrumb = true
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @project
= render partial: 'flash_messages', locals: { project: @project }
= render "home_panel"

View File

@ -1,6 +1,8 @@
- page_title _('No repository')
- @skip_current_level_breadcrumb = true
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @project
%h2.gl-display-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
= sprite_icon('warning-solid', size: 24, css_class: 'gl-mr-2')

View File

@ -1,6 +1,8 @@
- add_page_specific_style 'page_bundles/members'
- page_title _("Members")
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @project
.row.gl-mt-3
.col-lg-12
- if can_invite_members_for_project?(@project)

View File

@ -3,5 +3,6 @@
- @content_class = "limit-container-width" unless fluid_layout
#js-security-configuration{ data: { **@configuration.to_html_data_attribute,
vulnerability_training_docs_path: vulnerability_training_docs_path,
upgrade_path: security_upgrade_path,
project_full_path: @project.full_path } }

View File

@ -6,6 +6,7 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity")
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @project
= render partial: 'flash_messages', locals: { project: @project }
= render "projects/last_push"

View File

@ -0,0 +1,8 @@
---
name: admin_runners_bulk_delete
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81894
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353981
milestone: '14.9'
type: development
group: group::runner
default_enabled: false

View File

@ -0,0 +1,16 @@
- name: "Request a new review" # the name of the feature being removed. Avoid the words `deprecation`, `deprecate`, `removal`, and `remove` in this field because these are implied.
announcement_milestone: "15.0" # The milestone when this feature was deprecated.
announcement_date: "2022-05-22" # The date of the milestone release when this feature was deprecated. This should almost always be the 22nd of a month (YYYY-MM-DD), unless you did an out of band blog post.
removal_milestone: "15.0" # The milestone when this feature is being removed.
removal_date: "2022-05-22" # This should almost always be the 22nd of a month (YYYY-MM-DD), the date of the milestone release when this feature will be removed.
breaking_change: false # Change to true if this removal is a breaking change.
reporter: phikai # GitLab username of the person reporting the removal
body: | # Do not modify this line, instead modify the lines below.
The ability to [request a new review](https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#request-a-new-review) has been removed in GitLab 15.0. This feature is replaced with [requesting attention](https://docs.gitlab.com/ee/user/project/merge_requests/#request-attention-to-a-merge-request) to a merge request.
# The following items are not published on the docs page, but may be used in the future.
stage: Create # (optional - may be required in the future) String value of the stage that the feature was created in. e.g., Growth
tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
issue_url: # (optional) This is a link to the deprecation issue in GitLab
documentation_url: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#request-a-new-review # (optional) This is a link to the current documentation page
image_url: # (optional) This is a link to a thumbnail image depicting the feature
video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg

View File

@ -14,3 +14,4 @@ swap:
needs? to: "Rewrite the sentence, or use 'must', instead of"
note that: "Be concise: rewrite the sentence to not use"
please: "Remove this word from the sentence: "
respectively: "Rewrite the sentence to be more precise, instead of using "

View File

@ -51,7 +51,7 @@ If the highest number stable branch is unclear, check the [GitLab blog](https://
| [Ruby](#2-ruby) | `2.7` | From GitLab 13.6, Ruby 2.7 is required. Ruby 3.0 is not supported yet (see [the relevant epic](https://gitlab.com/groups/gitlab-org/-/epics/5149) for the current status). You must use the standard MRI implementation of Ruby. We love [JRuby](https://www.jruby.org/) and [Rubinius](https://github.com/rubinius/rubinius#the-rubinius-language-platform), but GitLab needs several Gems that have native extensions. |
| [Go](#3-go) | `1.16` | |
| [Git](#git) | `2.33.x` | From GitLab 14.4, Git 2.33.x and later is required. It's highly recommended that you use the [Git version provided by Gitaly](#git). |
| [Node.js](#4-node) | `12.22.1` | GitLab uses [webpack](https://webpack.js.org/) to compile frontend assets. Node.js 14.x is recommended, as it's faster. You can check which version you're running with `node -v`. You need to update it to a newer version if needed. |
| [Node.js](#4-node) | `14.15.0` | GitLab uses [webpack](https://webpack.js.org/) to compile frontend assets. Node.js 16.x is recommended, as it's faster. You can check which version you're running with `node -v`. You need to update it to a newer version if needed. |
## GitLab directory structure
@ -263,7 +263,7 @@ GitLab requires the use of Node to compile JavaScript
assets, and Yarn to manage JavaScript dependencies. The current minimum
requirements for these are:
- `node` >= v12.22.1. (We recommend node 14.x as it is faster)
- `node` >= v14.15.0. (We recommend node 16.x as it is faster)
- `yarn` = v1.22.x (Yarn 2 is not supported yet)
In many distributions,
@ -271,8 +271,8 @@ the versions provided by the official package repositories are out of date, so
we need to install through the following commands:
```shell
# install node v14.x
curl --location "https://deb.nodesource.com/setup_14.x" | sudo bash -
# install node v16.x
curl --location "https://deb.nodesource.com/setup_16.x" | sudo bash -
sudo apt-get install -y nodejs
npm install --global yarn

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0.
for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
GitLab provides some dashboards out-of-the-box for any project with
[Prometheus available](../../../user/project/integrations/prometheus.md). You can

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0.
for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
GitLab provides a template to make it easier for you to create templates for
[custom dashboards](index.md). Templates provide helpful guidance and

View File

@ -11,7 +11,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0.
for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
By default, all projects include a [GitLab-defined Prometheus dashboard](default.md), which
includes a few key metrics, but you can also define your own custom dashboards.

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0.
for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
The below panel types are supported in monitoring dashboards.

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0.
for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
You can configure your [Monitoring dashboard](../index.md) to
display the time zone of your choice, and the links of your choice.

View File

@ -11,7 +11,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0.
for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
Templating variables can be used to make your metrics dashboard more versatile.

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0.
for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
## Query variables

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0.
for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
Dashboards have several components:

View File

@ -28,6 +28,12 @@ For removal reviewers (Technical Writers only):
https://about.gitlab.com/handbook/marketing/blog/release-posts/#update-the-removals-doc
-->
## 15.0
### Request a new review
The ability to [request a new review](https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#request-a-new-review) has been removed in GitLab 15.0. This feature is replaced with [requesting attention](https://docs.gitlab.com/ee/user/project/merge_requests/#request-attention-to-a-merge-request) to a merge request.
## 14.9
### Integrated error tracking disabled by default

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -422,7 +422,7 @@ The following table lists group permissions available for each role:
| View 2FA status of members | | | | | ✓ |
| View [Billing](../subscriptions/gitlab_com/index.md#view-your-gitlab-saas-subscription) | | | | | ✓ (4) |
| View [Usage Quotas](usage_quotas.md) Page | | | | ✓ | ✓ (4) |
| Manage runners | | | | | ✓ |
| Manage group runners | | | | | ✓ |
<!-- markdownlint-disable MD029 -->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0.
for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
GitLab supports automatically detecting and monitoring AWS resources, starting
with the [Elastic Load Balancer](https://aws.amazon.com/elasticloadbalancing/) (ELB).

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0.
for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
GitLab has support for automatically detecting and monitoring HAProxy. This is provided by leveraging the [HAProxy Exporter](https://github.com/prometheus/haproxy_exporter), which translates HAProxy statistics into a Prometheus readable form.

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0.
for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
GitLab offers automatic detection of select [Prometheus exporters](https://prometheus.io/docs/instrumenting/exporters/).

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0.
for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
GitLab has support for automatically detecting and monitoring Kubernetes metrics.

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0.
for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
GitLab has support for automatically detecting and monitoring NGINX. This is provided by leveraging the [NGINX VTS exporter](https://github.com/hnlq715/nginx-vts-exporter), which translates [VTS statistics](https://github.com/vozlt/nginx-module-vts) into a Prometheus readable form.

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0.
for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
GitLab has support for automatically detecting and monitoring the Kubernetes NGINX Ingress controller. This is provided by leveraging the built-in Prometheus metrics included with Kubernetes NGINX Ingress controller [version 0.16.0](https://github.com/kubernetes/ingress-nginx/blob/master/Changelog.md#0160) onward.

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0.
for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
NOTE:
[NGINX Ingress version 0.16](nginx_ingress.md) and above have built-in Prometheus metrics, which are different than the VTS based metrics.

View File

@ -112,7 +112,13 @@ This example shows reviewers and approval rules in a merge request sidebar:
### Request a new review
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/293933) in GitLab 13.9.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/293933) in GitLab 13.9.
> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/357271) in GitLab 14.10.
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/357271)
for use in GitLab 14.10, and is planned for [removal](https://gitlab.com/gitlab-org/gitlab/-/issues/357271) in GitLab 15.0.
Use [attention requests](../index.md#request-attention-to-a-merge-request) instead.
After a reviewer completes their [merge request reviews](../../../discussions/index.md),
the author of the merge request can request a new review from the reviewer:

View File

@ -15153,6 +15153,9 @@ msgstr ""
msgid "Explore groups"
msgstr ""
msgid "Explore paid plans"
msgstr ""
msgid "Explore projects"
msgstr ""
@ -16258,6 +16261,9 @@ msgstr ""
msgid "From %{providerTitle}"
msgstr ""
msgid "From June 22, 2022 (GitLab 15.1), free personal namespaces and top-level groups will be limited to %{free_limit} members"
msgstr ""
msgid "From issue creation until deploy to production"
msgstr ""
@ -22896,6 +22902,9 @@ msgstr ""
msgid "Manage labels"
msgstr ""
msgid "Manage members"
msgstr ""
msgid "Manage milestones"
msgstr ""
@ -32184,6 +32193,16 @@ msgstr ""
msgid "Runners|%{percentage} spot."
msgstr ""
msgid "Runners|%{strongStart}%{count}%{strongEnd} runner selected"
msgid_plural "Runners|%{strongStart}%{count}%{strongEnd} runners selected"
msgstr[0] ""
msgstr[1] ""
msgid "Runners|%{strongStart}%{count}%{strongEnd} runner will be permanently deleted and no longer available for projects or groups in the instance. Are you sure you want to continue?"
msgid_plural "Runners|%{strongStart}%{count}%{strongEnd} runners will be permanently deleted and no longer available for projects or groups in the instance. Are you sure you want to continue?"
msgstr[0] ""
msgstr[1] ""
msgid "Runners|A capacity of 1 enables warm HA through Auto Scaling group re-spawn. A capacity of 2 enables hot HA because the service is available even when a node is lost. A capacity of 3 or more enables hot HA and manual scaling of runner fleet."
msgstr ""
@ -32229,9 +32248,15 @@ msgstr ""
msgid "Runners|Change to project runner"
msgstr ""
msgid "Runners|Checkbox"
msgstr ""
msgid "Runners|Choose your preferred GitLab Runner"
msgstr ""
msgid "Runners|Clear selection"
msgstr ""
msgid "Runners|Command to register runner"
msgstr ""
@ -32244,12 +32269,20 @@ msgstr ""
msgid "Runners|Copy registration token"
msgstr ""
msgid "Runners|Delete %d runner"
msgid_plural "Runners|Delete %d runners"
msgstr[0] ""
msgstr[1] ""
msgid "Runners|Delete runner"
msgstr ""
msgid "Runners|Delete runner %{name}?"
msgstr ""
msgid "Runners|Delete selected"
msgstr ""
msgid "Runners|Deploy GitLab Runner in AWS"
msgstr ""
@ -32340,6 +32373,11 @@ msgstr ""
msgid "Runners|Paused"
msgstr ""
msgid "Runners|Permanently delete %d runner"
msgid_plural "Runners|Permanently delete %d runners"
msgstr[0] ""
msgstr[1] ""
msgid "Runners|Platform"
msgstr ""
@ -43243,6 +43281,9 @@ msgstr ""
msgid "YouTube URL or ID"
msgstr ""
msgid "Your %{doc_link_start}namespace%{doc_link_end}, %{strong_start}%{namespace_name}%{strong_end} has more than %{free_limit} members. From June 22, 2022, it will be limited to %{free_limit}, and the remaining members will get a %{link_start}status of Over limit%{link_end} and lose access to the namespace. You can go to the Usage Quotas page to manage which %{free_limit} members will remain in your namespace. To get more members, an owner can start a trial or upgrade to a paid tier."
msgstr ""
msgid "Your %{group} membership will now expire in %{days}."
msgstr ""

View File

@ -75,13 +75,16 @@ module QA
# Download files from GCS bucket by environment name
# Delete the files afterward
def download(ci_project_name)
files_list = gcs_storage.list_objects(BUCKET, prefix: ci_project_name).items.each_with_object([]) do |obj, arr|
bucket_items = gcs_storage.list_objects(BUCKET, prefix: ci_project_name).items
files_list = bucket_items&.each_with_object([]) do |obj, arr|
arr << obj.name
end
return puts "\nNothing to download!" if files_list.empty?
return puts "\nNothing to download!" if files_list.blank?
FileUtils.mkdir_p('tmp/')
files_list.each do |file_name|
local_path = "tmp/#{file_name.split('/').last}"
Runtime::Logger.info("Downloading #{file_name} to #{local_path}")

View File

@ -58,6 +58,11 @@ FactoryBot.define do
end
end
trait :task do
issue_type { :task }
association :work_item_type, :default, :task
end
factory :incident do
issue_type { :incident }
association :work_item_type, :default, :incident

View File

@ -37,5 +37,10 @@ FactoryBot.define do
base_type { WorkItems::Type.base_types[:requirement] }
icon_name { 'issue-type-requirements' }
end
trait :task do
base_type { WorkItems::Type.base_types[:task] }
icon_name { 'issue-type-task' }
end
end
end

View File

@ -135,7 +135,7 @@ describe('Grouped code quality reports app', () => {
});
it('does not render a help icon', () => {
expect(findWidget().find('[data-testid="question-icon"]').exists()).toBe(false);
expect(findWidget().find('[data-testid="question-o-icon"]').exists()).toBe(false);
});
describe('when base report was not found', () => {
@ -144,7 +144,7 @@ describe('Grouped code quality reports app', () => {
});
it('renders a help icon with more information', () => {
expect(findWidget().find('[data-testid="question-icon"]').exists()).toBe(true);
expect(findWidget().find('[data-testid="question-o-icon"]').exists()).toBe(true);
});
});
});

View File

@ -13,6 +13,7 @@ import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
import { createLocalState } from '~/runner/graphql/list/local_state';
import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
@ -43,6 +44,7 @@ import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered
import { runnersData, runnersCountData, runnersDataPaginated } from '../mock_data';
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
const mockRunners = runnersData.data.runners.nodes;
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
@ -58,6 +60,8 @@ describe('AdminRunnersApp', () => {
let wrapper;
let mockRunnersQuery;
let mockRunnersCountQuery;
let cacheConfig;
let localMutations;
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
@ -69,18 +73,30 @@ describe('AdminRunnersApp', () => {
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
const createComponent = ({
props = {},
mountFn = shallowMountExtended,
provide,
...options
} = {}) => {
({ cacheConfig, localMutations } = createLocalState());
const handlers = [
[adminRunnersQuery, mockRunnersQuery],
[adminRunnersCountQuery, mockRunnersCountQuery],
];
wrapper = mountFn(AdminRunnersApp, {
apolloProvider: createMockApollo(handlers),
apolloProvider: createMockApollo(handlers, {}, cacheConfig),
propsData: {
registrationToken: mockRegistrationToken,
...props,
},
provide: {
localMutations,
...provide,
},
...options,
});
};
@ -173,7 +189,7 @@ describe('AdminRunnersApp', () => {
});
it('shows the runners list', () => {
expect(findRunnerList().props('runners')).toEqual(runnersData.data.runners.nodes);
expect(findRunnerList().props('runners')).toEqual(mockRunners);
});
it('runner item links to the runner admin page', async () => {
@ -181,7 +197,7 @@ describe('AdminRunnersApp', () => {
await waitForPromises();
const { id, shortSha } = runnersData.data.runners.nodes[0];
const { id, shortSha } = mockRunners[0];
const numericId = getIdFromGraphQLId(id);
const runnerLink = wrapper.find('tr [data-testid="td-summary"]').find(GlLink);
@ -197,7 +213,7 @@ describe('AdminRunnersApp', () => {
const runnerActions = wrapper.find('tr [data-testid="td-actions"]').find(RunnerActionsCell);
const runner = runnersData.data.runners.nodes[0];
const runner = mockRunners[0];
expect(runnerActions.props()).toEqual({
runner,
@ -232,8 +248,7 @@ describe('AdminRunnersApp', () => {
describe('Single runner row', () => {
let showToast;
const mockRunner = runnersData.data.runners.nodes[0];
const { id: graphqlId, shortSha } = mockRunner;
const { id: graphqlId, shortSha } = mockRunners[0];
const id = getIdFromGraphQLId(graphqlId);
const COUNT_QUERIES = 7; // Smart queries that display a filtered count of runners
const FILTERED_COUNT_QUERIES = 4; // Smart queries that display a count of runners in tabs
@ -333,6 +348,41 @@ describe('AdminRunnersApp', () => {
expect(findRunnerList().props('loading')).toBe(true);
});
describe('when bulk delete is enabled', () => {
beforeEach(() => {
createComponent({
provide: {
glFeatures: { adminRunnersBulkDelete: true },
},
});
});
it('runner list is checkable', () => {
expect(findRunnerList().props('checkable')).toBe(true);
});
it('responds to checked items by updating the local cache', () => {
const setRunnerCheckedMock = jest
.spyOn(localMutations, 'setRunnerChecked')
.mockImplementation(() => {});
const runner = mockRunners[0];
expect(setRunnerCheckedMock).toHaveBeenCalledTimes(0);
findRunnerList().vm.$emit('checked', {
runner,
isChecked: true,
});
expect(setRunnerCheckedMock).toHaveBeenCalledTimes(1);
expect(setRunnerCheckedMock).toHaveBeenCalledWith({
runner,
isChecked: true,
});
});
});
describe('when no runners are found', () => {
beforeEach(async () => {
mockRunnersQuery = jest.fn().mockResolvedValue({

View File

@ -0,0 +1,103 @@
import Vue from 'vue';
import { GlSprintf } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createLocalState } from '~/runner/graphql/list/local_state';
import waitForPromises from 'helpers/wait_for_promises';
Vue.use(VueApollo);
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
describe('RunnerBulkDelete', () => {
let wrapper;
let mockState;
let mockCheckedRunnerIds;
const findClearBtn = () => wrapper.findByTestId('clear-btn');
const findDeleteBtn = () => wrapper.findByTestId('delete-btn');
const createComponent = () => {
const { cacheConfig, localMutations } = mockState;
wrapper = shallowMountExtended(RunnerBulkDelete, {
apolloProvider: createMockApollo(undefined, undefined, cacheConfig),
provide: {
localMutations,
},
stubs: {
GlSprintf,
},
});
};
beforeEach(() => {
mockState = createLocalState();
jest
.spyOn(mockState.cacheConfig.typePolicies.Query.fields, 'checkedRunnerIds')
.mockImplementation(() => mockCheckedRunnerIds);
});
afterEach(() => {
wrapper.destroy();
});
describe('When no runners are checked', () => {
beforeEach(async () => {
mockCheckedRunnerIds = [];
createComponent();
await waitForPromises();
});
it('shows no contents', () => {
expect(wrapper.html()).toBe('');
});
});
describe.each`
count | ids | text
${1} | ${['gid:Runner/1']} | ${'1 runner'}
${2} | ${['gid:Runner/1', 'gid:Runner/2']} | ${'2 runners'}
`('When $count runner(s) are checked', ({ count, ids, text }) => {
beforeEach(() => {
mockCheckedRunnerIds = ids;
createComponent();
jest.spyOn(mockState.localMutations, 'clearChecked').mockImplementation(() => {});
});
it(`shows "${text}"`, () => {
expect(wrapper.text()).toContain(text);
});
it('clears selection', () => {
expect(mockState.localMutations.clearChecked).toHaveBeenCalledTimes(0);
findClearBtn().vm.$emit('click');
expect(mockState.localMutations.clearChecked).toHaveBeenCalledTimes(1);
});
it('shows confirmation modal', () => {
expect(confirmAction).toHaveBeenCalledTimes(0);
findDeleteBtn().vm.$emit('click');
expect(confirmAction).toHaveBeenCalledTimes(1);
const [, confirmOptions] = confirmAction.mock.calls[0];
const { title, modalHtmlMessage, primaryBtnText } = confirmOptions;
expect(title).toMatch(text);
expect(primaryBtnText).toMatch(text);
expect(modalHtmlMessage).toMatch(`<strong>${count}</strong>`);
});
});
});

View File

@ -55,7 +55,7 @@ describe('RunnerList', () => {
});
it('Sets runner id as a row key', () => {
createComponent({});
createComponent();
expect(findTable().attributes('primary-key')).toBe('id');
});
@ -90,6 +90,35 @@ describe('RunnerList', () => {
expect(findCell({ fieldKey: 'actions' }).exists()).toBe(true);
});
describe('When the list is checkable', () => {
beforeEach(() => {
createComponent(
{
props: {
checkable: true,
},
},
mountExtended,
);
});
it('Displays a checkbox field', () => {
expect(findCell({ fieldKey: 'checkbox' }).find('input').exists()).toBe(true);
});
it('Emits a checked event', () => {
const checkbox = findCell({ fieldKey: 'checkbox' }).find('input');
checkbox.setChecked();
expect(wrapper.emitted('checked')).toHaveLength(1);
expect(wrapper.emitted('checked')[0][0]).toEqual({
isChecked: true,
runner: mockRunners[0],
});
});
});
describe('Scoped cell slots', () => {
it('Render #runner-name slot in "summary" cell', () => {
createComponent(

View File

@ -0,0 +1,72 @@
import createApolloClient from '~/lib/graphql';
import { createLocalState } from '~/runner/graphql/list/local_state';
import getCheckedRunnerIdsQuery from '~/runner/graphql/list/checked_runner_ids.query.graphql';
describe('~/runner/graphql/list/local_state', () => {
let localState;
let apolloClient;
const createSubject = () => {
if (apolloClient) {
throw new Error('test subject already exists!');
}
localState = createLocalState();
const { cacheConfig, typeDefs } = localState;
apolloClient = createApolloClient({}, { cacheConfig, typeDefs });
};
const queryCheckedRunnerIds = () => {
const { checkedRunnerIds } = apolloClient.readQuery({
query: getCheckedRunnerIdsQuery,
});
return checkedRunnerIds;
};
beforeEach(() => {
createSubject();
});
afterEach(() => {
localState = null;
apolloClient = null;
});
describe('default', () => {
it('has empty checked list', () => {
expect(queryCheckedRunnerIds()).toEqual([]);
});
});
describe.each`
inputs | expected
${[['a', true], ['b', true], ['b', true]]} | ${['a', 'b']}
${[['a', true], ['b', true], ['a', false]]} | ${['b']}
${[['c', true], ['b', true], ['a', true], ['d', false]]} | ${['c', 'b', 'a']}
`('setRunnerChecked', ({ inputs, expected }) => {
beforeEach(() => {
inputs.forEach(([id, isChecked]) => {
localState.localMutations.setRunnerChecked({ runner: { id }, isChecked });
});
});
it(`for inputs="${inputs}" has a ids="[${expected}]"`, () => {
expect(queryCheckedRunnerIds()).toEqual(expected);
});
});
describe('clearChecked', () => {
it('clears all checked items', () => {
['a', 'b', 'c'].forEach((id) => {
localState.localMutations.setRunnerChecked({ runner: { id }, isChecked: true });
});
expect(queryCheckedRunnerIds()).toEqual(['a', 'b', 'c']);
localState.localMutations.clearChecked();
expect(queryCheckedRunnerIds()).toEqual([]);
});
});
});

View File

@ -44,6 +44,10 @@ describe('~/runner/utils', () => {
thClass: expect.arrayContaining(mockClasses),
});
});
it('a field with custom options', () => {
expect(tableField({ foo: 'bar' })).toMatchObject({ foo: 'bar' });
});
});
describe('getPaginationVariables', () => {

View File

@ -33,6 +33,7 @@ const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
const autoDevopsPath = '/autoDevopsPath';
const gitlabCiHistoryPath = 'test/historyPath';
const projectFullPath = 'namespace/project';
const vulnerabilityTrainingDocsPath = 'user/application_security/vulnerabilities/index';
useLocalStorageSpy();
@ -55,6 +56,7 @@ describe('App component', () => {
autoDevopsHelpPagePath,
autoDevopsPath,
projectFullPath,
vulnerabilityTrainingDocsPath,
glFeatures: {
secureVulnerabilityTraining,
},
@ -462,9 +464,7 @@ describe('App component', () => {
const trainingLink = findVulnerabilityManagementTab().findComponent(GlLink);
expect(trainingLink.text()).toBe('Learn more about vulnerability training');
expect(trainingLink.attributes('href')).toBe(
'/help/user/application_security/vulnerabilities/index#enable-security-training-for-vulnerabilities',
);
expect(trainingLink.attributes('href')).toBe(vulnerabilityTrainingDocsPath);
});
});

View File

@ -34,7 +34,7 @@ describe('HelpPopover', () => {
it('renders a link button with an icon question', () => {
expect(findQuestionButton().props()).toMatchObject({
icon: 'question',
icon: 'question-o',
variant: 'link',
});
});

View File

@ -10,4 +10,10 @@ RSpec.describe Projects::Security::ConfigurationHelper do
it { is_expected.to eq("https://#{ApplicationHelper.promo_host}/pricing/") }
end
describe 'vulnerability_training_docs_path' do
subject { helper.vulnerability_training_docs_path }
it { is_expected.to eq(help_page_path('user/application_security/vulnerabilities/index', anchor: 'enable-security-training-for-vulnerabilities')) }
end
end

View File

@ -5,19 +5,42 @@ require 'spec_helper'
RSpec.describe IssuePresenter do
include Gitlab::Routing.url_helpers
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:issue) { create(:issue, project: project) }
let(:presenter) { described_class.new(issue, current_user: user) }
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:task) { create(:issue, :task, project: project) }
before do
let(:presented_issue) { issue }
let(:presenter) { described_class.new(presented_issue, current_user: user) }
before_all do
group.add_developer(user)
end
describe '#web_url' do
it 'returns correct path' do
expect(presenter.web_url).to eq("http://localhost/#{group.name}/#{project.name}/-/issues/#{issue.iid}")
expect(presenter.web_url).to eq("http://localhost/#{group.name}/#{project.name}/-/issues/#{presented_issue.iid}")
end
context 'when issue type is task' do
let(:presented_issue) { task }
context 'when work_items feature flag is enabled' do
it 'returns a work item url for the task' do
expect(presenter.web_url).to eq(project_work_items_url(project, work_items_path: presented_issue.id))
end
end
context 'when work_items feature flag is disabled' do
before do
stub_feature_flags(work_items: false)
end
it 'returns an issue url for the task' do
expect(presenter.web_url).to eq("http://localhost/#{group.name}/#{project.name}/-/issues/#{presented_issue.iid}")
end
end
end
end
@ -29,7 +52,7 @@ RSpec.describe IssuePresenter do
end
it 'returns subscribed' do
create(:subscription, user: user, project: project, subscribable: issue, subscribed: true)
create(:subscription, user: user, project: project, subscribable: presented_issue, subscribed: true)
is_expected.to be(true)
end
@ -37,7 +60,27 @@ RSpec.describe IssuePresenter do
describe '#issue_path' do
it 'returns correct path' do
expect(presenter.issue_path).to eq("/#{group.name}/#{project.name}/-/issues/#{issue.iid}")
expect(presenter.issue_path).to eq("/#{group.name}/#{project.name}/-/issues/#{presented_issue.iid}")
end
context 'when issue type is task' do
let(:presented_issue) { task }
context 'when work_items feature flag is enabled' do
it 'returns a work item path for the task' do
expect(presenter.issue_path).to eq(project_work_items_path(project, work_items_path: presented_issue.id))
end
end
context 'when work_items feature flag is disabled' do
before do
stub_feature_flags(work_items: false)
end
it 'returns an issue path for the task' do
expect(presenter.issue_path).to eq("/#{group.name}/#{project.name}/-/issues/#{presented_issue.iid}")
end
end
end
end

View File

@ -1,118 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'groups/edit.html.haml' do
include Devise::Test::ControllerHelpers
describe '"Share with group lock" setting' do
let(:root_owner) { create(:user) }
let(:root_group) { create(:group) }
before do
root_group.add_owner(root_owner)
end
shared_examples_for '"Share with group lock" setting' do |checkbox_options|
it 'has the correct label, help text, and checkbox options' do
assign(:group, test_group)
allow(view).to receive(:can?).with(test_user, :admin_group, test_group).and_return(true)
allow(view).to receive(:can_change_group_visibility_level?).and_return(false)
allow(view).to receive(:current_user).and_return(test_user)
expect(view).to receive(:can_change_share_with_group_lock?).and_return(!checkbox_options[:disabled])
expect(view).to receive(:share_with_group_lock_help_text).and_return('help text here')
render
expect(rendered).to have_content("Prevent sharing a project within #{test_group.name} with other groups")
expect(rendered).to have_content('help text here')
expect(rendered).to have_field('group_share_with_group_lock', **checkbox_options)
end
end
context 'for a root group' do
let(:test_group) { root_group }
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
context 'for a subgroup' do
let!(:subgroup) { create(:group, parent: root_group) }
let(:sub_owner) { create(:user) }
let(:test_group) { subgroup }
context 'when the root_group has "Share with group lock" disabled' do
context 'when the subgroup has "Share with group lock" disabled' do
context 'as the root_owner' do
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
context 'as the sub_owner' do
let(:test_user) { sub_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
end
context 'when the subgroup has "Share with group lock" enabled' do
before do
subgroup.update_column(:share_with_group_lock, true)
end
context 'as the root_owner' do
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
end
context 'as the sub_owner' do
let(:test_user) { sub_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
end
end
end
context 'when the root_group has "Share with group lock" enabled' do
before do
root_group.update_column(:share_with_group_lock, true)
end
context 'when the subgroup has "Share with group lock" disabled (parent overridden)' do
context 'as the root_owner' do
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
context 'as the sub_owner' do
let(:test_user) { sub_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
end
context 'when the subgroup has "Share with group lock" enabled (same as parent)' do
before do
subgroup.update_column(:share_with_group_lock, true)
end
context 'as the root_owner' do
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
end
context 'as the sub_owner' do
let(:test_user) { sub_owner }
it_behaves_like '"Share with group lock" setting', { disabled: true, checked: true }
end
end
end
end
end
end