Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
237ead18b9
commit
981fb44c36
|
|
@ -69,7 +69,6 @@ export default {
|
|||
editButtonAttrs() {
|
||||
return {
|
||||
'data-testid': 'edit',
|
||||
icon: 'pencil-square',
|
||||
href: this.userPaths.edit,
|
||||
};
|
||||
},
|
||||
|
|
@ -101,6 +100,7 @@ export default {
|
|||
<gl-button
|
||||
v-else
|
||||
v-gl-tooltip="$options.i18n.edit"
|
||||
icon="pencil-square"
|
||||
v-bind="editButtonAttrs"
|
||||
:aria-label="$options.i18n.edit"
|
||||
/>
|
||||
|
|
@ -108,10 +108,9 @@ export default {
|
|||
|
||||
<div v-if="hasDropdownActions" class="gl-p-2">
|
||||
<gl-dropdown
|
||||
v-gl-tooltip="$options.i18n.userAdministration"
|
||||
data-testid="dropdown-toggle"
|
||||
:text="$options.i18n.userAdministration"
|
||||
:text-sr-only="!showButtonLabels"
|
||||
icon="ellipsis_h"
|
||||
icon="ellipsis_v"
|
||||
data-qa-selector="user_actions_dropdown_toggle"
|
||||
:data-qa-username="user.username"
|
||||
no-caret
|
||||
|
|
|
|||
|
|
@ -23,11 +23,21 @@ export default {
|
|||
GlModalDirective,
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
inject: ['newClusterPath', 'addClusterPath', 'canAddCluster'],
|
||||
inject: ['newClusterPath', 'addClusterPath', 'canAddCluster', 'displayClusterAgents'],
|
||||
computed: {
|
||||
tooltip() {
|
||||
const { connectWithAgent, dropdownDisabledHint } = this.$options.i18n;
|
||||
return this.canAddCluster ? connectWithAgent : dropdownDisabledHint;
|
||||
const { connectWithAgent, connectExistingCluster, dropdownDisabledHint } = this.$options.i18n;
|
||||
|
||||
if (!this.canAddCluster) {
|
||||
return dropdownDisabledHint;
|
||||
} else if (this.displayClusterAgents) {
|
||||
return connectWithAgent;
|
||||
}
|
||||
|
||||
return connectExistingCluster;
|
||||
},
|
||||
shouldTriggerModal() {
|
||||
return this.canAddCluster && this.displayClusterAgents;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -37,24 +47,27 @@ export default {
|
|||
<div class="nav-controls gl-ml-auto">
|
||||
<gl-dropdown
|
||||
ref="dropdown"
|
||||
v-gl-modal-directive="canAddCluster && $options.INSTALL_AGENT_MODAL_ID"
|
||||
v-gl-modal-directive="shouldTriggerModal && $options.INSTALL_AGENT_MODAL_ID"
|
||||
v-gl-tooltip="tooltip"
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
:text="$options.i18n.actionsButton"
|
||||
:disabled="!canAddCluster"
|
||||
split
|
||||
:split="displayClusterAgents"
|
||||
right
|
||||
>
|
||||
<gl-dropdown-section-header>{{ $options.i18n.agent }}</gl-dropdown-section-header>
|
||||
<gl-dropdown-item
|
||||
v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
|
||||
data-testid="connect-new-agent-link"
|
||||
>
|
||||
{{ $options.i18n.connectWithAgent }}
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-divider />
|
||||
<gl-dropdown-section-header>{{ $options.i18n.certificate }}</gl-dropdown-section-header>
|
||||
<template v-if="displayClusterAgents">
|
||||
<gl-dropdown-section-header>{{ $options.i18n.agent }}</gl-dropdown-section-header>
|
||||
<gl-dropdown-item
|
||||
v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
|
||||
data-testid="connect-new-agent-link"
|
||||
>
|
||||
{{ $options.i18n.connectWithAgent }}
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-divider />
|
||||
<gl-dropdown-section-header>{{ $options.i18n.certificate }}</gl-dropdown-section-header>
|
||||
</template>
|
||||
|
||||
<gl-dropdown-item :href="newClusterPath" data-testid="new-cluster-link" @click.stop>
|
||||
{{ $options.i18n.createNewCluster }}
|
||||
</gl-dropdown-item>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { GlTabs, GlTab } from '@gitlab/ui';
|
|||
import Tracking from '~/tracking';
|
||||
import {
|
||||
CLUSTERS_TABS,
|
||||
CERTIFICATE_TAB,
|
||||
MAX_CLUSTERS_LIST,
|
||||
MAX_LIST_COUNT,
|
||||
AGENT,
|
||||
|
|
@ -29,6 +30,7 @@ export default {
|
|||
},
|
||||
CLUSTERS_TABS,
|
||||
mixins: [trackingMixin],
|
||||
inject: ['displayClusterAgents'],
|
||||
props: {
|
||||
defaultBranchName: {
|
||||
default: '.noBranch',
|
||||
|
|
@ -42,6 +44,11 @@ export default {
|
|||
maxAgents: MAX_CLUSTERS_LIST,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
clusterTabs() {
|
||||
return this.displayClusterAgents ? CLUSTERS_TABS : [CERTIFICATE_TAB];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedTabIndex(val) {
|
||||
this.onTabChange(val);
|
||||
|
|
@ -49,10 +56,10 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
setSelectedTab(tabName) {
|
||||
this.selectedTabIndex = CLUSTERS_TABS.findIndex((tab) => tab.queryParamValue === tabName);
|
||||
this.selectedTabIndex = this.clusterTabs.findIndex((tab) => tab.queryParamValue === tabName);
|
||||
},
|
||||
onTabChange(tab) {
|
||||
const tabName = CLUSTERS_TABS[tab].queryParamValue;
|
||||
const tabName = this.clusterTabs[tab].queryParamValue;
|
||||
|
||||
this.maxAgents = tabName === AGENT ? MAX_LIST_COUNT : MAX_CLUSTERS_LIST;
|
||||
this.track(EVENT_ACTIONS_CHANGE, { property: tabName });
|
||||
|
|
@ -69,7 +76,7 @@ export default {
|
|||
lazy
|
||||
>
|
||||
<gl-tab
|
||||
v-for="(tab, idx) in $options.CLUSTERS_TABS"
|
||||
v-for="(tab, idx) in clusterTabs"
|
||||
:key="idx"
|
||||
:title="tab.title"
|
||||
:query-param-value="tab.queryParamValue"
|
||||
|
|
|
|||
|
|
@ -232,6 +232,12 @@ export const CERTIFICATE_BASED_CARD_INFO = {
|
|||
|
||||
export const MAX_CLUSTERS_LIST = 6;
|
||||
|
||||
export const CERTIFICATE_TAB = {
|
||||
title: s__('ClusterAgents|Certificate'),
|
||||
component: 'clusters',
|
||||
queryParamValue: 'certificate_based',
|
||||
};
|
||||
|
||||
export const CLUSTERS_TABS = [
|
||||
{
|
||||
title: s__('ClusterAgents|All'),
|
||||
|
|
@ -243,11 +249,7 @@ export const CLUSTERS_TABS = [
|
|||
component: 'agents',
|
||||
queryParamValue: 'agent',
|
||||
},
|
||||
{
|
||||
title: s__('ClusterAgents|Certificate'),
|
||||
component: 'clusters',
|
||||
queryParamValue: 'certificate_based',
|
||||
},
|
||||
CERTIFICATE_TAB,
|
||||
];
|
||||
|
||||
export const CLUSTERS_ACTIONS = {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,61 @@
|
|||
import { GlToast } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import loadClusters from './load_clusters';
|
||||
import loadMainView from './load_main_view';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import ClustersMainView from './components/clusters_main_view.vue';
|
||||
import { createStore } from './store';
|
||||
|
||||
Vue.use(GlToast);
|
||||
Vue.use(VueApollo);
|
||||
|
||||
export default () => {
|
||||
loadClusters(Vue);
|
||||
loadMainView(Vue, VueApollo);
|
||||
const el = document.querySelector('.js-clusters-main-view');
|
||||
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const defaultClient = createDefaultClient();
|
||||
|
||||
const {
|
||||
emptyStateImage,
|
||||
defaultBranchName,
|
||||
projectPath,
|
||||
kasAddress,
|
||||
newClusterPath,
|
||||
addClusterPath,
|
||||
emptyStateHelpText,
|
||||
clustersEmptyStateImage,
|
||||
canAddCluster,
|
||||
canAdminCluster,
|
||||
gitlabVersion,
|
||||
displayClusterAgents,
|
||||
} = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
apolloProvider: new VueApollo({ defaultClient }),
|
||||
provide: {
|
||||
emptyStateImage,
|
||||
projectPath,
|
||||
kasAddress,
|
||||
newClusterPath,
|
||||
addClusterPath,
|
||||
emptyStateHelpText,
|
||||
clustersEmptyStateImage,
|
||||
canAddCluster: parseBoolean(canAddCluster),
|
||||
canAdminCluster: parseBoolean(canAdminCluster),
|
||||
gitlabVersion,
|
||||
displayClusterAgents: parseBoolean(displayClusterAgents),
|
||||
},
|
||||
store: createStore(el.dataset),
|
||||
render(createElement) {
|
||||
return createElement(ClustersMainView, {
|
||||
props: {
|
||||
defaultBranchName,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
import Clusters from './components/clusters.vue';
|
||||
import { createStore } from './store';
|
||||
|
||||
export default (Vue) => {
|
||||
const el = document.querySelector('#js-clusters-list-app');
|
||||
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { emptyStateHelpText, newClusterPath, clustersEmptyStateImage } = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
provide: {
|
||||
emptyStateHelpText,
|
||||
newClusterPath,
|
||||
clustersEmptyStateImage,
|
||||
},
|
||||
store: createStore(el.dataset),
|
||||
render(createElement) {
|
||||
return createElement(Clusters);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import ClustersMainView from './components/clusters_main_view.vue';
|
||||
import { createStore } from './store';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
export default () => {
|
||||
const el = document.querySelector('.js-clusters-main-view');
|
||||
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const defaultClient = createDefaultClient();
|
||||
|
||||
const {
|
||||
emptyStateImage,
|
||||
defaultBranchName,
|
||||
projectPath,
|
||||
kasAddress,
|
||||
newClusterPath,
|
||||
addClusterPath,
|
||||
emptyStateHelpText,
|
||||
clustersEmptyStateImage,
|
||||
canAddCluster,
|
||||
canAdminCluster,
|
||||
gitlabVersion,
|
||||
} = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
apolloProvider: new VueApollo({ defaultClient }),
|
||||
provide: {
|
||||
emptyStateImage,
|
||||
projectPath,
|
||||
kasAddress,
|
||||
newClusterPath,
|
||||
addClusterPath,
|
||||
emptyStateHelpText,
|
||||
clustersEmptyStateImage,
|
||||
canAddCluster: parseBoolean(canAddCluster),
|
||||
canAdminCluster: parseBoolean(canAdminCluster),
|
||||
gitlabVersion,
|
||||
},
|
||||
store: createStore(el.dataset),
|
||||
render(createElement) {
|
||||
return createElement(ClustersMainView, {
|
||||
props: {
|
||||
defaultBranchName,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -5,7 +5,7 @@ import createStore from './store';
|
|||
|
||||
Vue.use(Translate);
|
||||
|
||||
export const initHeaderSearchApp = () => {
|
||||
export const initHeaderSearchApp = (search = '') => {
|
||||
const el = document.getElementById('js-header-search');
|
||||
|
||||
if (!el) {
|
||||
|
|
@ -18,7 +18,7 @@ export const initHeaderSearchApp = () => {
|
|||
|
||||
return new Vue({
|
||||
el,
|
||||
store: createStore({ searchPath, issuesPath, mrPath, autocompletePath, searchContext }),
|
||||
store: createStore({ searchPath, issuesPath, mrPath, autocompletePath, searchContext, search }),
|
||||
render(createElement) {
|
||||
return createElement(HeaderSearchApp);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,11 +13,12 @@ export const getStoreConfig = ({
|
|||
mrPath,
|
||||
autocompletePath,
|
||||
searchContext,
|
||||
search,
|
||||
}) => ({
|
||||
actions,
|
||||
getters,
|
||||
mutations,
|
||||
state: createState({ searchPath, issuesPath, mrPath, autocompletePath, searchContext }),
|
||||
state: createState({ searchPath, issuesPath, mrPath, autocompletePath, searchContext, search }),
|
||||
});
|
||||
|
||||
const createStore = (config) => new Vuex.Store(getStoreConfig(config));
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
const createState = ({ searchPath, issuesPath, mrPath, autocompletePath, searchContext }) => ({
|
||||
const createState = ({
|
||||
searchPath,
|
||||
issuesPath,
|
||||
mrPath,
|
||||
autocompletePath,
|
||||
searchContext,
|
||||
search: '',
|
||||
search,
|
||||
}) => ({
|
||||
searchPath,
|
||||
issuesPath,
|
||||
mrPath,
|
||||
autocompletePath,
|
||||
searchContext,
|
||||
search,
|
||||
autocompleteOptions: [],
|
||||
autocompleteError: false,
|
||||
loading: false,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,10 @@ export default {
|
|||
computed: {
|
||||
actionPrimary() {
|
||||
return {
|
||||
attributes: { variant: 'danger' },
|
||||
attributes: {
|
||||
variant: 'danger',
|
||||
'data-qa-selector': 'confirm_delete_issue_button',
|
||||
},
|
||||
text: this.title,
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -290,6 +290,7 @@ export default {
|
|||
class="gl-display-none gl-sm-display-inline-flex! gl-ml-3"
|
||||
icon="ellipsis_v"
|
||||
category="tertiary"
|
||||
data-qa-selector="issue_actions_ellipsis_dropdown"
|
||||
:text="dropdownText"
|
||||
:text-sr-only="true"
|
||||
data-testid="desktop-dropdown"
|
||||
|
|
@ -323,6 +324,7 @@ export default {
|
|||
<gl-dropdown-item
|
||||
v-gl-modal="$options.deleteModalId"
|
||||
variant="danger"
|
||||
data-qa-selector="delete_issue_button"
|
||||
@click="track('click_dropdown')"
|
||||
>
|
||||
{{ deleteButtonText }}
|
||||
|
|
|
|||
|
|
@ -116,16 +116,18 @@ function deferredInitialisation() {
|
|||
);
|
||||
}
|
||||
|
||||
const search = document.querySelector('#search');
|
||||
if (search) {
|
||||
search.addEventListener(
|
||||
const searchInputBox = document.querySelector('#search');
|
||||
if (searchInputBox) {
|
||||
searchInputBox.addEventListener(
|
||||
'focus',
|
||||
() => {
|
||||
if (gon.features?.newHeaderSearch) {
|
||||
import(/* webpackChunkName: 'globalSearch' */ '~/header_search')
|
||||
.then(async ({ initHeaderSearchApp }) => {
|
||||
await initHeaderSearchApp();
|
||||
document.querySelector('#search').focus();
|
||||
// In case the user started searching before we bootstrapped, let's pass the search along.
|
||||
const initialSearchValue = searchInputBox.value;
|
||||
await initHeaderSearchApp(initialSearchValue);
|
||||
searchInputBox.focus();
|
||||
})
|
||||
.catch(() => {});
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -360,27 +360,10 @@
|
|||
}
|
||||
|
||||
> li {
|
||||
// TODO: Remove this block once all sidebar badges use gl_badge_tag
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/350061
|
||||
.badge.badge-pill:not(.gl-badge) {
|
||||
@include gl-rounded-lg;
|
||||
@include gl-py-1;
|
||||
@include gl-px-3;
|
||||
background-color: $blue-100;
|
||||
color: $blue-700;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.sidebar-sub-level-items:not(.is-fly-out-only) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// TODO: Remove this block once all sidebar badges use gl_badge_tag
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/350061
|
||||
.badge.badge-pill:not(.gl-badge) {
|
||||
@include gl-font-weight-normal;
|
||||
color: $blue-700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1380,24 +1380,11 @@ input {
|
|||
border-radius: 4px;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.sidebar-top-level-items > li .badge.badge-pill:not(.gl-badge) {
|
||||
border-radius: 0.5rem;
|
||||
padding-top: 0.125rem;
|
||||
padding-bottom: 0.125rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
background-color: #064787;
|
||||
color: #9dc7f1;
|
||||
}
|
||||
.sidebar-top-level-items
|
||||
> li.active
|
||||
.sidebar-sub-level-items:not(.is-fly-out-only) {
|
||||
display: block;
|
||||
}
|
||||
.sidebar-top-level-items > li.active .badge.badge-pill:not(.gl-badge) {
|
||||
font-weight: 400;
|
||||
color: #9dc7f1;
|
||||
}
|
||||
.sidebar-top-level-items li > a.gl-link {
|
||||
color: #fafafa;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1365,24 +1365,11 @@ input {
|
|||
border-radius: 4px;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.sidebar-top-level-items > li .badge.badge-pill:not(.gl-badge) {
|
||||
border-radius: 0.5rem;
|
||||
padding-top: 0.125rem;
|
||||
padding-bottom: 0.125rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
background-color: #cbe2f9;
|
||||
color: #0b5cad;
|
||||
}
|
||||
.sidebar-top-level-items
|
||||
> li.active
|
||||
.sidebar-sub-level-items:not(.is-fly-out-only) {
|
||||
display: block;
|
||||
}
|
||||
.sidebar-top-level-items > li.active .badge.badge-pill:not(.gl-badge) {
|
||||
font-weight: 400;
|
||||
color: #0b5cad;
|
||||
}
|
||||
.sidebar-top-level-items li > a.gl-link {
|
||||
color: #303030;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
|||
# Overridden from Doorkeeper::AuthorizationsController to
|
||||
# include the call to session.delete
|
||||
def new
|
||||
logger.info("#{self.class.name}#new: pre_auth_params['scope'] = #{pre_auth_params['scope'].inspect}")
|
||||
|
||||
if pre_auth.authorizable?
|
||||
logger.info("#{self.class.name}#new: pre_auth.scopes = #{pre_auth.scopes.to_a.inspect}")
|
||||
if skip_authorization? || matching_token?
|
||||
auth = authorization.authorize
|
||||
parsed_redirect_uri = URI.parse(auth.redirect_uri)
|
||||
|
|
@ -46,15 +43,9 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
|||
auth_type = params.delete('gl_auth_type')
|
||||
return unless auth_type == 'login'
|
||||
|
||||
logger.info("#{self.class.name}: BEFORE application has read_user: #{application_has_read_user_scope?}")
|
||||
logger.info("#{self.class.name}: BEFORE scope = #{params['scope'].inspect}")
|
||||
|
||||
ensure_read_user_scope!
|
||||
|
||||
params['scope'] = Gitlab::Auth::READ_USER_SCOPE.to_s if application_has_read_user_scope?
|
||||
|
||||
logger.info("#{self.class.name}: AFTER application has read_user: #{application_has_read_user_scope?}")
|
||||
logger.info("#{self.class.name}: AFTER scope = #{params['scope'].inspect}")
|
||||
end
|
||||
|
||||
# Configure the application to support read_user scope, if it already
|
||||
|
|
|
|||
|
|
@ -28,8 +28,10 @@ module ClustersHelper
|
|||
clusters_empty_state_image: image_path('illustrations/empty-state/empty-state-clusters.svg'),
|
||||
empty_state_help_text: clusterable.empty_state_help_text,
|
||||
new_cluster_path: clusterable.new_path(tab: 'create'),
|
||||
add_cluster_path: clusterable.new_path(tab: 'add'),
|
||||
can_add_cluster: clusterable.can_add_cluster?.to_s,
|
||||
can_admin_cluster: clusterable.can_admin_cluster?.to_s
|
||||
can_admin_cluster: clusterable.can_admin_cluster?.to_s,
|
||||
display_cluster_agents: display_cluster_agents?(clusterable).to_s
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -38,7 +40,6 @@ module ClustersHelper
|
|||
default_branch_name: clusterable.default_branch,
|
||||
empty_state_image: image_path('illustrations/empty-state/empty-state-agents.svg'),
|
||||
project_path: clusterable.full_path,
|
||||
add_cluster_path: clusterable.new_path(tab: 'add'),
|
||||
kas_address: Gitlab::Kas.external_url,
|
||||
gitlab_version: Gitlab.version_info
|
||||
}.merge(js_clusters_list_data(clusterable))
|
||||
|
|
|
|||
|
|
@ -10,11 +10,41 @@ class Analytics::CycleAnalytics::Aggregation < ApplicationRecord
|
|||
scope :priority_order, -> { order('last_incremental_run_at ASC NULLS FIRST') }
|
||||
scope :enabled, -> { where('enabled IS TRUE') }
|
||||
|
||||
def estimated_next_run_at
|
||||
return unless enabled
|
||||
return if last_incremental_run_at.nil?
|
||||
|
||||
estimation = (last_incremental_run_at - earliest_last_run_at) + average_aggregation_duration
|
||||
estimation < 1 ? nil : estimation.from_now
|
||||
end
|
||||
|
||||
def self.safe_create_for_group(group)
|
||||
top_level_group = group.root_ancestor
|
||||
return if Analytics::CycleAnalytics::Aggregation.exists?(group_id: top_level_group.id)
|
||||
aggregation = find_by(group_id: top_level_group.id)
|
||||
return aggregation if aggregation.present?
|
||||
|
||||
insert({ group_id: top_level_group.id }, unique_by: :group_id)
|
||||
find_by(group_id: top_level_group.id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def average_aggregation_duration
|
||||
return 0.seconds if incremental_runtimes_in_seconds.empty?
|
||||
|
||||
average = incremental_runtimes_in_seconds.sum.fdiv(incremental_runtimes_in_seconds.size)
|
||||
average.seconds
|
||||
end
|
||||
|
||||
def earliest_last_run_at
|
||||
max = self.class.select(:last_incremental_run_at)
|
||||
.where(enabled: true)
|
||||
.where.not(last_incremental_run_at: nil)
|
||||
.priority_order
|
||||
.limit(1)
|
||||
.to_sql
|
||||
|
||||
connection.select_value("(#{max})")
|
||||
end
|
||||
|
||||
def self.load_batch(last_run_at, batch_size = 100)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module RunnersTokenPrefixable
|
||||
# Prefix for runners_token which can be used to invalidate existing tokens.
|
||||
# The value chosen here is GR (for Gitlab Runner) combined with the rotation
|
||||
# date (20220225) decimal to hex encoded.
|
||||
RUNNERS_TOKEN_PREFIX = 'GR1348941'
|
||||
end
|
||||
|
|
@ -22,11 +22,6 @@ class Group < Namespace
|
|||
|
||||
extend ::Gitlab::Utils::Override
|
||||
|
||||
# Prefix for runners_token which can be used to invalidate existing tokens.
|
||||
# The value chosen here is GR (for Gitlab Runner) combined with the rotation
|
||||
# date (20220225) decimal to hex encoded.
|
||||
RUNNERS_TOKEN_PREFIX = 'GR1348941'
|
||||
|
||||
def self.sti_name
|
||||
'Group'
|
||||
end
|
||||
|
|
@ -124,7 +119,7 @@ class Group < Namespace
|
|||
|
||||
add_authentication_token_field :runners_token,
|
||||
encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
|
||||
prefix: ->(instance) { instance.runners_token_prefix }
|
||||
prefix: RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
|
||||
|
||||
after_create :post_create_hook
|
||||
after_destroy :post_destroy_hook
|
||||
|
|
@ -678,13 +673,9 @@ class Group < Namespace
|
|||
ensure_runners_token!
|
||||
end
|
||||
|
||||
def runners_token_prefix
|
||||
Feature.enabled?(:groups_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
|
||||
end
|
||||
|
||||
override :format_runners_token
|
||||
def format_runners_token(token)
|
||||
"#{runners_token_prefix}#{token}"
|
||||
"#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}#{token}"
|
||||
end
|
||||
|
||||
def project_creation_level
|
||||
|
|
|
|||
|
|
@ -90,11 +90,6 @@ class Project < ApplicationRecord
|
|||
|
||||
DEFAULT_SQUASH_COMMIT_TEMPLATE = '%{title}'
|
||||
|
||||
# Prefix for runners_token which can be used to invalidate existing tokens.
|
||||
# The value chosen here is GR (for Gitlab Runner) combined with the rotation
|
||||
# date (20220225) decimal to hex encoded.
|
||||
RUNNERS_TOKEN_PREFIX = 'GR1348941'
|
||||
|
||||
cache_markdown_field :description, pipeline: :description
|
||||
|
||||
default_value_for :packages_enabled, true
|
||||
|
|
@ -117,7 +112,7 @@ class Project < ApplicationRecord
|
|||
|
||||
add_authentication_token_field :runners_token,
|
||||
encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required },
|
||||
prefix: ->(instance) { instance.runners_token_prefix }
|
||||
prefix: RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
|
||||
|
||||
before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
|
||||
|
||||
|
|
@ -1887,13 +1882,9 @@ class Project < ApplicationRecord
|
|||
ensure_runners_token!
|
||||
end
|
||||
|
||||
def runners_token_prefix
|
||||
Feature.enabled?(:projects_runners_token_prefix, self, default_enabled: :yaml) ? RUNNERS_TOKEN_PREFIX : ''
|
||||
end
|
||||
|
||||
override :format_runners_token
|
||||
def format_runners_token(token)
|
||||
"#{runners_token_prefix}#{token}"
|
||||
"#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}#{token}"
|
||||
end
|
||||
|
||||
def pages_deployed?
|
||||
|
|
|
|||
|
|
@ -27,8 +27,6 @@
|
|||
= render_if_exists 'admin/users/gma_user_badge'
|
||||
|
||||
.gl-my-3.gl-display-flex.gl-flex-wrap.gl-my-n2.gl-mx-n2
|
||||
.gl-p-2
|
||||
#js-admin-user-actions{ data: admin_user_actions_data_attributes(@user) }
|
||||
- if @user != current_user
|
||||
.gl-p-2
|
||||
- if impersonation_enabled? && @user.can?(:log_in)
|
||||
|
|
@ -36,6 +34,8 @@
|
|||
- if can_force_email_confirmation?(@user)
|
||||
%button.btn.gl-button.btn-info.js-confirm-modal-button{ data: confirm_user_data(@user) }
|
||||
= _('Confirm user')
|
||||
.gl-p-2
|
||||
#js-admin-user-actions{ data: admin_user_actions_data_attributes(@user) }
|
||||
= gl_tabs_nav do
|
||||
= gl_tab_link_to _("Account"), admin_user_path(@user)
|
||||
= gl_tab_link_to _("Groups and projects"), projects_admin_user_path(@user)
|
||||
|
|
|
|||
|
|
@ -7,4 +7,4 @@
|
|||
%span.btn.gl-button.btn-confirm.js-add-cluster.disabled.gl-py-2
|
||||
= s_("ClusterIntegration|Connect cluster with certificate")
|
||||
|
||||
#js-clusters-list-app{ data: js_clusters_list_data(clusterable) }
|
||||
.js-clusters-main-view{ data: js_clusters_list_data(clusterable) }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
#js-header-search.header-search{ data: { 'search-context' => header_search_context.to_json,
|
||||
'search-path' => search_path,
|
||||
'issues-path' => issues_dashboard_path,
|
||||
'mr-path' => merge_requests_dashboard_path,
|
||||
'autocomplete-path' => search_autocomplete_path } }
|
||||
= form_tag search_path, method: :get do |_f|
|
||||
.gl-search-box-by-type
|
||||
= sprite_icon('search', css_class: 'gl-search-box-by-type-search-icon gl-icon')
|
||||
%input{ id: 'search', name: 'search', type: "text", placeholder: s_('GlobalSearch|Search GitLab'), class: 'form-control gl-form-input gl-search-box-by-type-input', autocomplete: 'off' }
|
||||
|
||||
= hidden_field_tag :group_id, header_search_context[:group][:id] if header_search_context[:group]
|
||||
= hidden_field_tag :project_id, header_search_context[:project][:id] if header_search_context[:project]
|
||||
|
||||
- if header_search_context[:group] || header_search_context[:project]
|
||||
= hidden_field_tag :scope, header_search_context[:scope]
|
||||
= hidden_field_tag :search_code, header_search_context[:code_search]
|
||||
|
||||
= hidden_field_tag :snippets, header_search_context[:for_snippets]
|
||||
= hidden_field_tag :repository_ref, header_search_context[:ref]
|
||||
= hidden_field_tag :nav_source, 'navbar'
|
||||
|
||||
-# workaround for non-JS feature specs, see spec/support/helpers/search_helpers.rb
|
||||
- if ENV['RAILS_ENV'] == 'test'
|
||||
%noscript= button_tag 'Search'
|
||||
|
|
@ -41,14 +41,7 @@
|
|||
%li.nav-item.header-search-new.d-none.d-lg-block.m-auto
|
||||
- unless current_controller?(:search)
|
||||
- if Feature.enabled?(:new_header_search)
|
||||
#js-header-search.header-search{ data: { 'search-context' => header_search_context.to_json,
|
||||
'search-path' => search_path,
|
||||
'issues-path' => issues_dashboard_path,
|
||||
'mr-path' => merge_requests_dashboard_path,
|
||||
'autocomplete-path' => search_autocomplete_path } }
|
||||
.gl-search-box-by-type
|
||||
= sprite_icon('search', css_class: 'gl-search-box-by-type-search-icon gl-icon')
|
||||
%input{ type: "text", placeholder: s_('GlobalSearch|Search GitLab'), class: 'form-control gl-form-input gl-search-box-by-type-input', id: 'search', autocomplete: 'off' }
|
||||
= render 'layouts/header_search'
|
||||
- else
|
||||
= render 'layouts/search'
|
||||
%li.nav-item{ class: 'd-none d-sm-inline-block d-lg-none' }
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: groups_runners_token_prefix
|
||||
introduced_by_url:
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
|
||||
milestone: '14.9'
|
||||
type: development
|
||||
group: group::database
|
||||
default_enabled: true
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: projects_runners_token_prefix
|
||||
introduced_by_url:
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353805
|
||||
milestone: '14.9'
|
||||
type: development
|
||||
group: group::database
|
||||
default_enabled: true
|
||||
|
|
@ -377,7 +377,7 @@ sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.t
|
|||
The `track-repository` Praefect sub-command adds repositories on disk to the Praefect database to be tracked.
|
||||
|
||||
```shell
|
||||
sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml track-repository -virtual-storage <virtual-storage> -repository <repository> -replicate-immediately
|
||||
sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml track-repository -virtual-storage <virtual-storage> -authoritative-storage <storage-name> -repository <repository> -replicate-immediately
|
||||
```
|
||||
|
||||
- `-virtual-storage` is the virtual storage the repository is located in. Virtual storages are configured in `/etc/gitlab/gitlab.rb` under `praefect['virtual_storages]` and looks like the following:
|
||||
|
|
|
|||
|
|
@ -479,3 +479,19 @@ ssh: Could not resolve hostname gitlab.example.com: nodename nor servname provid
|
|||
```
|
||||
|
||||
If you receive this error, restart your terminal and try the command again.
|
||||
|
||||
### `Key enrollment failed: invalid format` error
|
||||
|
||||
You may receive the following error when [generating an SSH key pair for a FIDO/U2F hardware security key](#generate-an-ssh-key-pair-for-a-fidou2f-hardware-security-key):
|
||||
|
||||
```shell
|
||||
Key enrollment failed: invalid format
|
||||
```
|
||||
|
||||
You can troubleshoot this by trying the following:
|
||||
|
||||
- Run the `ssh-keygen` command using `sudo`.
|
||||
- Verify your IDO/U2F hardware security key supports
|
||||
the key type provided.
|
||||
- Verify the version of OpenSSH is 8.2 or greater by
|
||||
running `ssh -v`.
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ table.supported-languages ul {
|
|||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">Java</td>
|
||||
<td rowspan="2">8, 11, 13, 14, 15, or 16</td>
|
||||
<td rowspan="2">8, 11, 13, 14, 15, 16, or 17</td>
|
||||
<td><a href="https://gradle.org/">Gradle</a><sup><b><a href="#notes-regarding-supported-languages-and-package-managers-1">1</a></b></sup></td>
|
||||
<td>
|
||||
<ul>
|
||||
|
|
@ -335,26 +335,60 @@ To support the following package managers, the GitLab analyzers proceed in two s
|
|||
1. Execute the package manager or a specific task, to export the dependency information.
|
||||
1. Parse the exported dependency information.
|
||||
|
||||
| Package Manager | Preinstalled Versions | Tested Versions |
|
||||
| ------ | ------ | ------ |
|
||||
| Bundler | [2.1.4](https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit/-/blob/v2.11.3/Dockerfile#L15)<sup><b><a href="#exported-dependency-information-notes-1">1</a></b></sup> | [1.17.3](https://gitlab.com/gitlab-org/security-products/tests/ruby-bundler/-/blob/master/Gemfile.lock#L118), [2.1.4](https://gitlab.com/gitlab-org/security-products/tests/ruby-bundler/-/blob/bundler2-FREEZE/Gemfile.lock#L118) |
|
||||
| sbt | [1.6.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/config/.tool-versions#L4) | [1.0.4](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L330), [1.1.4](https://gitlab.com/gitlab-org/security-products/tests/scala-sbt-multiproject/-/blob/main/project/build.properties#L1), [1.1.6](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L339), [1.2.8](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L348), [1.3.12](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L357), [1.4.6](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L366), [1.6.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L384) |
|
||||
| Maven | [3.6.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.23.0/config/.tool-versions#L3) | [3.6.3](https://gitlab.com/gitlab-org/security-products/tests/java-maven/-/blob/master/pom.xml#L3) |
|
||||
| Gradle | [6.7.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.23.0/config/.tool-versions#L5) | [5.6.4](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/master/gradle/wrapper/gradle-wrapper.properties#L3), [6.5](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-14/gradle/wrapper/gradle-wrapper.properties#L3), [6.7-rc-1](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-15/gradle/wrapper/gradle-wrapper.properties#L3), [6.9](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-14-gradle-6-9/gradle/wrapper/gradle-wrapper.properties#L3), [7.0-rc-2](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-16/gradle/wrapper/gradle-wrapper.properties#L3) |
|
||||
| setuptools | [50.3.2](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v2.29.9/Dockerfile#L27) | [57.5.0](https://gitlab.com/gitlab-org/security-products/tests/python-setuptools/-/blob/main/setup.py) |
|
||||
| pip | [20.2.4](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v2.29.9/Dockerfile#L26) | [20.x](https://gitlab.com/gitlab-org/security-products/tests/python-pip/-/blob/master/requirements.txt) |
|
||||
| Pipenv | [2018.11.26](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-python/-/blob/v2.18.4/requirements.txt#L13) | [2018.11.26](https://gitlab.com/gitlab-org/security-products/tests/python-pipenv/-/blob/pipfile-lock-FREEZE/Pipfile.lock#L6)<sup><b><a href="#exported-dependency-information-notes-2">2</a></b></sup>, [2018.11.26](https://gitlab.com/gitlab-org/security-products/tests/python-pipenv/-/blob/master/Pipfile) |
|
||||
| Package Manager | Pre-installed Versions | Tested Versions |
|
||||
| ------ | ------ | ------ |
|
||||
| Bundler | [2.1.4](https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit/-/blob/v2.11.3/Dockerfile#L15)<sup><b><a href="#exported-dependency-information-notes-1">1</a></b></sup> | [1.17.3](https://gitlab.com/gitlab-org/security-products/tests/ruby-bundler/-/blob/master/Gemfile.lock#L118), [2.1.4](https://gitlab.com/gitlab-org/security-products/tests/ruby-bundler/-/blob/bundler2-FREEZE/Gemfile.lock#L118) |
|
||||
| sbt | [1.6.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/config/.tool-versions#L4) | [1.0.4](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L330), [1.1.4](https://gitlab.com/gitlab-org/security-products/tests/scala-sbt-multiproject/-/blob/main/project/build.properties#L1), [1.1.6](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L339), [1.2.8](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L348), [1.3.12](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L357), [1.4.6](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L366), [1.6.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.24.6/.gitlab-ci.yml#L384) |
|
||||
| Maven | [3.6.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.23.0/config/.tool-versions#L3) | [3.6.3](https://gitlab.com/gitlab-org/security-products/tests/java-maven/-/blob/master/pom.xml#L3) |
|
||||
| Gradle | [6.7.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.23.0/config/.tool-versions#L5)<sup><b><a href="#exported-dependency-information-notes-2">2</a></b></sup>, [7.3.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.26.0/config/.tool-versions#L5)<sup><b><a href="#exported-dependency-information-notes-2">2</a></b></sup> | [5.6.4](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/master/gradle/wrapper/gradle-wrapper.properties#L3), [6.5](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-14/gradle/wrapper/gradle-wrapper.properties#L3), [6.7-rc-1](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-15/gradle/wrapper/gradle-wrapper.properties#L3), [6.7.1](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.27.1/.gitlab-ci.yml#L289-297)<sup><b><a href="#exported-dependency-information-notes-3">3</a></b></sup>, [6.9](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-14-gradle-6-9/gradle/wrapper/gradle-wrapper.properties#L3), [7.0-rc-2](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-16/gradle/wrapper/gradle-wrapper.properties#L3), [7.3](https://gitlab.com/gitlab-org/security-products/tests/java-gradle/-/blob/java-14-gradle-7-3/gradle/wrapper/gradle-wrapper.properties#L3), [7.3.3](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven/-/blob/v2.27.1/.gitlab-ci.yml#L299-317)<sup><b><a href="#exported-dependency-information-notes-3">3</a></b></sup> |
|
||||
| setuptools | [50.3.2](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v2.29.9/Dockerfile#L27) | [57.5.0](https://gitlab.com/gitlab-org/security-products/tests/python-setuptools/-/blob/main/setup.py) |
|
||||
| pip | [20.2.4](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v2.29.9/Dockerfile#L26) | [20.x](https://gitlab.com/gitlab-org/security-products/tests/python-pip/-/blob/master/requirements.txt) |
|
||||
| Pipenv | [2018.11.26](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-python/-/blob/v2.18.4/requirements.txt#L13) | [2018.11.26](https://gitlab.com/gitlab-org/security-products/tests/python-pipenv/-/blob/pipfile-lock-FREEZE/Pipfile.lock#L6)<sup><b><a href="#exported-dependency-information-notes-4">4</a></b></sup>, [2018.11.26](https://gitlab.com/gitlab-org/security-products/tests/python-pipenv/-/blob/master/Pipfile) |
|
||||
|
||||
<!-- markdownlint-disable MD044 -->
|
||||
<ol>
|
||||
<li>
|
||||
<a id="exported-dependency-information-notes-1"></a>
|
||||
<p>
|
||||
The installed version of <code>Bundler</code> is only used for the <a href="https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit">bundler-audit</a> analyzer, and is not used for <a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">gemnasium</a>
|
||||
The pre-installed version of <code>Bundler</code> is only used for the <a href="https://gitlab.com/gitlab-org/security-products/analyzers/bundler-audit">bundler-audit</a> analyzer, and is not used for <a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">gemnasium</a>.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<a id="exported-dependency-information-notes-2"></a>
|
||||
<p>
|
||||
Different versions of Java require different versions of Gradle. The versions of Gradle listed in the above table are pre-installed
|
||||
in the analyzer image. The version of Gradle used by the analyzer depends on whether your project uses a <code>gradlew</code>
|
||||
(Gradle wrapper) file or not:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
If your project <i>does not use</i> a <code>gradlew</code> file, then the analyzer automatically switches to one of the
|
||||
pre-installed Gradle versions, based on the version of Java specified by the
|
||||
<a href="#configuring-specific-analyzers-used-by-dependency-scanning"><code>DS_JAVA_VERSION</code></a> variable.
|
||||
</p>
|
||||
<p>You can view the
|
||||
<a href="https://docs.gradle.org/current/userguide/compatibility.html#java">Gradle Java compatibility matrix</a> to see which version
|
||||
of Gradle is selected for each Java version. Note that we only support switching to one of these pre-installed Gradle versions
|
||||
for Java versions 13 to 17.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
If your project <i>does use</i> a <code>gradlew</code> file, then the version of Gradle pre-installed in the analyzer image is
|
||||
ignored, and the version specified in your <code>gradlew</code> file is used instead.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a id="exported-dependency-information-notes-3"></a>
|
||||
<p>
|
||||
These tests confirms that if a <code>gradlew</code> file does not exist, the version of <code>Gradle</code> pre-installed in the analyzer image is used.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<a id="exported-dependency-information-notes-4"></a>
|
||||
<p>
|
||||
This test confirms that if a <code>Pipfile.lock</code> file is found, it will be used by <a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a> to scan the exact package versions listed in this file.
|
||||
</p>
|
||||
|
|
@ -563,7 +597,7 @@ The following variables are used for configuring specific analyzers (used for a
|
|||
| `GEMNASIUM_DB_REF_NAME` | `gemnasium` | `master` | Branch name for remote repository database. `GEMNASIUM_DB_REMOTE_URL` is required. |
|
||||
| `DS_REMEDIATE` | `gemnasium` | `"true"` | Enable automatic remediation of vulnerable dependencies. |
|
||||
| `GEMNASIUM_LIBRARY_SCAN_ENABLED` | `gemnasium` | `"true"` | Enable detecting vulnerabilities in vendored JavaScript libraries. For now, `gemnasium` leverages [`Retire.js`](https://github.com/RetireJS/retire.js) to do this job. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/350512) in GitLab 14.8. |
|
||||
| `DS_JAVA_VERSION` | `gemnasium-maven` | `11` | Version of Java. Available versions: `8`, `11`, `13`, `14`, `15`, `16`. |
|
||||
| `DS_JAVA_VERSION` | `gemnasium-maven` | `11` | Version of Java. Available versions: `8`, `11`, `13`, `14`, `15`, `16`, `17`. |
|
||||
| `MAVEN_CLI_OPTS` | `gemnasium-maven` | `"-DskipTests --batch-mode"` | List of command line arguments that are passed to `maven` by the analyzer. See an example for [using private repositories](../index.md#using-private-maven-repositories). |
|
||||
| `GRADLE_CLI_OPTS` | `gemnasium-maven` | | List of command line arguments that are passed to `gradle` by the analyzer. |
|
||||
| `SBT_CLI_OPTS` | `gemnasium-maven` | | List of command-line arguments that the analyzer passes to `sbt`. |
|
||||
|
|
|
|||
|
|
@ -217,7 +217,11 @@ cannot change them:
|
|||
This ensures that your job uses the settings you intend and that they are not overridden by
|
||||
project-level pipelines.
|
||||
|
||||
##### Avoid parent and child pipelines
|
||||
##### Avoid parent and child pipelines in GitLab 14.7 and earlier
|
||||
|
||||
NOTE:
|
||||
This advice does not apply to GitLab 14.8 and later because [a fix](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78878) added
|
||||
compatibility for combining compliance pipelines, and parent and child pipelines.
|
||||
|
||||
Compliance pipelines start on the run of _every_ pipeline in a relevant project. This means that if a pipeline in the relevant project
|
||||
triggers a child pipeline, the compliance pipeline runs first. This can trigger the parent pipeline, instead of the child pipeline.
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ module Gitlab
|
|||
|
||||
def to_data_attributes
|
||||
{}.tap do |attrs|
|
||||
attrs[:aggregation] = aggregation_attributes if group
|
||||
attrs[:group] = group_data_attributes if group
|
||||
attrs[:value_stream] = value_stream_data_attributes.to_json if value_stream
|
||||
attrs[:created_after] = created_after.to_date.iso8601
|
||||
|
|
@ -103,6 +104,15 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def aggregation_attributes
|
||||
aggregation = ::Analytics::CycleAnalytics::Aggregation.safe_create_for_group(group)
|
||||
{
|
||||
enabled: aggregation.enabled.to_s,
|
||||
last_run_at: aggregation.last_incremental_run_at,
|
||||
next_run_at: aggregation.estimated_next_run_at
|
||||
}
|
||||
end
|
||||
|
||||
def group_data_attributes
|
||||
{
|
||||
id: group.id,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
module Gitlab
|
||||
class OmniauthInitializer
|
||||
OAUTH2_TIMEOUT_SECONDS = 10
|
||||
ConfigurationError = Class.new(StandardError)
|
||||
|
||||
def initialize(devise_config)
|
||||
@devise_config = devise_config
|
||||
|
|
@ -75,16 +76,29 @@ module Gitlab
|
|||
provider_arguments << provider[argument] if provider[argument]
|
||||
end
|
||||
|
||||
case provider['args']
|
||||
arguments = provider.fetch('args', {})
|
||||
defaults = provider_defaults(provider)
|
||||
|
||||
case arguments
|
||||
when Array
|
||||
# An Array from the configuration will be expanded.
|
||||
provider_arguments.concat provider['args']
|
||||
# An Array from the configuration will be expanded
|
||||
provider_arguments.concat arguments
|
||||
provider_arguments << defaults unless defaults.empty?
|
||||
when Hash
|
||||
defaults = provider_defaults(provider)
|
||||
hash_arguments = provider['args'].deep_symbolize_keys.deep_merge(defaults)
|
||||
hash_arguments = arguments.deep_symbolize_keys.deep_merge(defaults)
|
||||
normalized = normalize_hash_arguments(hash_arguments)
|
||||
|
||||
# A Hash from the configuration will be passed as is.
|
||||
provider_arguments << normalize_hash_arguments(hash_arguments)
|
||||
provider_arguments << normalized unless normalized.empty?
|
||||
else
|
||||
# this will prevent the application from starting in development mode.
|
||||
# we still set defaults, and let the application start in prod.
|
||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
|
||||
ConfigurationError.new("Arguments were provided for #{provider['name']}, but not as an array or a hash"),
|
||||
provider_name: provider['name'],
|
||||
arguments_type: arguments.class.name
|
||||
)
|
||||
provider_arguments << defaults unless defaults.empty?
|
||||
end
|
||||
|
||||
provider_arguments
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
def filter_by_name(name)
|
||||
within_element(:project_filter_form) do
|
||||
fill_in :name, with: name
|
||||
end
|
||||
end
|
||||
|
||||
def go_to_project(name)
|
||||
filter_by_name(name)
|
||||
|
||||
|
|
@ -40,14 +46,6 @@ module QA
|
|||
def clear_project_filter
|
||||
fill_element(:project_filter_form, "")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_by_name(name)
|
||||
within_element(:project_filter_form) do
|
||||
fill_in :name, with: name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
module Page
|
||||
module Modal
|
||||
class DeleteIssue < Base
|
||||
view 'app/assets/javascripts/issues/show/components/delete_issue_modal.vue' do
|
||||
element :confirm_delete_issue_button, required: true
|
||||
end
|
||||
|
||||
def confirm_delete_issue
|
||||
click_element :confirm_delete_issue_button
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -18,6 +18,8 @@ module QA
|
|||
view 'app/assets/javascripts/issues/show/components/header_actions.vue' do
|
||||
element :close_issue_button
|
||||
element :reopen_issue_button
|
||||
element :issue_actions_ellipsis_dropdown
|
||||
element :delete_issue_button
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/related_issues/components/add_issuable_form.vue' do
|
||||
|
|
@ -69,6 +71,20 @@ module QA
|
|||
def has_reopen_issue_button?
|
||||
has_element?(:reopen_issue_button)
|
||||
end
|
||||
|
||||
def has_delete_issue_button?
|
||||
click_element(:issue_actions_ellipsis_dropdown)
|
||||
has_element?(:delete_issue_button)
|
||||
end
|
||||
|
||||
def delete_issue
|
||||
click_element(:issue_actions_ellipsis_dropdown)
|
||||
click_element(:delete_issue_button, Page::Modal::DeleteIssue)
|
||||
|
||||
Page::Modal::DeleteIssue.perform(&:confirm_delete_issue)
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Manage' do
|
||||
describe 'Personal project permissions' do
|
||||
let!(:owner) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) }
|
||||
|
||||
let!(:owner_api_client) { Runtime::API::Client.new(:gitlab, user: owner) }
|
||||
|
||||
let!(:project) do
|
||||
Resource::Project.fabricate_via_api! do |project|
|
||||
project.api_client = owner_api_client
|
||||
project.name = 'qa-owner-personal-project'
|
||||
project.personal_namespace = owner.username
|
||||
end
|
||||
end
|
||||
|
||||
after do
|
||||
project&.remove_via_api!
|
||||
end
|
||||
|
||||
context 'when user is added as Owner' do
|
||||
let(:issue) do
|
||||
Resource::Issue.fabricate_via_api! do |issue|
|
||||
issue.api_client = owner_api_client
|
||||
issue.project = project
|
||||
issue.title = 'Test Owner deletes issue'
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
Flow::Login.sign_in(as: owner)
|
||||
end
|
||||
|
||||
it "has Owner role with Owner permissions", testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352542' do
|
||||
Page::Dashboard::Projects.perform do |projects|
|
||||
projects.filter_by_name(project.name)
|
||||
|
||||
expect(projects).to have_project_with_access_role(project.name, 'Owner')
|
||||
end
|
||||
|
||||
expect_owner_permissions_allow_delete_issue
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is added as Maintainer' do
|
||||
let(:maintainer) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_2, Runtime::Env.gitlab_qa_password_2) }
|
||||
|
||||
let(:issue) do
|
||||
Resource::Issue.fabricate_via_api! do |issue|
|
||||
issue.api_client = owner_api_client
|
||||
issue.project = project
|
||||
issue.title = 'Test Maintainer deletes issue'
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
project.add_member(maintainer, Resource::Members::AccessLevel::MAINTAINER)
|
||||
Flow::Login.sign_in(as: maintainer)
|
||||
end
|
||||
|
||||
it "has Maintainer role without Owner permissions", testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352607' do
|
||||
Page::Dashboard::Projects.perform do |projects|
|
||||
projects.filter_by_name(project.name)
|
||||
|
||||
expect(projects).to have_project_with_access_role(project.name, 'Maintainer')
|
||||
end
|
||||
|
||||
expect_maintainer_permissions_do_not_allow_delete_issue
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def expect_owner_permissions_allow_delete_issue
|
||||
expect do
|
||||
issue.visit!
|
||||
|
||||
Page::Project::Issue::Show.perform(&:delete_issue)
|
||||
|
||||
Page::Project::Issue::Index.perform do |index|
|
||||
expect(index).not_to have_issue(issue)
|
||||
end
|
||||
end.not_to raise_error
|
||||
end
|
||||
|
||||
def expect_maintainer_permissions_do_not_allow_delete_issue
|
||||
expect do
|
||||
issue.visit!
|
||||
|
||||
Page::Project::Issue::Show.perform do |issue|
|
||||
expect(issue).not_to have_delete_issue_button
|
||||
end
|
||||
end.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -18,6 +18,9 @@ require_relative '../config/settings'
|
|||
require_relative 'support/rspec'
|
||||
require 'active_support/all'
|
||||
|
||||
require_relative 'simplecov_env'
|
||||
SimpleCovEnv.start!
|
||||
|
||||
unless ActiveSupport::Dependencies.autoload_paths.frozen?
|
||||
ActiveSupport::Dependencies.autoload_paths << 'lib'
|
||||
ActiveSupport::Dependencies.autoload_paths << 'ee/lib'
|
||||
|
|
|
|||
|
|
@ -72,6 +72,10 @@ RSpec.describe 'Global search' do
|
|||
# TODO: Remove this along with feature flag #339348
|
||||
stub_feature_flags(new_header_search: true)
|
||||
visit dashboard_projects_path
|
||||
|
||||
# intialize javascript loaded input search input field
|
||||
find('#search').click
|
||||
find('body').click
|
||||
end
|
||||
|
||||
it 'renders updated search bar' do
|
||||
|
|
|
|||
|
|
@ -77,6 +77,12 @@ describe('AdminUserActions component', () => {
|
|||
expect(findActionsDropdown().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the tooltip', () => {
|
||||
const tooltip = getBinding(findActionsDropdown().element, 'gl-tooltip');
|
||||
|
||||
expect(tooltip.value).toBe(I18N_USER_ACTIONS.userAdministration);
|
||||
});
|
||||
|
||||
describe('when there are actions that require confirmation', () => {
|
||||
beforeEach(() => {
|
||||
initComponent({ actions: CONFIRMATION_ACTIONS });
|
||||
|
|
@ -152,7 +158,7 @@ describe('AdminUserActions component', () => {
|
|||
|
||||
describe('when `showButtonLabels` prop is `false`', () => {
|
||||
beforeEach(() => {
|
||||
initComponent({ actions: [EDIT, ...CONFIRMATION_ACTIONS] });
|
||||
initComponent({ actions: [EDIT] });
|
||||
});
|
||||
|
||||
it('does not render "Edit" button label', () => {
|
||||
|
|
@ -163,16 +169,11 @@ describe('AdminUserActions component', () => {
|
|||
expect(tooltip).toBeDefined();
|
||||
expect(tooltip.value).toBe(I18N_USER_ACTIONS.edit);
|
||||
});
|
||||
|
||||
it('does not render "User administration" dropdown button label', () => {
|
||||
expect(findActionsDropdown().props('text')).toBe(I18N_USER_ACTIONS.userAdministration);
|
||||
expect(findActionsDropdown().props('textSrOnly')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `showButtonLabels` prop is `true`', () => {
|
||||
beforeEach(() => {
|
||||
initComponent({ actions: [EDIT, ...CONFIRMATION_ACTIONS], showButtonLabels: true });
|
||||
initComponent({ actions: [EDIT], showButtonLabels: true });
|
||||
});
|
||||
|
||||
it('renders "Edit" button label', () => {
|
||||
|
|
@ -181,10 +182,5 @@ describe('AdminUserActions component', () => {
|
|||
expect(findEditButton().text()).toBe(I18N_USER_ACTIONS.edit);
|
||||
expect(tooltip).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('renders "User administration" dropdown button label', () => {
|
||||
expect(findActionsDropdown().props('text')).toBe(I18N_USER_ACTIONS.userAdministration);
|
||||
expect(findActionsDropdown().props('textSrOnly')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,10 +14,13 @@ describe('ClustersActionsComponent', () => {
|
|||
newClusterPath,
|
||||
addClusterPath,
|
||||
canAddCluster: true,
|
||||
displayClusterAgents: true,
|
||||
};
|
||||
|
||||
const findDropdown = () => wrapper.findComponent(GlDropdown);
|
||||
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
|
||||
const findDropdownItemIds = () =>
|
||||
findDropdownItems().wrappers.map((x) => x.attributes('data-testid'));
|
||||
const findNewClusterLink = () => wrapper.findByTestId('new-cluster-link');
|
||||
const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link');
|
||||
const findConnectNewAgentLink = () => wrapper.findByTestId('connect-new-agent-link');
|
||||
|
|
@ -47,26 +50,11 @@ describe('ClustersActionsComponent', () => {
|
|||
expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.actionsButton);
|
||||
});
|
||||
|
||||
it('renders a dropdown with 3 actions items', () => {
|
||||
expect(findDropdownItems()).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('renders correct href attributes for the links', () => {
|
||||
expect(findNewClusterLink().attributes('href')).toBe(newClusterPath);
|
||||
expect(findConnectClusterLink().attributes('href')).toBe(addClusterPath);
|
||||
});
|
||||
|
||||
it('renders correct modal id for the agent link', () => {
|
||||
const binding = getBinding(findConnectNewAgentLink().element, 'gl-modal-directive');
|
||||
|
||||
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
|
||||
});
|
||||
|
||||
it('shows tooltip', () => {
|
||||
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
|
||||
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectWithAgent);
|
||||
});
|
||||
|
||||
describe('when user cannot add clusters', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({ canAddCluster: false });
|
||||
|
|
@ -80,5 +68,67 @@ describe('ClustersActionsComponent', () => {
|
|||
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
|
||||
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.dropdownDisabledHint);
|
||||
});
|
||||
|
||||
it('does not bind split dropdown button', () => {
|
||||
const binding = getBinding(findDropdown().element, 'gl-modal-directive');
|
||||
|
||||
expect(binding.value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when on project level', () => {
|
||||
it('renders a dropdown with 3 actions items', () => {
|
||||
expect(findDropdownItemIds()).toEqual([
|
||||
'connect-new-agent-link',
|
||||
'new-cluster-link',
|
||||
'connect-cluster-link',
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders correct modal id for the agent link', () => {
|
||||
const binding = getBinding(findConnectNewAgentLink().element, 'gl-modal-directive');
|
||||
|
||||
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
|
||||
});
|
||||
|
||||
it('shows tooltip', () => {
|
||||
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
|
||||
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectWithAgent);
|
||||
});
|
||||
|
||||
it('shows split button in dropdown', () => {
|
||||
expect(findDropdown().props('split')).toBe(true);
|
||||
});
|
||||
|
||||
it('binds split button with modal id', () => {
|
||||
const binding = getBinding(findDropdown().element, 'gl-modal-directive');
|
||||
|
||||
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when on group or admin level', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({ displayClusterAgents: false });
|
||||
});
|
||||
|
||||
it('renders a dropdown with 2 actions items', () => {
|
||||
expect(findDropdownItemIds()).toEqual(['new-cluster-link', 'connect-cluster-link']);
|
||||
});
|
||||
|
||||
it('shows tooltip', () => {
|
||||
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
|
||||
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectExistingCluster);
|
||||
});
|
||||
|
||||
it('does not show split button in dropdown', () => {
|
||||
expect(findDropdown().props('split')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not bind dropdown button to modal', () => {
|
||||
const binding = getBinding(findDropdown().element, 'gl-modal-directive');
|
||||
|
||||
expect(binding.value).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
AGENT,
|
||||
CERTIFICATE_BASED,
|
||||
CLUSTERS_TABS,
|
||||
CERTIFICATE_TAB,
|
||||
MAX_CLUSTERS_LIST,
|
||||
MAX_LIST_COUNT,
|
||||
EVENT_LABEL_TABS,
|
||||
|
|
@ -23,12 +24,12 @@ describe('ClustersMainViewComponent', () => {
|
|||
defaultBranchName,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const createWrapper = ({ displayClusterAgents }) => {
|
||||
wrapper = shallowMountExtended(ClustersMainView, {
|
||||
propsData,
|
||||
provide: { displayClusterAgents },
|
||||
});
|
||||
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
|
@ -40,66 +41,90 @@ describe('ClustersMainViewComponent', () => {
|
|||
const findComponent = () => wrapper.findByTestId('clusters-tab-component');
|
||||
const findModal = () => wrapper.findComponent(InstallAgentModal);
|
||||
|
||||
it('renders `GlTabs` with `syncActiveTabWithQueryParams` and `queryParamName` props set', () => {
|
||||
expect(findTabs().exists()).toBe(true);
|
||||
expect(findTabs().props('syncActiveTabWithQueryParams')).toBe(true);
|
||||
});
|
||||
describe('when on project level', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({ displayClusterAgents: true });
|
||||
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
});
|
||||
|
||||
it('renders correct number of tabs', () => {
|
||||
expect(findAllTabs()).toHaveLength(CLUSTERS_TABS.length);
|
||||
});
|
||||
it('renders `GlTabs` with `syncActiveTabWithQueryParams` and `queryParamName` props set', () => {
|
||||
expect(findTabs().exists()).toBe(true);
|
||||
expect(findTabs().props('syncActiveTabWithQueryParams')).toBe(true);
|
||||
});
|
||||
|
||||
describe('tabs', () => {
|
||||
it.each`
|
||||
tabTitle | queryParamValue | lineNumber
|
||||
${'All'} | ${'all'} | ${0}
|
||||
${'Agent'} | ${AGENT} | ${1}
|
||||
${'Certificate'} | ${CERTIFICATE_BASED} | ${2}
|
||||
it('renders correct number of tabs', () => {
|
||||
expect(findAllTabs()).toHaveLength(CLUSTERS_TABS.length);
|
||||
});
|
||||
|
||||
describe('tabs', () => {
|
||||
it.each`
|
||||
tabTitle | queryParamValue | lineNumber
|
||||
${'All'} | ${'all'} | ${0}
|
||||
${'Agent'} | ${AGENT} | ${1}
|
||||
${'Certificate'} | ${CERTIFICATE_BASED} | ${2}
|
||||
`(
|
||||
'renders correct tab title and query param value',
|
||||
({ tabTitle, queryParamValue, lineNumber }) => {
|
||||
expect(findGlTabAtIndex(lineNumber).attributes('title')).toBe(tabTitle);
|
||||
expect(findGlTabAtIndex(lineNumber).props('queryParamValue')).toBe(queryParamValue);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe.each`
|
||||
tab | tabName
|
||||
${'1'} | ${AGENT}
|
||||
${'2'} | ${CERTIFICATE_BASED}
|
||||
`(
|
||||
'renders correct tab title and query param value',
|
||||
({ tabTitle, queryParamValue, lineNumber }) => {
|
||||
expect(findGlTabAtIndex(lineNumber).attributes('title')).toBe(tabTitle);
|
||||
expect(findGlTabAtIndex(lineNumber).props('queryParamValue')).toBe(queryParamValue);
|
||||
'when the child component emits the tab change event for $tabName tab',
|
||||
({ tab, tabName }) => {
|
||||
beforeEach(() => {
|
||||
findComponent().vm.$emit('changeTab', tabName);
|
||||
});
|
||||
|
||||
it(`changes the tab value to ${tab}`, () => {
|
||||
expect(findTabs().attributes('value')).toBe(tab);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe.each`
|
||||
tab | tabName
|
||||
${'1'} | ${AGENT}
|
||||
${'2'} | ${CERTIFICATE_BASED}
|
||||
`('when the child component emits the tab change event for $tabName tab', ({ tab, tabName }) => {
|
||||
beforeEach(() => {
|
||||
findComponent().vm.$emit('changeTab', tabName);
|
||||
});
|
||||
describe.each`
|
||||
tab | tabName | maxAgents
|
||||
${1} | ${AGENT} | ${MAX_LIST_COUNT}
|
||||
${2} | ${CERTIFICATE_BASED} | ${MAX_CLUSTERS_LIST}
|
||||
`('when the active tab is $tabName', ({ tab, tabName, maxAgents }) => {
|
||||
beforeEach(() => {
|
||||
findTabs().vm.$emit('input', tab);
|
||||
});
|
||||
|
||||
it(`changes the tab value to ${tab}`, () => {
|
||||
expect(findTabs().attributes('value')).toBe(tab);
|
||||
});
|
||||
});
|
||||
it('passes child-component param to the component', () => {
|
||||
expect(findComponent().props('defaultBranchName')).toBe(defaultBranchName);
|
||||
});
|
||||
|
||||
describe.each`
|
||||
tab | tabName | maxAgents
|
||||
${1} | ${AGENT} | ${MAX_LIST_COUNT}
|
||||
${2} | ${CERTIFICATE_BASED} | ${MAX_CLUSTERS_LIST}
|
||||
`('when the active tab is $tabName', ({ tab, tabName, maxAgents }) => {
|
||||
beforeEach(() => {
|
||||
findTabs().vm.$emit('input', tab);
|
||||
});
|
||||
it(`sets max-agents param to ${maxAgents} and passes it to the modal`, () => {
|
||||
expect(findModal().props('maxAgents')).toBe(maxAgents);
|
||||
});
|
||||
|
||||
it('passes child-component param to the component', () => {
|
||||
expect(findComponent().props('defaultBranchName')).toBe(defaultBranchName);
|
||||
});
|
||||
|
||||
it(`sets max-agents param to ${maxAgents} and passes it to the modal`, () => {
|
||||
expect(findModal().props('maxAgents')).toBe(maxAgents);
|
||||
});
|
||||
|
||||
it(`sends the correct tracking event with the property '${tabName}'`, () => {
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTIONS_CHANGE, {
|
||||
label: EVENT_LABEL_TABS,
|
||||
property: tabName,
|
||||
it(`sends the correct tracking event with the property '${tabName}'`, () => {
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTIONS_CHANGE, {
|
||||
label: EVENT_LABEL_TABS,
|
||||
property: tabName,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when on group or admin level', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({ displayClusterAgents: false });
|
||||
});
|
||||
|
||||
it('renders correct number of tabs', () => {
|
||||
expect(findAllTabs()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders correct tab title', () => {
|
||||
expect(findGlTabAtIndex(0).attributes('title')).toBe(CERTIFICATE_TAB.title);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -92,6 +92,10 @@ RSpec.describe ClustersHelper do
|
|||
expect(subject[:new_cluster_path]).to eq("#{project_path(project)}/-/clusters/new?tab=create")
|
||||
end
|
||||
|
||||
it 'displays add cluster using certificate path' do
|
||||
expect(subject[:add_cluster_path]).to eq("#{project_path(project)}/-/clusters/new?tab=add")
|
||||
end
|
||||
|
||||
context 'user has no permissions to create a cluster' do
|
||||
it 'displays that user can\'t add cluster' do
|
||||
expect(subject[:can_add_cluster]).to eq("false")
|
||||
|
|
@ -114,6 +118,10 @@ RSpec.describe ClustersHelper do
|
|||
it 'doesn\'t display empty state help text' do
|
||||
expect(subject[:empty_state_help_text]).to be_nil
|
||||
end
|
||||
|
||||
it 'displays display_cluster_agents as true' do
|
||||
expect(subject[:display_cluster_agents]).to eq("true")
|
||||
end
|
||||
end
|
||||
|
||||
context 'group cluster' do
|
||||
|
|
@ -123,6 +131,10 @@ RSpec.describe ClustersHelper do
|
|||
it 'displays empty state help text' do
|
||||
expect(subject[:empty_state_help_text]).to eq(s_('ClusterIntegration|Adding an integration to your group will share the cluster across all your projects.'))
|
||||
end
|
||||
|
||||
it 'displays display_cluster_agents as false' do
|
||||
expect(subject[:display_cluster_agents]).to eq("false")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -145,10 +157,6 @@ RSpec.describe ClustersHelper do
|
|||
expect(subject[:project_path]).to eq(project.full_path)
|
||||
end
|
||||
|
||||
it 'displays add cluster using certificate path' do
|
||||
expect(subject[:add_cluster_path]).to eq("#{project_path(project)}/-/clusters/new?tab=add")
|
||||
end
|
||||
|
||||
it 'displays kas address' do
|
||||
expect(subject[:kas_address]).to eq(Gitlab::Kas.external_url)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,161 @@ require 'spec_helper'
|
|||
RSpec.describe Gitlab::OmniauthInitializer do
|
||||
let(:devise_config) { class_double(Devise) }
|
||||
|
||||
subject { described_class.new(devise_config) }
|
||||
subject(:initializer) { described_class.new(devise_config) }
|
||||
|
||||
describe '.arguments_for' do
|
||||
let(:devise_config) { nil }
|
||||
|
||||
let(:arguments) { initializer.send(:arguments_for, provider) }
|
||||
|
||||
context 'when there are no args at all' do
|
||||
let(:provider) { { 'name' => 'unknown' } }
|
||||
|
||||
it 'returns an empty array' do
|
||||
expect(arguments).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is an app_id and an app_secret' do
|
||||
let(:provider) { { 'name' => 'unknown', 'app_id' => 1, 'app_secret' => 2 } }
|
||||
|
||||
it 'includes both of them, in positional order' do
|
||||
expect(arguments).to eq [1, 2]
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is an app_id and an app_secret, and an array of args' do
|
||||
let(:provider) do
|
||||
{
|
||||
'name' => 'unknown',
|
||||
'app_id' => 1,
|
||||
'app_secret' => 2,
|
||||
'args' => %w[one two three]
|
||||
}
|
||||
end
|
||||
|
||||
it 'concatenates the args on the end' do
|
||||
expect(arguments).to eq [1, 2, 'one', 'two', 'three']
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is an app_id and an app_secret, and an array of args, and default values' do
|
||||
let(:provider) do
|
||||
{
|
||||
'name' => 'unknown',
|
||||
'app_id' => 1,
|
||||
'app_secret' => 2,
|
||||
'args' => %w[one two three]
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
expect(described_class)
|
||||
.to receive(:default_arguments_for).with('unknown')
|
||||
.and_return({ default_arg: :some_value })
|
||||
end
|
||||
|
||||
it 'concatenates the args on the end' do
|
||||
expect(arguments)
|
||||
.to eq [1, 2, 'one', 'two', 'three', { default_arg: :some_value }]
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is an app_id and an app_secret, and a hash of args' do
|
||||
let(:provider) do
|
||||
{
|
||||
'name' => 'unknown',
|
||||
'app_id' => 1,
|
||||
'app_secret' => 2,
|
||||
'args' => { 'foo' => 100, 'bar' => 200, 'nested' => { 'value' => 300 } }
|
||||
}
|
||||
end
|
||||
|
||||
it 'concatenates the args on the end' do
|
||||
expect(arguments)
|
||||
.to eq [1, 2, { foo: 100, bar: 200, nested: { value: 300 } }]
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is an app_id and an app_secret, and a hash of args, and default arguments' do
|
||||
let(:provider) do
|
||||
{
|
||||
'name' => 'unknown',
|
||||
'app_id' => 1,
|
||||
'app_secret' => 2,
|
||||
'args' => { 'foo' => 100, 'bar' => 200, 'nested' => { 'value' => 300 } }
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
expect(described_class)
|
||||
.to receive(:default_arguments_for).with('unknown')
|
||||
.and_return({ default_arg: :some_value })
|
||||
end
|
||||
|
||||
it 'concatenates the args on the end' do
|
||||
expect(arguments)
|
||||
.to eq [1, 2, { default_arg: :some_value, foo: 100, bar: 200, nested: { value: 300 } }]
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is an app_id and an app_secret, no args, and default values' do
|
||||
let(:provider) do
|
||||
{
|
||||
'name' => 'unknown',
|
||||
'app_id' => 1,
|
||||
'app_secret' => 2
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
expect(described_class)
|
||||
.to receive(:default_arguments_for).with('unknown')
|
||||
.and_return({ default_arg: :some_value })
|
||||
end
|
||||
|
||||
it 'concatenates the args on the end' do
|
||||
expect(arguments)
|
||||
.to eq [1, 2, { default_arg: :some_value }]
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are args, of an unsupported type' do
|
||||
let(:provider) do
|
||||
{
|
||||
'name' => 'unknown',
|
||||
'args' => 1
|
||||
}
|
||||
end
|
||||
|
||||
context 'when there are default arguments' do
|
||||
before do
|
||||
expect(described_class)
|
||||
.to receive(:default_arguments_for).with('unknown')
|
||||
.and_return({ default_arg: :some_value })
|
||||
end
|
||||
|
||||
it 'tracks a configuration error' do
|
||||
expect(::Gitlab::ErrorTracking)
|
||||
.to receive(:track_and_raise_for_dev_exception)
|
||||
.with(described_class::ConfigurationError, provider_name: 'unknown', arguments_type: 'Integer')
|
||||
|
||||
expect(arguments)
|
||||
.to eq [{ default_arg: :some_value }]
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are no default arguments' do
|
||||
it 'tracks a configuration error' do
|
||||
expect(::Gitlab::ErrorTracking)
|
||||
.to receive(:track_and_raise_for_dev_exception)
|
||||
.with(described_class::ConfigurationError, provider_name: 'unknown', arguments_type: 'Integer')
|
||||
|
||||
expect(arguments).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
it 'configures providers from array' do
|
||||
|
|
@ -105,9 +259,48 @@ RSpec.describe Gitlab::OmniauthInitializer do
|
|||
it 'configures defaults for gitlab' do
|
||||
conf = {
|
||||
'name' => 'gitlab',
|
||||
"args" => {}
|
||||
"args" => { 'client_options' => { 'site' => generate(:url) } }
|
||||
}
|
||||
|
||||
expect(devise_config).to receive(:omniauth).with(
|
||||
:gitlab,
|
||||
client_options: { site: conf.dig('args', 'client_options', 'site') },
|
||||
authorize_params: { gl_auth_type: 'login' }
|
||||
)
|
||||
|
||||
subject.execute([conf])
|
||||
end
|
||||
|
||||
it 'configures defaults for gitlab, when arguments are not provided' do
|
||||
conf = { 'name' => 'gitlab' }
|
||||
|
||||
expect(devise_config).to receive(:omniauth).with(
|
||||
:gitlab,
|
||||
authorize_params: { gl_auth_type: 'login' }
|
||||
)
|
||||
|
||||
subject.execute([conf])
|
||||
end
|
||||
|
||||
it 'configures defaults for gitlab, when array arguments are provided' do
|
||||
conf = { 'name' => 'gitlab', 'args' => ['a'] }
|
||||
|
||||
expect(devise_config).to receive(:omniauth).with(
|
||||
:gitlab,
|
||||
'a',
|
||||
authorize_params: { gl_auth_type: 'login' }
|
||||
)
|
||||
|
||||
subject.execute([conf])
|
||||
end
|
||||
|
||||
it 'tracks a configuration error if the arguments are neither a hash nor an array' do
|
||||
conf = { 'name' => 'gitlab', 'args' => 17 }
|
||||
|
||||
expect(::Gitlab::ErrorTracking)
|
||||
.to receive(:track_and_raise_for_dev_exception)
|
||||
.with(described_class::ConfigurationError, provider_name: 'gitlab', arguments_type: 'Integer')
|
||||
|
||||
expect(devise_config).to receive(:omniauth).with(
|
||||
:gitlab,
|
||||
authorize_params: { gl_auth_type: 'login' }
|
||||
|
|
|
|||
|
|
@ -25,18 +25,16 @@ RSpec.describe Analytics::CycleAnalytics::Aggregation, type: :model do
|
|||
let_it_be(:subgroup) { create(:group, parent: group) }
|
||||
|
||||
it 'creates the aggregation record' do
|
||||
described_class.safe_create_for_group(group)
|
||||
record = described_class.safe_create_for_group(group)
|
||||
|
||||
record = described_class.find_by(group_id: group)
|
||||
expect(record).to be_present
|
||||
expect(record).to be_persisted
|
||||
end
|
||||
|
||||
context 'when non top-level group is given' do
|
||||
it 'creates the aggregation record for the top-level group' do
|
||||
described_class.safe_create_for_group(subgroup)
|
||||
record = described_class.safe_create_for_group(subgroup)
|
||||
|
||||
record = described_class.find_by(group_id: group)
|
||||
expect(record).to be_present
|
||||
expect(record).to be_persisted
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -75,4 +73,56 @@ RSpec.describe Analytics::CycleAnalytics::Aggregation, type: :model do
|
|||
expect(last_two).to eq([aggregation5, aggregation2])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#estimated_next_run_at' do
|
||||
around do |example|
|
||||
freeze_time { example.run }
|
||||
end
|
||||
|
||||
context 'when aggregation was not yet executed for the given group' do
|
||||
let(:aggregation) { create(:cycle_analytics_aggregation, last_incremental_run_at: nil) }
|
||||
|
||||
it { expect(aggregation.estimated_next_run_at).to eq(nil) }
|
||||
end
|
||||
|
||||
context 'when aggregation was already run' do
|
||||
let!(:other_aggregation1) { create(:cycle_analytics_aggregation, last_incremental_run_at: 10.minutes.ago) }
|
||||
let!(:other_aggregation2) { create(:cycle_analytics_aggregation, last_incremental_run_at: 15.minutes.ago) }
|
||||
let!(:aggregation) { create(:cycle_analytics_aggregation, last_incremental_run_at: 5.minutes.ago) }
|
||||
|
||||
it 'returns the duration between the previous run timestamp and the earliest last_incremental_run_at' do
|
||||
expect(aggregation.estimated_next_run_at).to eq(10.minutes.from_now)
|
||||
end
|
||||
|
||||
context 'when the aggregation has persisted previous runtimes' do
|
||||
before do
|
||||
aggregation.update!(incremental_runtimes_in_seconds: [30, 60, 90])
|
||||
end
|
||||
|
||||
it 'adds the runtime to the estimation' do
|
||||
expect(aggregation.estimated_next_run_at).to eq((10.minutes.seconds + 60.seconds).from_now)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no records are present in the DB' do
|
||||
it 'returns nil' do
|
||||
expect(described_class.new.estimated_next_run_at).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only one aggregation record present' do
|
||||
let!(:aggregation) { create(:cycle_analytics_aggregation, last_incremental_run_at: 5.minutes.ago) }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(aggregation.estimated_next_run_at).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the aggregation is disabled' do
|
||||
it 'returns nil' do
|
||||
expect(described_class.new(enabled: false).estimated_next_run_at).to eq(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe RunnersTokenPrefixable do
|
||||
describe 'runners token prefix' do
|
||||
subject { described_class::RUNNERS_TOKEN_PREFIX }
|
||||
|
||||
it 'has the correct value' do
|
||||
expect(subject).to eq('GR1348941')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -441,7 +441,7 @@ RSpec.shared_examples 'prefixed token rotation' do
|
|||
|
||||
context 'token is not set' do
|
||||
it 'generates a new token' do
|
||||
expect(subject).to match(/^#{instance.class::RUNNERS_TOKEN_PREFIX}/)
|
||||
expect(subject).to match(/^#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}/)
|
||||
expect(instance).not_to be_persisted
|
||||
end
|
||||
end
|
||||
|
|
@ -452,26 +452,14 @@ RSpec.shared_examples 'prefixed token rotation' do
|
|||
end
|
||||
|
||||
it 'generates a new token' do
|
||||
expect(subject).to match(/^#{instance.class::RUNNERS_TOKEN_PREFIX}/)
|
||||
expect(subject).to match(/^#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}/)
|
||||
expect(instance).not_to be_persisted
|
||||
end
|
||||
|
||||
context 'feature flag is disabled' do
|
||||
before do
|
||||
flag = "#{described_class.name.downcase.pluralize}_runners_token_prefix"
|
||||
stub_feature_flags(flag => false)
|
||||
end
|
||||
|
||||
it 'leaves the token unchanged' do
|
||||
expect { subject }.not_to change(instance, :runners_token)
|
||||
expect(instance).not_to be_persisted
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'token is set and matches prefix' do
|
||||
before do
|
||||
instance.set_runners_token(instance.class::RUNNERS_TOKEN_PREFIX + '-abcdef')
|
||||
instance.set_runners_token(RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX + '-abcdef')
|
||||
end
|
||||
|
||||
it 'leaves the token unchanged' do
|
||||
|
|
@ -486,7 +474,7 @@ RSpec.shared_examples 'prefixed token rotation' do
|
|||
|
||||
context 'token is not set' do
|
||||
it 'generates a new token' do
|
||||
expect(subject).to match(/^#{instance.class::RUNNERS_TOKEN_PREFIX}/)
|
||||
expect(subject).to match(/^#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}/)
|
||||
expect(instance).to be_persisted
|
||||
end
|
||||
end
|
||||
|
|
@ -497,25 +485,14 @@ RSpec.shared_examples 'prefixed token rotation' do
|
|||
end
|
||||
|
||||
it 'generates a new token' do
|
||||
expect(subject).to match(/^#{instance.class::RUNNERS_TOKEN_PREFIX}/)
|
||||
expect(subject).to match(/^#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}/)
|
||||
expect(instance).to be_persisted
|
||||
end
|
||||
|
||||
context 'feature flag is disabled' do
|
||||
before do
|
||||
flag = "#{described_class.name.downcase.pluralize}_runners_token_prefix"
|
||||
stub_feature_flags(flag => false)
|
||||
end
|
||||
|
||||
it 'leaves the token unchanged' do
|
||||
expect { subject }.not_to change(instance, :runners_token)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'token is set and matches prefix' do
|
||||
before do
|
||||
instance.set_runners_token(instance.class::RUNNERS_TOKEN_PREFIX + '-abcdef')
|
||||
instance.set_runners_token(RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX + '-abcdef')
|
||||
instance.save!
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -3239,12 +3239,4 @@ RSpec.describe Group do
|
|||
it_behaves_like 'no effective expiration interval'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#runners_token' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
subject { group }
|
||||
|
||||
it_behaves_like 'it has a prefixable runners_token', :groups_runners_token_prefix
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -813,8 +813,8 @@ RSpec.describe Project, factory_default: :keep do
|
|||
end
|
||||
|
||||
it 'does not set an random token if one provided' do
|
||||
project = FactoryBot.create(:project, runners_token: "#{Project::RUNNERS_TOKEN_PREFIX}my-token")
|
||||
expect(project.runners_token).to eq("#{Project::RUNNERS_TOKEN_PREFIX}my-token")
|
||||
project = FactoryBot.create(:project, runners_token: "#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}my-token")
|
||||
expect(project.runners_token).to eq("#{RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX}my-token")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -8077,14 +8077,6 @@ RSpec.describe Project, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#runners_token' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
subject { project }
|
||||
|
||||
it_behaves_like 'it has a prefixable runners_token', :projects_runners_token_prefix
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def finish_job(export_job)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'fast_spec_helper'
|
||||
require_relative '../../../../rubocop/cop/database/establish_connection'
|
||||
|
||||
RSpec.describe RuboCop::Cop::Database::EstablishConnection do
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'it has a prefixable runners_token' do |feature_flag|
|
||||
context 'feature flag enabled' do
|
||||
before do
|
||||
stub_feature_flags(feature_flag => [subject])
|
||||
end
|
||||
|
||||
describe '#runners_token' do
|
||||
it 'has a runners_token_prefix' do
|
||||
expect(subject.runners_token_prefix).not_to be_empty
|
||||
end
|
||||
|
||||
it 'starts with the runners_token_prefix' do
|
||||
expect(subject.runners_token).to start_with(subject.runners_token_prefix)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'feature flag disabled' do
|
||||
before do
|
||||
stub_feature_flags(feature_flag => false)
|
||||
end
|
||||
|
||||
describe '#runners_token' do
|
||||
it 'does not have a runners_token_prefix' do
|
||||
expect(subject.runners_token_prefix).to be_empty
|
||||
end
|
||||
|
||||
it 'starts with the runners_token_prefix' do
|
||||
expect(subject.runners_token).to start_with(subject.runners_token_prefix)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'layouts/_header_search' do
|
||||
let(:project) { nil }
|
||||
let(:group) { nil }
|
||||
let(:scope) { nil }
|
||||
let(:ref) { nil }
|
||||
let(:code_search) { false }
|
||||
let(:for_snippets) { false}
|
||||
|
||||
let(:header_search_context) do
|
||||
{
|
||||
project: project,
|
||||
group: group,
|
||||
scope: scope,
|
||||
ref: ref,
|
||||
code_search: code_search,
|
||||
for_snippets: for_snippets
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
allow(view).to receive(:header_search_context).and_return(header_search_context)
|
||||
end
|
||||
|
||||
shared_examples 'hidden fields are properly set' do
|
||||
context 'when search_context has a scope value' do
|
||||
let(:scope) { 'issues' }
|
||||
|
||||
it 'sets scope input to issues' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_css("input[name='scope'][value='#{scope}']", count: 1, visible: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when search_context has a code_search value' do
|
||||
let(:code_search) { true }
|
||||
|
||||
it 'sets search_code input to true' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_css("input[name='search_code'][value='#{code_search}']", count: 1, visible: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when search_context has a ref value' do
|
||||
let(:ref) { 'test-branch' }
|
||||
|
||||
it 'sets repository_ref input to test-branch' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_css("input[name='repository_ref'][value='#{ref}']", count: 1, visible: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when search_context has a for_snippets value' do
|
||||
let(:for_snippets) { true }
|
||||
|
||||
it 'sets for_snippets input to true' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_css("input[name='snippets'][value='#{for_snippets}']", count: 1, visible: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'nav_source' do
|
||||
it 'always set to navbar' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_css("input[name='nav_source'][value='navbar']", count: 1, visible: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'submit button' do
|
||||
it 'always renders for specs' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_css('noscript button', text: 'Search')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when doing a project level search' do
|
||||
let(:project) do
|
||||
{ id: 123, name: 'foo' }
|
||||
end
|
||||
|
||||
it 'sets project_id field' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_css("input[name='project_id'][value='#{project[:id]}']", count: 1, visible: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'hidden fields are properly set'
|
||||
end
|
||||
|
||||
context 'when doing a group level search' do
|
||||
let(:group) do
|
||||
{ id: 123, name: 'bar' }
|
||||
end
|
||||
|
||||
it 'sets group_id field' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_css("input[name='group_id'][value='#{group[:id]}']", count: 1, visible: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'hidden fields are properly set'
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue