Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-12-02 21:10:16 +00:00
parent d1ade10ba6
commit 515f39456f
69 changed files with 735 additions and 438 deletions

View File

@ -6,8 +6,7 @@ import { INSTALL_AGENT_MODAL_ID, I18N_AGENTS_EMPTY_STATE } from '../constants';
export default {
i18n: I18N_AGENTS_EMPTY_STATE,
modalId: INSTALL_AGENT_MODAL_ID,
multipleClustersDocsUrl: helpPagePath('user/project/clusters/multiple_kubernetes_clusters'),
installDocsUrl: helpPagePath('administration/clusters/kas'),
agentDocsUrl: helpPagePath('user/clusters/agent/index'),
components: {
GlButton,
GlEmptyState,
@ -32,38 +31,24 @@ export default {
<gl-empty-state :svg-path="emptyStateImage" title="" class="agents-empty-state">
<template #description>
<p class="gl-text-left">
{{ $options.i18n.introText }}
</p>
<p class="gl-text-left">
<gl-sprintf :message="$options.i18n.multipleClustersText">
<gl-sprintf :message="$options.i18n.introText">
<template #link="{ content }">
<gl-link
:href="$options.multipleClustersDocsUrl"
target="_blank"
data-testid="multiple-clusters-docs-link"
>
<gl-link :href="$options.agentDocsUrl">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
<p>
<gl-link :href="$options.installDocsUrl" target="_blank" data-testid="install-docs-link">
{{ $options.i18n.learnMoreText }}
</gl-link>
</p>
</template>
<template #actions>
<gl-button
v-if="!isChildComponent"
v-gl-modal-directive="$options.modalId"
data-testid="integration-primary-button"
category="primary"
variant="confirm"
>
{{ $options.i18n.primaryButtonText }}
{{ $options.i18n.buttonText }}
</gl-button>
</template>
</gl-empty-state>

View File

@ -1,5 +1,5 @@
<script>
import { GlEmptyState, GlButton, GlLink, GlSprintf } from '@gitlab/ui';
import { GlEmptyState, GlButton, GlLink, GlSprintf, GlAlert } from '@gitlab/ui';
import { mapState } from 'vuex';
import { helpPagePath } from '~/helpers/help_page_helper';
import { I18N_CLUSTERS_EMPTY_STATE } from '../constants';
@ -11,6 +11,7 @@ export default {
GlButton,
GlLink,
GlSprintf,
GlAlert,
},
inject: ['emptyStateHelpText', 'clustersEmptyStateImage', 'newClusterPath'],
props: {
@ -20,8 +21,11 @@ export default {
type: Boolean,
},
},
learnMoreHelpUrl: helpPagePath('user/project/clusters/index'),
multipleClustersHelpUrl: helpPagePath('user/project/clusters/multiple_kubernetes_clusters'),
clustersHelpUrl: helpPagePath('user/infrastructure/clusters/index', {
anchor: 'certificate-based-kubernetes-integration-deprecated',
}),
blogPostUrl:
'https://about.gitlab.com/blog/2021/11/15/deprecating-the-cert-based-kubernetes-integration/',
computed: {
...mapState(['canAddCluster']),
},
@ -29,48 +33,45 @@ export default {
</script>
<template>
<gl-empty-state :svg-path="clustersEmptyStateImage" title="">
<template #description>
<p class="gl-text-left">
{{ $options.i18n.description }}
</p>
<p class="gl-text-left">
<gl-sprintf :message="$options.i18n.multipleClustersText">
<template #link="{ content }">
<gl-link
:href="$options.multipleClustersHelpUrl"
target="_blank"
data-testid="multiple-clusters-docs-link"
>
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
<div>
<gl-empty-state :svg-path="clustersEmptyStateImage" title="">
<template #description>
<p class="gl-text-left">
<gl-sprintf :message="$options.i18n.introText">
<template #link="{ content }">
<gl-link :href="$options.clustersHelpUrl">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<p v-if="emptyStateHelpText" data-testid="clusters-empty-state-text">
{{ emptyStateHelpText }}
</p>
<p v-if="emptyStateHelpText" data-testid="clusters-empty-state-text">
{{ emptyStateHelpText }}
</p>
</template>
<p>
<gl-link :href="$options.learnMoreHelpUrl" target="_blank" data-testid="clusters-docs-link">
{{ $options.i18n.learnMoreLinkText }}
</gl-link>
</p>
</template>
<template #actions>
<gl-button
v-if="!isChildComponent"
data-testid="integration-primary-button"
data-qa-selector="add_kubernetes_cluster_link"
category="primary"
variant="confirm"
:disabled="!canAddCluster"
:href="newClusterPath"
>
{{ $options.i18n.buttonText }}
</gl-button>
</template>
</gl-empty-state>
<template #actions>
<gl-button
v-if="!isChildComponent"
data-testid="integration-primary-button"
data-qa-selector="add_kubernetes_cluster_link"
category="primary"
variant="confirm"
:disabled="!canAddCluster"
:href="newClusterPath"
>
{{ $options.i18n.buttonText }}
</gl-button>
</template>
</gl-empty-state>
<gl-alert variant="warning" :dismissible="false">
<gl-sprintf :message="$options.i18n.alertText">
<template #link="{ content }">
<gl-link :href="$options.blogPostUrl" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</gl-alert>
</div>
</template>

View File

@ -34,10 +34,12 @@ export default {
directives: {
GlModalDirective,
},
AGENT_CARD_INFO,
CERTIFICATE_BASED_CARD_INFO,
MAX_CLUSTERS_LIST,
INSTALL_AGENT_MODAL_ID,
i18n: {
agent: AGENT_CARD_INFO,
certificate: CERTIFICATE_BASED_CARD_INFO,
},
inject: ['addClusterPath'],
props: {
defaultBranchName: {
@ -122,21 +124,21 @@ export default {
</gl-sprintf>
</h3>
<gl-badge id="clusters-recommended-badge" size="md" variant="info">{{
$options.AGENT_CARD_INFO.tooltip.label
<gl-badge id="clusters-recommended-badge" variant="info">{{
$options.i18n.agent.tooltip.label
}}</gl-badge>
<gl-popover
target="clusters-recommended-badge"
container="viewport"
placement="bottom"
:title="$options.AGENT_CARD_INFO.tooltip.title"
:title="$options.i18n.agent.tooltip.title"
>
<p class="gl-mb-0">
<gl-sprintf :message="$options.AGENT_CARD_INFO.tooltip.text">
<gl-sprintf :message="$options.i18n.agent.tooltip.text">
<template #link="{ content }">
<gl-link
:href="$options.AGENT_CARD_INFO.tooltip.link"
:href="$options.i18n.agent.tooltip.link"
target="_blank"
class="gl-font-sm"
>
@ -159,9 +161,9 @@ export default {
<gl-link
v-if="totalAgents"
data-testid="agents-tab-footer-link"
:href="`?tab=${$options.AGENT_CARD_INFO.tabName}`"
@click="changeTab($event, $options.AGENT_CARD_INFO.tabName)"
><gl-sprintf :message="$options.AGENT_CARD_INFO.footerText"
:href="`?tab=${$options.i18n.agent.tabName}`"
@click="changeTab($event, $options.i18n.agent.tabName)"
><gl-sprintf :message="$options.i18n.agent.footerText"
><template #number>{{ cardFooterNumber(totalAgents) }}</template></gl-sprintf
></gl-link
><gl-button
@ -169,7 +171,7 @@ export default {
class="gl-ml-4"
category="secondary"
variant="confirm"
>{{ $options.AGENT_CARD_INFO.actionText }}</gl-button
>{{ $options.i18n.agent.actionText }}</gl-button
>
</template>
</gl-card>
@ -190,6 +192,7 @@ export default {
<template #total>{{ clustersCardTitle.total }}</template>
</gl-sprintf>
</h3>
<gl-badge variant="warning">{{ $options.i18n.certificate.badgeText }}</gl-badge>
</template>
<clusters :limit="$options.MAX_CLUSTERS_LIST" :is-child-component="true" />
@ -198,9 +201,9 @@ export default {
<gl-link
v-if="totalClusters"
data-testid="clusters-tab-footer-link"
:href="`?tab=${$options.CERTIFICATE_BASED_CARD_INFO.tabName}`"
@click="changeTab($event, $options.CERTIFICATE_BASED_CARD_INFO.tabName)"
><gl-sprintf :message="$options.CERTIFICATE_BASED_CARD_INFO.footerText"
:href="`?tab=${$options.i18n.certificate.tabName}`"
@click="changeTab($event, $options.i18n.certificate.tabName)"
><gl-sprintf :message="$options.i18n.certificate.footerText"
><template #number>{{ cardFooterNumber(totalClusters) }}</template></gl-sprintf
></gl-link
><gl-button
@ -209,7 +212,7 @@ export default {
variant="confirm"
class="gl-ml-4"
:href="addClusterPath"
>{{ $options.CERTIFICATE_BASED_CARD_INFO.actionText }}</gl-button
>{{ $options.i18n.certificate.actionText }}</gl-button
>
</template>
</gl-card>

View File

@ -9,7 +9,7 @@ import {
GlSprintf,
} from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import Tracking from '~/tracking';
import { generateAgentRegistrationCommand } from '../clusters_util';
@ -38,9 +38,19 @@ export default {
EVENT_ACTIONS_OPEN,
EVENT_ACTIONS_CLICK,
EVENT_LABEL_MODAL,
basicInstallPath: helpPagePath('user/clusters/agent/install/index', {
anchor: 'install-the-agent-into-the-cluster',
}),
advancedInstallPath: helpPagePath('user/clusters/agent/install/index', {
anchor: 'advanced-installation',
}),
enableKasPath: helpPagePath('administration/clusters/kas'),
installAgentPath: helpPagePath('user/clusters/agent/install/index'),
registerAgentPath: helpPagePath('user/clusters/agent/install/index', {
anchor: 'create-an-agent-record-in-gitlab',
}),
components: {
AvailableAgentsDropdown,
ClipboardButton,
CodeBlock,
GlAlert,
GlButton,
@ -49,6 +59,7 @@ export default {
GlLink,
GlModal,
GlSprintf,
ModalCopyButton,
},
mixins: [trackingMixin],
inject: ['projectPath', 'kasAddress', 'emptyStateImage'],
@ -103,17 +114,6 @@ export default {
agentRegistrationCommand() {
return generateAgentRegistrationCommand(this.agentToken, this.kasAddress);
},
basicInstallPath() {
return helpPagePath('user/clusters/agent/install/index', {
anchor: 'install-the-agent-into-the-cluster',
});
},
advancedInstallPath() {
return helpPagePath('user/clusters/agent/install/index', { anchor: 'advanced-installation' });
},
enableKasPath() {
return helpPagePath('administration/clusters/kas');
},
getAgentsQueryVariables() {
return {
defaultBranchName: this.defaultBranchName,
@ -122,11 +122,6 @@ export default {
projectPath: this.projectPath,
};
},
installAgentPath() {
return helpPagePath('user/clusters/agent/index', {
anchor: 'define-a-configuration-repository',
});
},
i18n() {
return I18N_AGENT_MODAL[this.modalType];
},
@ -272,7 +267,7 @@ export default {
<p class="gl-mb-0">{{ i18n.selectAgentBody }}</p>
<p>
<gl-link :href="basicInstallPath" target="_blank"> {{ i18n.learnMoreLink }}</gl-link>
<gl-link :href="$options.registerAgentPath"> {{ i18n.learnMoreLink }}</gl-link>
</p>
<form>
@ -301,7 +296,7 @@ export default {
<p>
<gl-sprintf :message="i18n.tokenBody">
<template #link="{ content }">
<gl-link :href="basicInstallPath" target="_blank"> {{ content }}</gl-link>
<gl-link :href="$options.basicInstallPath" target="_blank"> {{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
@ -315,7 +310,11 @@ export default {
<p>
<gl-form-input-group readonly :value="agentToken" :select-on-click="true">
<template #append>
<clipboard-button :text="agentToken" :title="i18n.copyToken" />
<modal-copy-button
:text="agentToken"
:title="i18n.copyToken"
:modal-id="$options.modalId"
/>
</template>
</gl-form-input-group>
</p>
@ -339,7 +338,7 @@ export default {
<p>
<gl-sprintf :message="i18n.advancedInstallBody">
<template #link="{ content }">
<gl-link :href="advancedInstallPath" target="_blank"> {{ content }}</gl-link>
<gl-link :href="$options.advancedInstallPath" target="_blank"> {{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
@ -350,33 +349,25 @@ export default {
<div class="gl-text-center gl-mb-5">
<img :alt="i18n.altText" :src="emptyStateImage" height="100" />
</div>
<p>{{ i18n.modalBody }}</p>
<p v-if="kasDisabled">
<gl-sprintf :message="i18n.enableKasText">
<p>
<gl-sprintf :message="i18n.modalBody">
<template #link="{ content }">
<gl-link :href="enableKasPath"> {{ content }}</gl-link>
<gl-link :href="$options.installAgentPath"> {{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<p class="gl-mb-0">
<gl-link :href="installAgentPath">
{{ i18n.docsLinkText }}
</gl-link>
<p v-if="kasDisabled">
<gl-sprintf :message="i18n.enableKasText">
<template #link="{ content }">
<gl-link :href="$options.enableKasPath"> {{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</template>
<template #modal-footer>
<gl-button
v-if="canCancel"
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="cancel"
@click="closeModal"
>{{ i18n.cancel }}
</gl-button>
<gl-button
v-if="registered"
variant="confirm"
@ -400,6 +391,15 @@ export default {
>{{ i18n.registerAgentButton }}
</gl-button>
<gl-button
v-if="canCancel"
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="cancel"
@click="closeModal"
>{{ i18n.cancel }}
</gl-button>
<gl-button
v-if="isEmptyStateModal"
:href="repositoryPath"

View File

@ -66,63 +66,61 @@ export const STATUSES = {
export const I18N_AGENT_MODAL = {
agent_registration: {
registerAgentButton: s__('ClusterAgents|Register Agent'),
registerAgentButton: s__('ClusterAgents|Register'),
close: __('Close'),
cancel: __('Cancel'),
modalTitle: s__('ClusterAgents|Connect with Agent'),
selectAgentTitle: s__('ClusterAgents|Select which Agent you want to install'),
modalTitle: s__('ClusterAgents|Connect a cluster through the Agent'),
selectAgentTitle: s__('ClusterAgents|Select an agent to register with GitLab'),
selectAgentBody: s__(
'ClusterAgents|Select an Agent to register with GitLab and install on your cluster.',
'ClusterAgents|Register an agent to generate a token that will be used to install the agent on your cluster in the next step.',
),
learnMoreLink: s__('ClusterAgents|Learn more about the GitLab Kubernetes Agent registration.'),
learnMoreLink: s__('ClusterAgents|How to register an agent?'),
copyToken: s__('ClusterAgents|Copy token'),
tokenTitle: s__('ClusterAgents|Registration token'),
tokenBody: s__(
`ClusterAgents|The registration token will be used to connect the Agent on your cluster to GitLab. To learn more about the registration tokens and how they are used %{linkStart}go to the documentation%{linkEnd}.`,
`ClusterAgents|The registration token will be used to connect the agent on your cluster to GitLab. %{linkStart}What are registration tokens?%{linkEnd}`,
),
tokenSingleUseWarningTitle: s__(
'ClusterAgents|The token value will not be shown again after you close this window.',
'ClusterAgents|You cannot see this token again after you close this window.',
),
tokenSingleUseWarningBody: s__(
`ClusterAgents|The recommended installation method provided below includes the token. If you want to follow the alternative installation method provided in the docs make sure you save the token value before you close the window.`,
`ClusterAgents|The recommended installation method includes the token. If you want to follow the advanced installation method provided in the docs, make sure you save the token value before you close this window.`,
),
basicInstallTitle: s__('ClusterAgents|Recommended installation method'),
basicInstallBody: __(
`Open a CLI and connect to the cluster you want to install the Agent in. Use this installation method to minimize any manual steps. The token is already included in the command.`,
`Open a CLI and connect to the cluster you want to install the agent in. Use this installation method to minimize any manual steps. The token is already included in the command.`,
),
advancedInstallTitle: s__('ClusterAgents|Alternative installation methods'),
advancedInstallTitle: s__('ClusterAgents|Advanced installation methods'),
advancedInstallBody: s__(
'ClusterAgents|For alternative installation methods %{linkStart}go to the documentation%{linkEnd}.',
'ClusterAgents|For the advanced installation method %{linkStart}see the documentation%{linkEnd}.',
),
registrationErrorTitle: __('Failed to register Agent'),
registrationErrorTitle: s__('ClusterAgents|Failed to register an agent'),
unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
},
empty_state: {
modalTitle: s__('ClusterAgents|Install new Agent'),
modalTitle: s__('ClusterAgents|Connect your cluster through the Agent'),
modalBody: s__(
'ClusterAgents|To install an Agent you should create an agent directory in the Repository first. We recommend that you add the Agent configuration to the directory before you start the installation process.',
"ClusterAgents|To install a new agent, first add the agent's configuration file to this repository. %{linkStart}What's the agent's configuration file?%{linkEnd}",
),
docsLinkText: s__('ClusterAgents|Learn more about installing a GitLab Kubernetes Agent'),
enableKasText: s__(
'ClusterAgents|The GitLab Agent also requires %{linkStart}enabling the Agent Server%{linkEnd}',
"ClusterAgents|Your instance doesn't have the %{linkStart}GitLab Agent Server (KAS)%{linkEnd} set up. Ask a GitLab Administrator to install it.",
),
altText: s__('ClusterAgents|GitLab Kubernetes Agent'),
secondaryButton: s__('ClusterAgents|Go to the repository'),
done: __('Done'),
altText: s__('ClusterAgents|GitLab Agent for Kubernetes'),
secondaryButton: s__('ClusterAgents|Go to the repository files'),
done: __('Cancel'),
},
};
export const KAS_DISABLED_ERROR = 'Gitlab::Kas::Client::ConfigurationError';
export const I18N_AVAILABLE_AGENTS_DROPDOWN = {
selectAgent: s__('ClusterAgents|Select an Agent'),
selectAgent: s__('ClusterAgents|Select an agent'),
registeringAgent: s__('ClusterAgents|Registering Agent'),
};
@ -143,7 +141,7 @@ export const AGENT_STATUSES = {
title: s__('ClusterAgents|Agent might not be connected to GitLab'),
body: sprintf(
s__(
'ClusterAgents|The Agent has not been connected in a long time. There might be a connectivity issue. Last contact was %{timeAgo}.',
'ClusterAgents|The agent has not been connected in a long time. There might be a connectivity issue. Last contact was %{timeAgo}.',
),
),
},
@ -161,50 +159,48 @@ export const AGENT_STATUSES = {
export const I18N_AGENTS_EMPTY_STATE = {
introText: s__(
'ClusterAgents|Use GitLab Agents to more securely integrate with your clusters to deploy your applications, run your pipelines, use review apps and much more.',
'ClusterIntegration|Use the %{linkStart}GitLab Agent%{linkEnd} to safely connect your Kubernetes clusters to GitLab. You can deploy your applications, run your pipelines, use Review Apps, and much more.',
),
multipleClustersText: s__(
'ClusterAgents|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}',
),
learnMoreText: s__('ClusterAgents|Learn more about the GitLab Kubernetes Agent.'),
primaryButtonText: s__('ClusterAgents|Connect with a GitLab Agent'),
buttonText: s__('ClusterAgents|Connect with the GitLab Agent'),
};
export const I18N_CLUSTERS_EMPTY_STATE = {
description: s__(
'ClusterIntegration|Use certificates to integrate with your clusters to deploy your applications, run your pipelines, use review apps and much more in an easy way.',
introText: s__(
'ClusterIntegration|Connect your cluster to GitLab through %{linkStart}cluster certificates%{linkEnd}.',
),
multipleClustersText: s__(
'ClusterIntegration|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}',
),
learnMoreLinkText: s__('ClusterIntegration|Learn more about the GitLab managed clusters'),
buttonText: s__('ClusterIntegration|Connect with a certificate'),
alertText: s__(
'ClusterIntegration|The certificate-based method to connect clusters to GitLab was %{linkStart}deprecated%{linkEnd} in GitLab 14.5.',
),
};
export const AGENT_CARD_INFO = {
tabName: 'agent',
title: sprintf(s__('ClusterAgents|%{number} of %{total} Agent based integrations')),
emptyTitle: s__('ClusterAgents|No Agent based integrations'),
title: sprintf(s__('ClusterAgents|%{number} of %{total} agents')),
emptyTitle: s__('ClusterAgents|No agents'),
tooltip: {
label: s__('ClusterAgents|Recommended'),
title: s__('ClusterAgents|GitLab Agents'),
title: s__('ClusterAgents|GitLab Agent'),
text: sprintf(
s__(
'ClusterAgents|GitLab Agents provide an increased level of security when integrating with clusters. %{linkStart}Learn more about the GitLab Kubernetes Agent.%{linkEnd}',
'ClusterAgents|The GitLab Agent provides an increased level of security when connecting Kubernetes clusters to GitLab. %{linkStart}Learn more about the GitLab Agent.%{linkEnd}',
),
),
link: helpPagePath('user/clusters/agent/index'),
},
actionText: s__('ClusterAgents|Install new Agent'),
footerText: sprintf(s__('ClusterAgents|View all %{number} Agent based integrations')),
actionText: s__('ClusterAgents|Install a new agent'),
footerText: sprintf(s__('ClusterAgents|View all %{number} agents')),
};
export const CERTIFICATE_BASED_CARD_INFO = {
tabName: 'certificate_based',
title: sprintf(s__('ClusterAgents|%{number} of %{total} Certificate based integrations')),
emptyTitle: s__('ClusterAgents|No Certificate based integrations'),
title: sprintf(
s__('ClusterAgents|%{number} of %{total} clusters connected through cluster certificates'),
),
emptyTitle: s__('ClusterAgents|No clusters connected through cluster certificates'),
actionText: s__('ClusterAgents|Connect existing cluster'),
footerText: sprintf(s__('ClusterAgents|View all %{number} Certificate based integrations')),
footerText: sprintf(s__('ClusterAgents|View all %{number} clusters')),
badgeText: s__('ClusterAgents|Deprecated'),
};
export const MAX_CLUSTERS_LIST = 6;
@ -221,7 +217,7 @@ export const CLUSTERS_TABS = [
queryParamValue: 'agent',
},
{
title: s__('ClusterAgents|Certificate based'),
title: s__('ClusterAgents|Certificate'),
component: 'clusters',
queryParamValue: 'certificate_based',
},
@ -229,9 +225,9 @@ export const CLUSTERS_TABS = [
export const CLUSTERS_ACTIONS = {
actionsButton: s__('ClusterAgents|Actions'),
createNewCluster: s__('ClusterAgents|Create new cluster'),
connectWithAgent: s__('ClusterAgents|Connect with Agent'),
connectExistingCluster: s__('ClusterAgents|Connect with certificate'),
createNewCluster: s__('ClusterAgents|Create a new cluster'),
connectWithAgent: s__('ClusterAgents|Connect with the Agent'),
connectExistingCluster: s__('ClusterAgents|Connect with a certificate'),
};
export const AGENT = 'agent';

View File

@ -4,6 +4,7 @@ import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.
import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { query, mutate } from './gql';
const fetchApiProjectData = (projectPath) => Api.project(projectPath).then(({ data }) => data);
@ -12,7 +13,10 @@ const fetchGqlProjectData = (projectPath) =>
query({
query: getIdeProject,
variables: { projectPath },
}).then(({ data }) => data.project);
}).then(({ data }) => ({
...data.project,
id: getIdFromGraphQLId(data.project.id),
}));
export default {
getFileData(endpoint) {

View File

@ -64,6 +64,9 @@ export default {
canReadJob() {
return this.job.userPermissions?.readBuild;
},
canUpdateJob() {
return this.job.userPermissions?.updateBuild;
},
isActive() {
return this.job.active;
},
@ -139,7 +142,7 @@ export default {
<template>
<gl-button-group>
<template v-if="canReadJob">
<template v-if="canReadJob && canUpdateJob">
<gl-button
v-if="isActive"
data-testid="cancel-button"

View File

@ -75,6 +75,7 @@ query getJobs(
userPermissions {
readBuild
readJobArtifacts
updateBuild
}
}
}

View File

@ -1,6 +1,6 @@
export const DASH_SCOPE = '-';
const PATH_SEPARATOR = '/';
export const PATH_SEPARATOR = '/';
const PATH_SEPARATOR_LEADING_REGEX = new RegExp(`^${PATH_SEPARATOR}+`);
const PATH_SEPARATOR_ENDING_REGEX = new RegExp(`${PATH_SEPARATOR}+$`);
const SHA_REGEX = /[\da-f]{40}/gi;

View File

@ -8,7 +8,7 @@ import {
GlDropdownSectionHeader,
GlSearchBoxByType,
} from '@gitlab/ui';
import { joinPaths } from '~/lib/utils/url_utility';
import { joinPaths, PATH_SEPARATOR } from '~/lib/utils/url_utility';
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
@ -36,7 +36,9 @@ export default {
};
},
skip() {
return this.search.length > 0 && this.search.length < MINIMUM_SEARCH_LENGTH;
const hasNotEnoughSearchCharacters =
this.search.length > 0 && this.search.length < MINIMUM_SEARCH_LENGTH;
return this.shouldSkipQuery || hasNotEnoughSearchCharacters;
},
debounce: DEBOUNCE_DELAY,
},
@ -52,7 +54,7 @@ export default {
data() {
return {
currentUser: {},
groupToFilterBy: undefined,
groupPathToFilterBy: undefined,
search: '',
selectedNamespace: this.namespaceId
? {
@ -63,6 +65,7 @@ export default {
id: this.userNamespaceId,
fullPath: this.userNamespaceFullPath,
},
shouldSkipQuery: true,
};
},
computed: {
@ -73,10 +76,8 @@ export default {
return this.currentUser.namespace || {};
},
filteredGroups() {
return this.groupToFilterBy
? this.userGroups.filter((group) =>
group.fullPath.startsWith(this.groupToFilterBy.fullPath),
)
return this.groupPathToFilterBy
? this.userGroups.filter((group) => group.fullPath.startsWith(this.groupPathToFilterBy))
: this.userGroups;
},
hasGroupMatches() {
@ -85,7 +86,7 @@ export default {
hasNamespaceMatches() {
return (
this.userNamespace.fullPath?.toLowerCase().includes(this.search.toLowerCase()) &&
!this.groupToFilterBy
!this.groupPathToFilterBy
);
},
hasNoMatches() {
@ -99,7 +100,10 @@ export default {
eventHub.$off('select-template', this.handleSelectTemplate);
},
methods: {
focusInput() {
handleDropdownShown() {
if (this.shouldSkipQuery) {
this.shouldSkipQuery = false;
}
this.$refs.search.focusInput();
},
handleDropdownItemClick(namespace) {
@ -111,13 +115,9 @@ export default {
});
this.setNamespace(namespace);
},
handleSelectTemplate(groupId) {
this.groupToFilterBy = this.userGroups.find(
(group) => getIdFromGraphQLId(group.id) === groupId,
);
if (this.groupToFilterBy) {
this.setNamespace(this.groupToFilterBy);
}
handleSelectTemplate(id, fullPath) {
this.groupPathToFilterBy = fullPath.split(PATH_SEPARATOR).shift();
this.setNamespace({ id, fullPath });
},
setNamespace({ id, fullPath }) {
this.selectedNamespace = {
@ -137,7 +137,7 @@ export default {
toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base! gl-w-20"
data-qa-selector="select_namespace_dropdown"
@show="track('activate_form_input', { label: trackLabel, property: 'project_path' })"
@shown="focusInput"
@shown="handleDropdownShown"
>
<gl-search-box-by-type
ref="search"

View File

@ -3,6 +3,7 @@
module Users
class TermsController < ApplicationController
include InternalRedirect
include OneTrustCSP
skip_before_action :authenticate_user!, only: [:index]
skip_before_action :enforce_terms!

View File

@ -12,30 +12,18 @@ module Environments
# rubocop: disable CodeReuse/ActiveRecord
def execute
deployments = project.deployments
deployments =
if ref
deployments_query = params[:with_tags] ? 'ref = :ref OR tag IS TRUE' : 'ref = :ref'
deployments.where(deployments_query, ref: ref.to_s)
Deployment.where(deployments_query, ref: ref.to_s)
elsif commit
deployments.where(sha: commit.sha)
Deployment.where(sha: commit.sha)
else
deployments.none
Deployment.none
end
environments =
if Feature.enabled?(:environments_by_deployments_finder_exists_optimization, project, default_enabled: :yaml)
# TODO: replace unscope with deployments = Deployment on top of the method https://gitlab.com/gitlab-org/gitlab/-/issues/343544
project.environments.available
.where('EXISTS (?)', deployments.unscope(where: :project_id).where('environment_id = environments.id'))
else
environment_ids = deployments
.group(:environment_id)
.select(:environment_id)
project.environments.available
.where(id: environment_ids)
end
environments = project.environments.available
.where('EXISTS (?)', deployments.where('environment_id = environments.id'))
if params[:find_latest]
find_one(environments.order_by_last_deployed_at_desc)

View File

@ -605,7 +605,7 @@ class Issue < ApplicationRecord
def could_not_move(exception)
# Symptom of running out of space - schedule rebalancing
IssueRebalancingWorker.perform_async(nil, *project.self_or_root_group_ids)
Issues::RebalancingWorker.perform_async(nil, *project.self_or_root_group_ids)
end
end

View File

@ -25,7 +25,7 @@ class Member < ApplicationRecord
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
has_one :member_task
delegate :name, :username, :email, to: :user, prefix: true
delegate :name, :username, :email, :last_activity_on, to: :user, prefix: true
delegate :tasks_to_be_done, to: :member_task, allow_nil: true
validates :expires_at, allow_blank: true, future_date: true

View File

@ -30,7 +30,7 @@ module Issues
gates = [issue.project, issue.project.group].compact
return unless gates.any? { |gate| Feature.enabled?(:rebalance_issues, gate) }
IssueRebalancingWorker.perform_async(nil, *issue.project.self_or_root_group_ids)
Issues::RebalancingWorker.perform_async(nil, *issue.project.self_or_root_group_ids)
end
private

View File

@ -41,7 +41,7 @@ module Issues
user = current_user
issue.run_after_commit do
NewIssueWorker.perform_async(issue.id, user.id)
IssuePlacementWorker.perform_async(nil, issue.project_id)
Issues::PlacementWorker.perform_async(nil, issue.project_id)
Namespaces::OnboardingIssueCreatedWorker.perform_async(issue.namespace.id)
end
end

View File

@ -1,5 +1,9 @@
- redirect_params = { redirect: @redirect } if @redirect
- accept_term_link = accept_term_path(@term, redirect_params)
- content_for :page_specific_javascripts do
= render "layouts/google_tag_manager_head"
= render "layouts/one_trust"
= render "layouts/google_tag_manager_body"
- if Feature.enabled?(:terms_of_service_vue, current_user, default_enabled: :yaml)
#js-terms-of-service{ data: { terms_data: terms_data(@term, @redirect) } }

View File

@ -1,5 +1,8 @@
# frozen_string_literal: true
# DEPRECATED. Will be removed in 14.7 https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72803
# Please use Issues::PlacementWorker instead
#
# todo: remove this worker and it's queue definition from all_queues after Issues::PlacementWorker is deployed
# We want to keep it for one release in case some jobs are already scheduled in the old queue so we need the worker
# to be available to finish those. All new jobs will be queued into the new queue.
@ -43,10 +46,10 @@ class IssuePlacementWorker
Issue.move_nulls_to_end(to_place)
Issues::BaseService.new(project: nil).rebalance_if_needed(to_place.max_by(&:relative_position))
IssuePlacementWorker.perform_async(nil, leftover.project_id) if leftover.present?
Issues::PlacementWorker.perform_async(nil, leftover.project_id) if leftover.present?
rescue RelativePositioning::NoSpaceLeft => e
Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id, project_id: project_id)
IssueRebalancingWorker.perform_async(nil, *root_namespace_id_to_rebalance(issue, project_id))
Issues::RebalancingWorker.perform_async(nil, *root_namespace_id_to_rebalance(issue, project_id))
end
def find_issue(issue_id, project_id)

View File

@ -1,5 +1,8 @@
# frozen_string_literal: true
# DEPRECATED. Will be removed in 14.7 https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72803
# Please use Issues::RebalancingWorker instead
#
# todo: remove this worker and it's queue definition from all_queues after Issue::RebalancingWorker is released.
# We want to keep it for one release in case some jobs are already scheduled in the old queue so we need the worker
# to be available to finish those. All new jobs will be queued into the new queue.

View File

@ -17,6 +17,7 @@ module Issues
# we need to have exactly one of the project_id and root_namespace_id params be non-nil
raise ArgumentError, "Expected only one of the params project_id: #{project_id} and root_namespace_id: #{root_namespace_id}" if project_id && root_namespace_id
return if project_id.nil? && root_namespace_id.nil?
return if ::Gitlab::Issues::Rebalancing::State.rebalance_recently_finished?(project_id, root_namespace_id)
# pull the projects collection to be rebalanced either the project if namespace is not a group(i.e. user namesapce)
# or the root namespace, this also makes the worker backward compatible with previous version where a project_id was

View File

@ -20,13 +20,13 @@ module Issues
namespaces = Namespace.id_in(namespace_ids)
projects = Project.id_in(project_ids)
IssueRebalancingWorker.bulk_perform_async_with_contexts(
Issues::RebalancingWorker.bulk_perform_async_with_contexts(
namespaces,
arguments_proc: -> (namespace) { [nil, nil, namespace.id] },
context_proc: -> (namespace) { { namespace: namespace } }
)
IssueRebalancingWorker.bulk_perform_async_with_contexts(
Issues::RebalancingWorker.bulk_perform_async_with_contexts(
projects,
arguments_proc: -> (project) { [nil, project.id, nil] },
context_proc: -> (project) { { project: project } }

View File

@ -1,8 +0,0 @@
---
name: environments_by_deployments_finder_exists_optimization
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72781/
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/343544
milestone: '14.5'
type: development
group: group::release
default_enabled: false

View File

@ -1,7 +1,5 @@
# frozen_string_literal: true
return unless Gitlab.com? || Gitlab.dev_or_test_env?
def feature_flags_available?
# When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised
active_db_connection = ActiveRecord::Base.connection.active? rescue false
@ -11,6 +9,8 @@ rescue ActiveRecord::NoDatabaseError
false
end
return unless Gitlab.com? || Gitlab.dev_or_test_env?
Gitlab::Application.configure do
if feature_flags_available? && ::Feature.enabled?(:active_record_transactions_tracking, type: :ops, default_enabled: :yaml)
Gitlab::Database::Transaction::Observer.register!

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
class CreateVulnerabilityReads < Gitlab::Database::Migration[1.0]
def change
create_table :vulnerability_reads do |t|
t.bigint :vulnerability_id, null: false
t.bigint :project_id, null: false
t.bigint :scanner_id, null: false
t.integer :report_type, limit: 2, null: false
t.integer :severity, limit: 2, null: false
t.integer :state, limit: 2, null: false
t.boolean :has_issues, default: false, null: false
t.boolean :resolved_on_default_branch, default: false, null: false
t.uuid :uuid, null: false
t.text :location_image, limit: 2048
t.index :vulnerability_id, unique: true
t.index :scanner_id
t.index :uuid, unique: true
t.index [:project_id, :state, :severity, :vulnerability_id], name: :index_vuln_reads_on_project_id_state_severity_and_vuln_id, order: { vulnerability_id: :desc }
t.index :location_image, where: "report_type IN (2, 7)", name: :index_vulnerability_reads_on_location_image
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddForeignKeyToVulnerabilityReadsOnVulnerability < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_concurrent_foreign_key :vulnerability_reads, :vulnerabilities, column: :vulnerability_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :vulnerability_reads, column: :vulnerability_id
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddForeignKeyToVulnerabilityReadsOnProject < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_concurrent_foreign_key :vulnerability_reads, :projects, column: :project_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :vulnerability_reads, column: :project_id
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddForeignKeyToVulnerabilityReadsOnScanner < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
add_concurrent_foreign_key :vulnerability_reads, :vulnerability_scanners, column: :scanner_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :vulnerability_reads, column: :scanner_id
end
end
end

View File

@ -0,0 +1 @@
c7c29b136fbe00271807fcd3133baf7a6e9ded40989fc274e941fc99f2c19e4d

View File

@ -0,0 +1 @@
d9a0886d95cd54add9e63475a2f1ca0601304bb64ffe6e6d9e62cb8997d5fe40

View File

@ -0,0 +1 @@
f25ee0df287f1c44740be143831537bf262d09d7068ceca1c516ee964bc3aa24

View File

@ -0,0 +1 @@
e032fd334d175d803b943c6328048705e81bd70af6ac226a032281304840f1cd

View File

@ -20843,6 +20843,30 @@ CREATE SEQUENCE vulnerability_occurrences_id_seq
ALTER SEQUENCE vulnerability_occurrences_id_seq OWNED BY vulnerability_occurrences.id;
CREATE TABLE vulnerability_reads (
id bigint NOT NULL,
vulnerability_id bigint NOT NULL,
project_id bigint NOT NULL,
scanner_id bigint NOT NULL,
report_type smallint NOT NULL,
severity smallint NOT NULL,
state smallint NOT NULL,
has_issues boolean DEFAULT false NOT NULL,
resolved_on_default_branch boolean DEFAULT false NOT NULL,
uuid uuid NOT NULL,
location_image text,
CONSTRAINT check_380451bdbe CHECK ((char_length(location_image) <= 2048))
);
CREATE SEQUENCE vulnerability_reads_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE vulnerability_reads_id_seq OWNED BY vulnerability_reads.id;
CREATE TABLE vulnerability_remediations (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@ -22093,6 +22117,8 @@ ALTER TABLE ONLY vulnerability_occurrence_pipelines ALTER COLUMN id SET DEFAULT
ALTER TABLE ONLY vulnerability_occurrences ALTER COLUMN id SET DEFAULT nextval('vulnerability_occurrences_id_seq'::regclass);
ALTER TABLE ONLY vulnerability_reads ALTER COLUMN id SET DEFAULT nextval('vulnerability_reads_id_seq'::regclass);
ALTER TABLE ONLY vulnerability_remediations ALTER COLUMN id SET DEFAULT nextval('vulnerability_remediations_id_seq'::regclass);
ALTER TABLE ONLY vulnerability_scanners ALTER COLUMN id SET DEFAULT nextval('vulnerability_scanners_id_seq'::regclass);
@ -24118,6 +24144,9 @@ ALTER TABLE ONLY vulnerability_occurrence_pipelines
ALTER TABLE ONLY vulnerability_occurrences
ADD CONSTRAINT vulnerability_occurrences_pkey PRIMARY KEY (id);
ALTER TABLE ONLY vulnerability_reads
ADD CONSTRAINT vulnerability_reads_pkey PRIMARY KEY (id);
ALTER TABLE ONLY vulnerability_remediations
ADD CONSTRAINT vulnerability_remediations_pkey PRIMARY KEY (id);
@ -27667,6 +27696,8 @@ COMMENT ON INDEX index_verification_codes_on_phone_and_visitor_id_code IS 'JiHu-
CREATE UNIQUE INDEX index_vuln_historical_statistics_on_project_id_and_date ON vulnerability_historical_statistics USING btree (project_id, date);
CREATE INDEX index_vuln_reads_on_project_id_state_severity_and_vuln_id ON vulnerability_reads USING btree (project_id, state, severity, vulnerability_id DESC);
CREATE INDEX index_vulnerabilities_on_author_id ON vulnerabilities USING btree (author_id);
CREATE INDEX index_vulnerabilities_on_confirmed_by_id ON vulnerabilities USING btree (confirmed_by_id);
@ -27761,6 +27792,14 @@ CREATE UNIQUE INDEX index_vulnerability_occurrences_on_uuid ON vulnerability_occ
CREATE INDEX index_vulnerability_occurrences_on_vulnerability_id ON vulnerability_occurrences USING btree (vulnerability_id);
CREATE INDEX index_vulnerability_reads_on_location_image ON vulnerability_reads USING btree (location_image) WHERE (report_type = ANY (ARRAY[2, 7]));
CREATE INDEX index_vulnerability_reads_on_scanner_id ON vulnerability_reads USING btree (scanner_id);
CREATE UNIQUE INDEX index_vulnerability_reads_on_uuid ON vulnerability_reads USING btree (uuid);
CREATE UNIQUE INDEX index_vulnerability_reads_on_vulnerability_id ON vulnerability_reads USING btree (vulnerability_id);
CREATE UNIQUE INDEX index_vulnerability_remediations_on_project_id_and_checksum ON vulnerability_remediations USING btree (project_id, checksum);
CREATE UNIQUE INDEX index_vulnerability_scanners_on_project_id_and_external_id ON vulnerability_scanners USING btree (project_id, external_id);
@ -29089,6 +29128,9 @@ ALTER TABLE ONLY releases
ALTER TABLE ONLY geo_event_log
ADD CONSTRAINT fk_4a99ebfd60 FOREIGN KEY (repositories_changed_event_id) REFERENCES geo_repositories_changed_events(id) ON DELETE CASCADE;
ALTER TABLE ONLY vulnerability_reads
ADD CONSTRAINT fk_5001652292 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY alert_management_alerts
ADD CONSTRAINT fk_51ab4b6089 FOREIGN KEY (prometheus_alert_id) REFERENCES prometheus_alerts(id) ON DELETE CASCADE;
@ -29137,6 +29179,9 @@ ALTER TABLE ONLY dast_profile_schedules
ALTER TABLE ONLY events
ADD CONSTRAINT fk_61fbf6ca48 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY vulnerability_reads
ADD CONSTRAINT fk_62736f638f FOREIGN KEY (vulnerability_id) REFERENCES vulnerabilities(id) ON DELETE CASCADE;
ALTER TABLE ONLY merge_requests
ADD CONSTRAINT fk_641731faff FOREIGN KEY (updated_by_id) REFERENCES users(id) ON DELETE SET NULL;
@ -29428,6 +29473,9 @@ ALTER TABLE ONLY vulnerabilities
ALTER TABLE ONLY project_access_tokens
ADD CONSTRAINT fk_b27801bfbf FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY vulnerability_reads
ADD CONSTRAINT fk_b28c28abf1 FOREIGN KEY (scanner_id) REFERENCES vulnerability_scanners(id) ON DELETE CASCADE;
ALTER TABLE ONLY issues
ADD CONSTRAINT fk_b37be69be6 FOREIGN KEY (work_item_type_id) REFERENCES work_item_types(id);

View File

@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
At the heart of GitLab is a web application [built using the Ruby on Rails
framework](https://about.gitlab.com/blog/2018/10/29/why-we-use-rails-to-build-gitlab/).
Thanks to this, we also get access to the amazing tools built right into Rails.
In this guide, we'll introduce the [Rails console](../operations/rails_console.md#starting-a-rails-console-session)
In this guide, we are introducing the [Rails console](../operations/rails_console.md#starting-a-rails-console-session)
and the basics of interacting with your GitLab instance from the command line.
WARNING:
@ -19,7 +19,7 @@ or destroying production data. If you would like to explore the Rails console
with no consequences, you are strongly advised to do so in a test environment.
This guide is targeted at GitLab system administrators who are troubleshooting
a problem or need to retrieve some data that can only be done through direct
a problem or must retrieve some data that can only be done through direct
access of the GitLab application. Basic knowledge of Ruby is needed (try [this
30-minute tutorial](https://try.ruby-lang.org/) for a quick introduction).
Rails experience is helpful to have but not a must.
@ -29,7 +29,7 @@ Rails experience is helpful to have but not a must.
Your type of GitLab installation determines how
[to start a rails console](../operations/rails_console.md).
The following code examples will all take place inside the Rails console and also
The following code examples take place inside the Rails console and also
assume an Omnibus GitLab installation.
## Active Record objects
@ -37,7 +37,7 @@ assume an Omnibus GitLab installation.
### Looking up database-persisted objects
Under the hood, Rails uses [Active Record](https://guides.rubyonrails.org/active_record_basics.html),
an object-relational mapping system, to read, write and map application objects
an object-relational mapping system, to read, write, and map application objects
to the PostgreSQL database. These mappings are handled by Active Record models,
which are Ruby classes defined in a Rails app. For GitLab, the model classes
can be found at `/opt/gitlab/embedded/service/gitlab-rails/app/models`.
@ -144,7 +144,7 @@ NoMethodError (undefined method `username' for #<ActiveRecord::Relation [#<User
Did you mean? by_username
```
We need to retrieve the single object from the collection by using the `.first`
Let's retrieve the single object from the collection by using the `.first`
method to get the first item in the collection:
```ruby
@ -164,7 +164,7 @@ Record, please see the [Active Record Query Interface documentation](https://gui
### Modifying Active Record objects
In the previous section, we learned about retrieving database records using
Active Record. Now, we'll learn how to write changes to the database.
Active Record. Now, let's learn how to write changes to the database.
First, let's retrieve the `root` user:
@ -195,7 +195,7 @@ a background job to deliver an email notification. This is an example of an
-- code which is designated to run in response to events in the Active Record
object life cycle. This is also why using the Rails console is preferred when
direct changes to data is necessary as changes made via direct database queries
will not trigger these callbacks.
does not trigger these callbacks.
It's also possible to update attributes in a single line:
@ -265,8 +265,8 @@ user.save!(validate: false)
This is not recommended, as validations are usually put in place to ensure the
integrity and consistency of user-provided data.
A validation error will prevent the entire object from being saved to
the database. We'll see a little of this in the next section. If you're getting
A validation error prevents the entire object from being saved to
the database. You can see a little of this in the section below. If you're getting
a mysterious red banner in the GitLab UI when submitting a form, this can often
be the fastest way to get to the root of the problem.
@ -336,7 +336,7 @@ user.activate
user.state
```
Earlier, we mentioned that a validation error will prevent the entire object
Earlier, we mentioned that a validation error prevents the entire object
from being saved to the database. Let's see how this can have unexpected
interactions:
@ -455,7 +455,7 @@ Ci::Build.find(66124)
```
The pipeline and job ID numbers increment globally across your GitLab
instance, so there's no need to use an internal ID attribute to look them up,
instance, so there's no requirement to use an internal ID attribute to look them up,
unlike with issues or merge requests.
**Get the current application settings object:**

View File

@ -698,7 +698,10 @@ aware of the support.
The documentation will mention that the old Global ID style is now deprecated.
See also [Aliasing and deprecating mutations](#aliasing-and-deprecating-mutations).
See also:
- [Aliasing and deprecating mutations](#aliasing-and-deprecating-mutations).
- [How to filter Kibana for queries that used deprecated fields](graphql_guide/monitoring.md#queries-that-used-a-deprecated-field).
## Enums
@ -2027,3 +2030,7 @@ elimination of laziness, where needed.
For dealing with lazy values without forcing them, use
`Gitlab::Graphql::Lazy.with_value`.
## Monitoring GraphQL
See the [Monitoring GraphQL](graphql_guide/monitoring.md) guide for tips on how to inspect logs of GraphQL requests and monitor the performance of your GraphQL queries.

View File

@ -113,8 +113,9 @@ The more we reflexively add information to the documentation, the more
the documentation helps others efficiently accomplish tasks and solve problems.
If you have questions when considering, authoring, or editing documentation, ask
the Technical Writing team. They're available on Slack in `#docs` or in GitLab by mentioning the
writer for the applicable [DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages).
the Technical Writing team. They're available on Slack in `#docs` or in GitLab by
mentioning [the writer for](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments)
the applicable [DevOps stage or group](https://about.gitlab.com/handbook/product/categories/#devops-stages).
Otherwise, forge ahead with your best effort. It does not need to be perfect;
the team is happy to review and improve upon your content. Review the
[Documentation guidelines](index.md) before you begin your first documentation MR.
@ -787,8 +788,8 @@ This is overridden by the [documentation-specific punctuation rules](#punctuatio
- When possible, avoid including words that might change in the future. Changing
a heading changes its anchor URL, which affects other linked pages.
- When introducing a new document, be careful for the headings to be
grammatically and syntactically correct. Mention an [assigned technical writer (TW)](https://about.gitlab.com/handbook/product/categories/)
for review.
grammatically and syntactically correct. Mention an [assigned technical writer (TW)](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments)
for review, based upon the [product category](https://about.gitlab.com/handbook/product/categories/).
This is to ensure that no document with wrong heading is going live without an
audit, thus preventing dead links and redirection issues when corrected.
- Leave exactly one blank line before and after a heading.

View File

@ -703,6 +703,14 @@ Do not use **type** if you can avoid it. Use **enter** instead.
Do not use **useful**. If the user doesn't find the process to be useful, we lose their trust. ([Vale](../testing.md#vale) rule: [`Simplicity.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/Simplicity.yml))
## user, users
When possible, address the reader directly, instead of calling them **users**.
Use the [second person](#you-your-yours), **you**, instead.
- Do: You can configure a pipeline.
- Do not: Users can configure a pipeline.
## utilize
Do not use **utilize**. Use **use** instead. It's more succinct and easier for non-native English speakers to understand.
@ -729,5 +737,13 @@ One exception: You can use **we recommend** instead of **it is recommended** or
Do not use **whitelist**. Another option is **allowlist**. ([Vale](../testing.md#vale) rule: [`InclusionCultural.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/InclusionCultural.yml))
## you, your, yours
Use **you**, **your**, and **yours** instead of [**the user** and **the user's**](#user-users).
Documentation should be from the [point of view](https://design.gitlab.com/content/voice-tone#point-of-view) of the reader.
- Do: You can configure a pipeline.
- Do not: Users can configure a pipeline.
<!-- vale on -->
<!-- markdownlint-enable -->

View File

@ -21,3 +21,4 @@ feedback, and suggestions.
- [GraphQL BatchLoader](batchloader.md): development documentation on the BatchLoader.
- [GraphQL pagination](pagination.md): development documentation on pagination.
- [GraphQL Pro](graphql_pro.md): information on our GraphQL Pro subscription.
- [GraphQL monitoring](monitoring.md): tips on how to use our monitoring tools to inspect GraphQL queries.

View File

@ -0,0 +1,89 @@
---
stage: Ecosystem
group: Integrations
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Monitoring GraphQL
This page gives tips on how to analyze GraphQL data in our monitoring tools.
Please contribute your own tips to improve this document.
## Kibana
We use Kibana to filter GraphQL query logs. Sign in to [Kibana](https://log.gprd.gitlab.net/)
with a `@gitlab.com` email address.
In Kibana we can inspect two kinds of GraphQL logs:
- Logs of each GraphQL query executed within the request.
- Logs of the full request, which due to [query multiplexing](https://graphql-ruby.org/queries/multiplex.html)
may have executed multiple queries.
### Logs of each GraphQL query
In a [multiplex query](https://graphql-ruby.org/queries/multiplex.html), each individual query
is logged separately. We can use subcomponent filtering to inspect these logs.
[Visit Kibana with this filter enabled](https://log.gprd.gitlab.net/goto/a0da8c9a1e9c1f533a058b7d29d13956)
or set up the subcomponent filter using these steps:
1. Add a filter:
1. Filter: `json.subcomponent`
1. Operator: `is`
1. Value: `graphql_json`
1. Select **Refresh**.
You can select Kibana fields from the **Available fields** section of the sidebar to
add columns to the log table, or [visit this view](https://log.gprd.gitlab.net/goto/5826d3d3affb41cac52e637ffc205905),
which already has a set of Kibana fields selected. Some relevant Kibana fields include:
| Kibana field | Description |
| --- | --- |
| `json.operation_name` | The [operation name](https://graphql.org/learn/queries/#operation-name) used by the client. |
| `json.operation_fingerprint`| The [fingerprint](https://graphql-ruby.org/api-doc/1.12.20/GraphQL/Query#fingerprint-instance_method) of the query, used to recognize repeated queries over time. |
| `json.meta.caller_id` | Appears as `graphql:<operation_name>` for queries that came from the GitLab frontend, otherwise as `graphql:unknown`. Can be used to identify internal versus external queries. |
| `json.query_string` | The query string itself. |
| `json.is_mutation` | `true` when a mutation, `false` when not. |
| `json.query_analysis.used_fields` | List of GraphQL fields selected by the query. |
| `json.query_analysis.used_deprecated_fields` | List of deprecated GraphQL fields selected by the query. |
| `json.query_analysis.duration_s` | Duration of query execution in seconds. |
| `json.query_analysis.complexity` | The [complexity](../api_graphql_styleguide.md#max-complexity) score of the query. |
#### Useful filters
Combine the [subcomponent filter](#logs-of-each-graphql-query) with the following Kibana filters to further interrogate the query logs.
##### Queries that used a particular field
Filter logs by queries that used a particular field:
1. Add a filter:
1. Filter: `json.query_analysis.used_fields`
1. Operator: `is`
1. Value: `Type.myField`, where `Type.myField` is the type name and field name as it
appears in [our GraphQL reference documentation](../../api/graphql/reference/index.md).
1. Select **Refresh**.
##### Queries that used a deprecated field
Filter logs of queries that used a particular deprecated field by following the
[steps above](#queries-that-used-a-particular-field) but use the `json.graphql.used_deprecated_fields`
filter instead.
### Logs of the full request
The full request logs encompass log data for all [multiplexed queries](https://graphql-ruby.org/queries/multiplex.html)
in the request, as well as data from time spent outside of `GraphQLController#execute`.
To see the full request logs, do **not** apply the `json.subcomponent` [filter](#logs-of-each-graphql-query), and instead:
1. Add a filter:
1. Filter: `json.meta.caller_id`
1. Operator: `is`
1. Value: `GraphqlController#execute`
1. Select **Refresh**.
Some differences from the [query logs](#logs-of-each-graphql-query) described above:
- Some of the [Kibana fields mentioned above](#logs-of-each-graphql-query) are not available to the full request logs.
- The names of filters differ. For example, instead of `json.query_analysis.used_fields` you select `json.graphql.used_fields`.

View File

@ -199,6 +199,7 @@ The following data is included in the export:
- Type
- Path
- Access level ([Project](../permissions.md#project-members-permissions) and [Group](../permissions.md#group-members-permissions))
- Date of last activity ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345388) in GitLab 14.6). For a list of activities that populate this column, see the [Users API documentation](../../api/users.md#get-user-activities-admin-only).
![user permission export button](img/export_permissions_v13_11.png)

View File

@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: howto
---
# Active sessions
# Active sessions **(FREE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17867) in GitLab 10.8.

View File

@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: concepts, howto
---
# Profile preferences
# Profile preferences **(FREE)**
A user's profile preferences page allows the user to customize various aspects
of GitLab to their liking.
@ -120,7 +120,7 @@ You can include the following options for your default dashboard view:
- Your [To-Do List](../todos.md)
- Assigned Issues
- Assigned Merge Requests
- Operations Dashboard **(PREMIUM)**
- [Operations Dashboard](../operations_dashboard/index.md)
### Group overview content
@ -130,7 +130,7 @@ displayed on a group's home page.
You can choose between 2 options:
- Details (default)
- [Security dashboard](../application_security/security_dashboard/index.md) **(ULTIMATE)**
- [Security dashboard](../application_security/security_dashboard/index.md)
### Project overview content

View File

@ -15,7 +15,7 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def seconds
@query = @query.select(median_duration_in_seconds.as('median')).reorder(nil)
@query = @query.select(duration_in_seconds(percentile_cont).as('median')).reorder(nil)
result = @query.take || {}
result['median'] || nil

View File

@ -36,7 +36,7 @@ module Gitlab
def serialized_records
strong_memoize(:serialized_records) do
records = ordered_and_limited_query.select(stage_event_model.arel_table[Arel.star], duration.as('total_time'))
records = ordered_and_limited_query.select(stage_event_model.arel_table[Arel.star], duration_in_seconds.as('total_time'))
yield records if block_given?
issuables_and_records = load_issuables(records)

View File

@ -27,13 +27,13 @@ module Gitlab
end
end
def median_duration_in_seconds
Arel::Nodes::Extract.new(percentile_cont, :epoch)
end
def in_progress?
params[:end_event_filter] == :in_progress
end
def duration_in_seconds(duration_expression = duration)
Arel::Nodes::Extract.new(duration_expression, :epoch)
end
end
end
end

View File

@ -531,6 +531,7 @@ vulnerability_issue_links: :gitlab_main
vulnerability_occurrence_identifiers: :gitlab_main
vulnerability_occurrence_pipelines: :gitlab_main
vulnerability_occurrences: :gitlab_main
vulnerability_reads: :gitlab_main
vulnerability_remediations: :gitlab_main
vulnerability_scanners: :gitlab_main
vulnerability_statistics: :gitlab_main

View File

@ -97,11 +97,15 @@ module Gitlab
end
def use_primary_and_secondary_stores?
Feature.enabled?("use_primary_and_secondary_stores_for_#{instance_name.underscore}", default_enabled: :yaml) && !same_redis_store?
feature_flags_available? &&
Feature.enabled?("use_primary_and_secondary_stores_for_#{instance_name.underscore}", default_enabled: :yaml) &&
!same_redis_store?
end
def use_primary_store_as_default?
Feature.enabled?("use_primary_store_as_default_for_#{instance_name.underscore}", default_enabled: :yaml) && !same_redis_store?
feature_flags_available? &&
Feature.enabled?("use_primary_store_as_default_for_#{instance_name.underscore}", default_enabled: :yaml) &&
!same_redis_store?
end
private

View File

@ -7368,10 +7368,10 @@ msgstr ""
msgid "Cluster type must be specificed for Stages::ClusterEndpointInserter"
msgstr ""
msgid "ClusterAgents|%{number} of %{total} Agent based integrations"
msgid "ClusterAgents|%{number} of %{total} agents"
msgstr ""
msgid "ClusterAgents|%{number} of %{total} Certificate based integrations"
msgid "ClusterAgents|%{number} of %{total} clusters connected through cluster certificates"
msgstr ""
msgid "ClusterAgents|Access tokens"
@ -7380,6 +7380,9 @@ msgstr ""
msgid "ClusterAgents|Actions"
msgstr ""
msgid "ClusterAgents|Advanced installation methods"
msgstr ""
msgid "ClusterAgents|Agent"
msgstr ""
@ -7392,9 +7395,6 @@ msgstr ""
msgid "ClusterAgents|All"
msgstr ""
msgid "ClusterAgents|Alternative installation methods"
msgstr ""
msgid "ClusterAgents|An error occurred while loading your GitLab Agents"
msgstr ""
@ -7404,22 +7404,28 @@ msgstr ""
msgid "ClusterAgents|An unknown error occurred. Please try again."
msgstr ""
msgid "ClusterAgents|Certificate based"
msgid "ClusterAgents|Certificate"
msgstr ""
msgid "ClusterAgents|Configuration"
msgstr ""
msgid "ClusterAgents|Connect a cluster through the Agent"
msgstr ""
msgid "ClusterAgents|Connect existing cluster"
msgstr ""
msgid "ClusterAgents|Connect with Agent"
msgid "ClusterAgents|Connect with a certificate"
msgstr ""
msgid "ClusterAgents|Connect with a GitLab Agent"
msgid "ClusterAgents|Connect with the Agent"
msgstr ""
msgid "ClusterAgents|Connect with certificate"
msgid "ClusterAgents|Connect with the GitLab Agent"
msgstr ""
msgid "ClusterAgents|Connect your cluster through the Agent"
msgstr ""
msgid "ClusterAgents|Connected"
@ -7431,7 +7437,7 @@ msgstr ""
msgid "ClusterAgents|Copy token"
msgstr ""
msgid "ClusterAgents|Create new cluster"
msgid "ClusterAgents|Create a new cluster"
msgstr ""
msgid "ClusterAgents|Created by"
@ -7443,28 +7449,31 @@ msgstr ""
msgid "ClusterAgents|Date created"
msgstr ""
msgid "ClusterAgents|Deprecated"
msgstr ""
msgid "ClusterAgents|Description"
msgstr ""
msgid "ClusterAgents|For alternative installation methods %{linkStart}go to the documentation%{linkEnd}."
msgid "ClusterAgents|Failed to register an agent"
msgstr ""
msgid "ClusterAgents|GitLab Agents"
msgid "ClusterAgents|For the advanced installation method %{linkStart}see the documentation%{linkEnd}."
msgstr ""
msgid "ClusterAgents|GitLab Agents provide an increased level of security when integrating with clusters. %{linkStart}Learn more about the GitLab Kubernetes Agent.%{linkEnd}"
msgid "ClusterAgents|GitLab Agent"
msgstr ""
msgid "ClusterAgents|GitLab Kubernetes Agent"
msgid "ClusterAgents|GitLab Agent for Kubernetes"
msgstr ""
msgid "ClusterAgents|Go to the repository"
msgid "ClusterAgents|Go to the repository files"
msgstr ""
msgid "ClusterAgents|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}"
msgid "ClusterAgents|How to register an agent?"
msgstr ""
msgid "ClusterAgents|Install new Agent"
msgid "ClusterAgents|Install a new agent"
msgstr ""
msgid "ClusterAgents|Last connected %{timeAgo}."
@ -7479,15 +7488,6 @@ msgstr ""
msgid "ClusterAgents|Learn how to troubleshoot"
msgstr ""
msgid "ClusterAgents|Learn more about installing a GitLab Kubernetes Agent"
msgstr ""
msgid "ClusterAgents|Learn more about the GitLab Kubernetes Agent registration."
msgstr ""
msgid "ClusterAgents|Learn more about the GitLab Kubernetes Agent."
msgstr ""
msgid "ClusterAgents|Make sure you are using a valid token."
msgstr ""
@ -7500,10 +7500,10 @@ msgstr ""
msgid "ClusterAgents|Never connected"
msgstr ""
msgid "ClusterAgents|No Agent based integrations"
msgid "ClusterAgents|No agents"
msgstr ""
msgid "ClusterAgents|No Certificate based integrations"
msgid "ClusterAgents|No clusters connected through cluster certificates"
msgstr ""
msgid "ClusterAgents|Not connected"
@ -7515,7 +7515,10 @@ msgstr ""
msgid "ClusterAgents|Recommended installation method"
msgstr ""
msgid "ClusterAgents|Register Agent"
msgid "ClusterAgents|Register"
msgstr ""
msgid "ClusterAgents|Register an agent to generate a token that will be used to install the agent on your cluster in the next step."
msgstr ""
msgid "ClusterAgents|Registering Agent"
@ -7527,51 +7530,48 @@ msgstr ""
msgid "ClusterAgents|Security"
msgstr ""
msgid "ClusterAgents|Select an Agent"
msgid "ClusterAgents|Select an agent"
msgstr ""
msgid "ClusterAgents|Select an Agent to register with GitLab and install on your cluster."
msgid "ClusterAgents|Select an agent to register with GitLab"
msgstr ""
msgid "ClusterAgents|Select which Agent you want to install"
msgid "ClusterAgents|The GitLab Agent provides an increased level of security when connecting Kubernetes clusters to GitLab. %{linkStart}Learn more about the GitLab Agent.%{linkEnd}"
msgstr ""
msgid "ClusterAgents|The Agent has not been connected in a long time. There might be a connectivity issue. Last contact was %{timeAgo}."
msgid "ClusterAgents|The agent has not been connected in a long time. There might be a connectivity issue. Last contact was %{timeAgo}."
msgstr ""
msgid "ClusterAgents|The GitLab Agent also requires %{linkStart}enabling the Agent Server%{linkEnd}"
msgid "ClusterAgents|The recommended installation method includes the token. If you want to follow the advanced installation method provided in the docs, make sure you save the token value before you close this window."
msgstr ""
msgid "ClusterAgents|The recommended installation method provided below includes the token. If you want to follow the alternative installation method provided in the docs make sure you save the token value before you close the window."
msgstr ""
msgid "ClusterAgents|The registration token will be used to connect the Agent on your cluster to GitLab. To learn more about the registration tokens and how they are used %{linkStart}go to the documentation%{linkEnd}."
msgstr ""
msgid "ClusterAgents|The token value will not be shown again after you close this window."
msgid "ClusterAgents|The registration token will be used to connect the agent on your cluster to GitLab. %{linkStart}What are registration tokens?%{linkEnd}"
msgstr ""
msgid "ClusterAgents|This agent has no tokens"
msgstr ""
msgid "ClusterAgents|To install an Agent you should create an agent directory in the Repository first. We recommend that you add the Agent configuration to the directory before you start the installation process."
msgid "ClusterAgents|To install a new agent, first add the agent's configuration file to this repository. %{linkStart}What's the agent's configuration file?%{linkEnd}"
msgstr ""
msgid "ClusterAgents|Unknown user"
msgstr ""
msgid "ClusterAgents|Use GitLab Agents to more securely integrate with your clusters to deploy your applications, run your pipelines, use review apps and much more."
msgid "ClusterAgents|View all %{number} agents"
msgstr ""
msgid "ClusterAgents|View all %{number} Agent based integrations"
msgid "ClusterAgents|View all %{number} clusters"
msgstr ""
msgid "ClusterAgents|View all %{number} Certificate based integrations"
msgid "ClusterAgents|You cannot see this token again after you close this window."
msgstr ""
msgid "ClusterAgents|You will need to create a token to connect to your agent"
msgstr ""
msgid "ClusterAgents|Your instance doesn't have the %{linkStart}GitLab Agent Server (KAS)%{linkEnd} set up. Ask a GitLab Administrator to install it."
msgstr ""
msgid "ClusterAgent|User has insufficient permissions to create a token for this project"
msgstr ""
@ -7722,6 +7722,9 @@ msgstr ""
msgid "ClusterIntegration|Connect with a certificate"
msgstr ""
msgid "ClusterIntegration|Connect your cluster to GitLab through %{linkStart}cluster certificates%{linkEnd}."
msgstr ""
msgid "ClusterIntegration|Connection Error"
msgstr ""
@ -7881,9 +7884,6 @@ msgstr ""
msgid "ClusterIntegration|If you are setting up multiple clusters and are using Auto DevOps, %{help_link_start}read this first%{help_link_end}."
msgstr ""
msgid "ClusterIntegration|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}"
msgstr ""
msgid "ClusterIntegration|If you do not wish to delete all associated GitLab resources, you can simply remove the integration."
msgstr ""
@ -7938,9 +7938,6 @@ msgstr ""
msgid "ClusterIntegration|Learn more about instance Kubernetes clusters"
msgstr ""
msgid "ClusterIntegration|Learn more about the GitLab managed clusters"
msgstr ""
msgid "ClusterIntegration|Loading IAM Roles"
msgstr ""
@ -8220,6 +8217,9 @@ msgstr ""
msgid "ClusterIntegration|The URL used to access the Kubernetes API."
msgstr ""
msgid "ClusterIntegration|The certificate-based method to connect clusters to GitLab was %{linkStart}deprecated%{linkEnd} in GitLab 14.5."
msgstr ""
msgid "ClusterIntegration|The namespace associated with your project. This will be used for deploy boards, logs, and Web terminals."
msgstr ""
@ -8274,7 +8274,7 @@ msgstr ""
msgid "ClusterIntegration|Unknown Error"
msgstr ""
msgid "ClusterIntegration|Use certificates to integrate with your clusters to deploy your applications, run your pipelines, use review apps and much more in an easy way."
msgid "ClusterIntegration|Use the %{linkStart}GitLab Agent%{linkEnd} to safely connect your Kubernetes clusters to GitLab. You can deploy your applications, run your pipelines, use Review Apps, and much more."
msgstr ""
msgid "ClusterIntegration|Uses the Cloud Run, Istio, and HTTP Load Balancing addons for this cluster."
@ -14456,9 +14456,6 @@ msgstr ""
msgid "Failed to publish issue on status page."
msgstr ""
msgid "Failed to register Agent"
msgstr ""
msgid "Failed to remove a Zoom meeting"
msgstr ""
@ -23050,9 +23047,6 @@ msgstr ""
msgid "NetworkPolicies|None selected"
msgstr ""
msgid "NetworkPolicies|Please %{installLinkStart}install%{installLinkEnd} and %{configureLinkStart}configure a Kubernetes Agent for this project%{configureLinkEnd} to enable alerts."
msgstr ""
msgid "NetworkPolicies|Policy %{policyName} was successfully changed"
msgstr ""
@ -23077,6 +23071,9 @@ msgstr ""
msgid "NetworkPolicies|Something went wrong, unable to fetch policies"
msgstr ""
msgid "NetworkPolicies|To enable alerts, %{installLinkStart}install an agent%{installLinkEnd} first."
msgstr ""
msgid "NetworkPolicies|Traffic that does not match any rule will be blocked."
msgstr ""
@ -24414,7 +24411,7 @@ msgstr ""
msgid "Open Selection"
msgstr ""
msgid "Open a CLI and connect to the cluster you want to install the Agent in. Use this installation method to minimize any manual steps. The token is already included in the command."
msgid "Open a CLI and connect to the cluster you want to install the agent in. Use this installation method to minimize any manual steps. The token is already included in the command."
msgstr ""
msgid "Open epics"

View File

@ -25,13 +25,13 @@ RSpec.describe 'Cluster agent registration', :js do
it 'allows the user to select an agent to install, and displays the resulting agent token' do
click_button('Actions')
expect(page).to have_content('Register Agent')
expect(page).to have_content('Register')
click_button('Select an Agent')
click_button('Select an agent')
click_button('example-agent-2')
click_button('Register Agent')
click_button('Register')
expect(page).to have_content('The token value will not be shown again after you close this window.')
expect(page).to have_content('You cannot see this token again after you close this window.')
expect(page).to have_content('example-agent-token')
expect(page).to have_content('docker run --pull=always --rm')

View File

@ -22,7 +22,7 @@ RSpec.describe 'ClusterAgents', :js do
end
it 'displays empty state', :aggregate_failures do
expect(page).to have_content('Install new Agent')
expect(page).to have_content('Install a new agent')
expect(page).to have_selector('.empty-state')
end
end

View File

@ -19,7 +19,7 @@ RSpec.describe 'AWS EKS Cluster', :js do
before do
visit project_clusters_path(project)
click_link 'Certificate based'
click_link 'Certificate'
click_link 'Connect with a certificate'
end

View File

@ -33,7 +33,7 @@ RSpec.describe 'Gcp Cluster', :js do
before do
visit project_clusters_path(project)
click_link 'Certificate based'
click_link 'Certificate'
click_link 'Connect with a certificate'
click_link 'Create new cluster'
click_link 'Google GKE'
@ -145,9 +145,9 @@ RSpec.describe 'Gcp Cluster', :js do
before do
visit project_clusters_path(project)
click_link 'Certificate based'
click_link 'Certificate'
click_button(class: 'dropdown-toggle-split')
click_link 'Connect with certificate'
click_link 'Connect with a certificate'
end
it 'user sees the "Environment scope" field' do
@ -161,7 +161,7 @@ RSpec.describe 'Gcp Cluster', :js do
click_button 'Remove integration and resources'
fill_in 'confirm_cluster_name_input', with: cluster.name
click_button 'Remove integration'
click_link 'Certificate based'
click_link 'Certificate'
end
it 'user sees creation form with the successful message' do
@ -175,7 +175,7 @@ RSpec.describe 'Gcp Cluster', :js do
context 'when user has not dismissed GCP signup offer' do
before do
visit project_clusters_path(project)
click_link 'Certificate based'
click_link 'Certificate'
end
it 'user sees offer on cluster index page' do
@ -192,7 +192,7 @@ RSpec.describe 'Gcp Cluster', :js do
context 'when user has dismissed GCP signup offer' do
before do
visit project_clusters_path(project)
click_link 'Certificate based'
click_link 'Certificate'
end
it 'user does not see offer after dismissing' do

View File

@ -25,7 +25,7 @@ RSpec.describe 'User Cluster', :js do
before do
visit project_clusters_path(project)
click_link 'Certificate based'
click_link 'Certificate'
click_link 'Connect with a certificate'
click_link 'Connect existing cluster'
end
@ -113,7 +113,7 @@ RSpec.describe 'User Cluster', :js do
click_button 'Remove integration and resources'
fill_in 'confirm_cluster_name_input', with: cluster.name
click_button 'Remove integration'
click_link 'Certificate based'
click_link 'Certificate'
end
it 'user sees creation form with the successful message' do

View File

@ -10,13 +10,13 @@ RSpec.describe 'Clusters', :js do
before do
project.add_maintainer(user)
gitlab_sign_in(user)
sign_in(user)
end
context 'when user does not have a cluster and visits cluster index page' do
before do
visit project_clusters_path(project)
click_link 'Certificate based'
click_link 'Certificate'
end
it 'sees empty state' do
@ -34,17 +34,17 @@ RSpec.describe 'Clusters', :js do
before do
create(:cluster, :provided_by_user, name: 'default-cluster', environment_scope: '*', projects: [project])
visit project_clusters_path(project)
click_link 'Certificate based'
click_link 'Certificate'
click_button(class: 'dropdown-toggle-split')
end
it 'user sees an add cluster button' do
expect(page).to have_content('Connect with certificate')
expect(page).to have_content('Connect with a certificate')
end
context 'when user filled form with environment scope' do
before do
click_link 'Connect with certificate'
click_link 'Connect with a certificate'
fill_in 'cluster_name', with: 'staging-cluster'
fill_in 'cluster_environment_scope', with: 'staging/*'
click_button 'Add Kubernetes cluster'
@ -72,7 +72,7 @@ RSpec.describe 'Clusters', :js do
context 'when user updates duplicated environment scope' do
before do
click_link 'Connect with certificate'
click_link 'Connect with a certificate'
fill_in 'cluster_name', with: 'staging-cluster'
fill_in 'cluster_environment_scope', with: '*'
fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'https://0.0.0.0'
@ -109,13 +109,13 @@ RSpec.describe 'Clusters', :js do
create(:cluster, :provided_by_gcp, name: 'default-cluster', environment_scope: '*', projects: [project])
visit project_clusters_path(project)
click_link 'Certificate based'
click_link 'Certificate'
end
context 'when user filled form with environment scope' do
before do
click_button(class: 'dropdown-toggle-split')
click_link 'Create new cluster'
click_link 'Create a new cluster'
click_link 'Google GKE'
sleep 2 # wait for ajax
@ -160,7 +160,7 @@ RSpec.describe 'Clusters', :js do
context 'when user updates duplicated environment scope' do
before do
click_button(class: 'dropdown-toggle-split')
click_link 'Create new cluster'
click_link 'Create a new cluster'
click_link 'Google GKE'
sleep 2 # wait for ajax
@ -190,7 +190,7 @@ RSpec.describe 'Clusters', :js do
before do
visit project_clusters_path(project)
click_link 'Certificate based'
click_link 'Certificate'
end
it 'user sees a table with one cluster' do
@ -213,7 +213,7 @@ RSpec.describe 'Clusters', :js do
before do
visit project_clusters_path(project)
click_link 'Certificate based'
click_link 'Certificate'
click_link 'Connect with a certificate'
click_link 'Create new cluster'
end

View File

@ -11,7 +11,7 @@ RSpec.describe Environments::EnvironmentsByDeploymentsFinder do
project.add_maintainer(user)
end
shared_examples 'execute' do
describe '#execute' do
context 'tagged deployment' do
let(:environment_two) { create(:environment, project: project) }
# Environments need to include commits, so rewind two commits to fit
@ -124,16 +124,4 @@ RSpec.describe Environments::EnvironmentsByDeploymentsFinder do
end
end
end
describe "#execute" do
include_examples 'execute'
context 'when environments_by_deployments_finder_exists_optimization is disabled' do
before do
stub_feature_flags(environments_by_deployments_finder_exists_optimization: false)
end
include_examples 'execute'
end
end
end

View File

@ -1,4 +1,4 @@
import { GlEmptyState, GlSprintf } from '@gitlab/ui';
import { GlEmptyState, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
import AgentEmptyState from '~/clusters_list/components/agent_empty_state.vue';
import { INSTALL_AGENT_MODAL_ID } from '~/clusters_list/constants';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@ -6,8 +6,7 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { helpPagePath } from '~/helpers/help_page_helper';
const emptyStateImage = '/path/to/image';
const multipleClustersDocsUrl = helpPagePath('user/project/clusters/multiple_kubernetes_clusters');
const installDocsUrl = helpPagePath('administration/clusters/kas');
const installDocsUrl = helpPagePath('user/clusters/agent/index');
describe('AgentEmptyStateComponent', () => {
let wrapper;
@ -15,9 +14,8 @@ describe('AgentEmptyStateComponent', () => {
emptyStateImage,
};
const findMultipleClustersDocsLink = () => wrapper.findByTestId('multiple-clusters-docs-link');
const findInstallDocsLink = () => wrapper.findByTestId('install-docs-link');
const findIntegrationButton = () => wrapper.findByTestId('integration-primary-button');
const findInstallDocsLink = () => wrapper.findComponent(GlLink);
const findIntegrationButton = () => wrapper.findComponent(GlButton);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
beforeEach(() => {
@ -44,8 +42,7 @@ describe('AgentEmptyStateComponent', () => {
expect(findIntegrationButton().exists()).toBe(true);
});
it('renders correct href attributes for the links', () => {
expect(findMultipleClustersDocsLink().attributes('href')).toBe(multipleClustersDocsUrl);
it('renders correct href attributes for the docs link', () => {
expect(findInstallDocsLink().attributes('href')).toBe(installDocsUrl);
});

View File

@ -6,35 +6,33 @@ import ClusterStore from '~/clusters_list/store';
const clustersEmptyStateImage = 'path/to/svg';
const newClusterPath = '/path/to/connect/cluster';
const emptyStateHelpText = 'empty state text';
const canAddCluster = true;
describe('ClustersEmptyStateComponent', () => {
let wrapper;
const propsData = {
isChildComponent: false,
};
const provideData = {
const defaultProvideData = {
clustersEmptyStateImage,
emptyStateHelpText: null,
newClusterPath,
};
const entryData = {
canAddCluster,
};
const findButton = () => wrapper.findComponent(GlButton);
const findEmptyStateText = () => wrapper.findByTestId('clusters-empty-state-text');
beforeEach(() => {
const createWrapper = ({
provideData = { emptyStateHelpText: null },
isChildComponent = false,
canAddCluster = true,
} = {}) => {
wrapper = shallowMountExtended(ClustersEmptyState, {
store: ClusterStore(entryData),
propsData,
provide: provideData,
store: ClusterStore({ canAddCluster }),
propsData: { isChildComponent },
provide: { ...defaultProvideData, ...provideData },
stubs: { GlEmptyState },
});
};
beforeEach(() => {
createWrapper();
});
afterEach(() => {
@ -55,16 +53,7 @@ describe('ClustersEmptyStateComponent', () => {
describe('when the component is loaded as a child component', () => {
beforeEach(() => {
propsData.isChildComponent = true;
wrapper = shallowMountExtended(ClustersEmptyState, {
store: ClusterStore(entryData),
propsData,
provide: provideData,
});
});
afterEach(() => {
propsData.isChildComponent = false;
createWrapper({ isChildComponent: true });
});
it('should not render the action button', () => {
@ -74,12 +63,7 @@ describe('ClustersEmptyStateComponent', () => {
describe('when the help text is provided', () => {
beforeEach(() => {
provideData.emptyStateHelpText = emptyStateHelpText;
wrapper = shallowMountExtended(ClustersEmptyState, {
store: ClusterStore(entryData),
propsData,
provide: provideData,
});
createWrapper({ provideData: { emptyStateHelpText } });
});
it('should show the empty state text', () => {
@ -88,14 +72,8 @@ describe('ClustersEmptyStateComponent', () => {
});
describe('when the user cannot add clusters', () => {
entryData.canAddCluster = false;
beforeEach(() => {
wrapper = shallowMountExtended(ClustersEmptyState, {
store: ClusterStore(entryData),
propsData,
provide: provideData,
stubs: { GlEmptyState },
});
createWrapper({ canAddCluster: false });
});
it('should disable the button', () => {
expect(findButton().props('disabled')).toBe(true);

View File

@ -59,10 +59,10 @@ describe('ClustersMainViewComponent', () => {
describe('tabs', () => {
it.each`
tabTitle | queryParamValue | lineNumber
${'All'} | ${'all'} | ${0}
${'Agent'} | ${AGENT} | ${1}
${'Certificate based'} | ${CERTIFICATE_BASED} | ${2}
tabTitle | queryParamValue | lineNumber
${'All'} | ${'all'} | ${0}
${'Agent'} | ${AGENT} | ${1}
${'Certificate'} | ${CERTIFICATE_BASED} | ${2}
`(
'renders correct tab title and query param value',
({ tabTitle, queryParamValue, lineNumber }) => {

View File

@ -219,15 +219,21 @@ describe('IDE services', () => {
describe('getProjectData', () => {
it('combines gql and API requests', () => {
const gqlProjectData = {
id: 'gid://gitlab/Project/1',
userPermissions: {
bogus: true,
},
};
const expectedResponse = {
...projectData,
...gqlProjectData,
id: 1,
};
Api.project.mockReturnValue(Promise.resolve({ data: { ...projectData } }));
query.mockReturnValue(Promise.resolve({ data: { project: gqlProjectData } }));
return services.getProjectData(TEST_NAMESPACE, TEST_PROJECT).then((response) => {
expect(response).toEqual({ data: { ...projectData, ...gqlProjectData } });
expect(response).toEqual({ data: expectedResponse });
expect(Api.project).toHaveBeenCalledWith(TEST_PROJECT_ID);
expect(query).toHaveBeenCalledWith({
query: getIdeProject,

View File

@ -5,7 +5,14 @@ import ActionsCell from '~/jobs/components/table/cells/actions_cell.vue';
import JobPlayMutation from '~/jobs/components/table/graphql/mutations/job_play.mutation.graphql';
import JobRetryMutation from '~/jobs/components/table/graphql/mutations/job_retry.mutation.graphql';
import JobUnscheduleMutation from '~/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql';
import { playableJob, retryableJob, scheduledJob } from '../../../mock_data';
import {
playableJob,
retryableJob,
scheduledJob,
cannotRetryJob,
cannotPlayJob,
cannotPlayScheduledJob,
} from '../../../mock_data';
describe('Job actions cell', () => {
let wrapper;
@ -57,6 +64,17 @@ describe('Job actions cell', () => {
expect(findDownloadArtifactsButton().exists()).toBe(false);
});
it.each`
button | action | jobType
${findPlayButton} | ${'play'} | ${cannotPlayJob}
${findRetryButton} | ${'retry'} | ${cannotRetryJob}
${findPlayScheduledJobButton} | ${'play scheduled'} | ${cannotPlayScheduledJob}
`('does not display the $action button if user cannot update build', ({ button, jobType }) => {
createComponent(jobType);
expect(button().exists()).toBe(false);
});
it.each`
button | action | jobType
${findPlayButton} | ${'play'} | ${playableJob}

View File

@ -1563,6 +1563,7 @@ export const mockJobsQueryResponse = {
userPermissions: {
readBuild: true,
readJobArtifacts: true,
updateBuild: true,
__typename: 'JobPermissions',
},
__typename: 'CiJob',
@ -1636,10 +1637,15 @@ export const retryableJob = {
cancelable: false,
active: false,
stuck: false,
userPermissions: { readBuild: true, __typename: 'JobPermissions' },
userPermissions: { readBuild: true, updateBuild: true, __typename: 'JobPermissions' },
__typename: 'CiJob',
};
export const cannotRetryJob = {
...retryableJob,
userPermissions: { readBuild: true, updateBuild: false, __typename: 'JobPermissions' },
};
export const playableJob = {
artifacts: {
nodes: [
@ -1700,10 +1706,25 @@ export const playableJob = {
cancelable: false,
active: false,
stuck: false,
userPermissions: { readBuild: true, readJobArtifacts: true, __typename: 'JobPermissions' },
userPermissions: {
readBuild: true,
readJobArtifacts: true,
updateBuild: true,
__typename: 'JobPermissions',
},
__typename: 'CiJob',
};
export const cannotPlayJob = {
...playableJob,
userPermissions: {
readBuild: true,
readJobArtifacts: true,
updateBuild: false,
__typename: 'JobPermissions',
},
};
export const scheduledJob = {
artifacts: { nodes: [], __typename: 'CiJobArtifactConnection' },
allowFailure: false,
@ -1756,6 +1777,16 @@ export const scheduledJob = {
cancelable: false,
active: false,
stuck: false,
userPermissions: { readBuild: true, __typename: 'JobPermissions' },
userPermissions: { readBuild: true, updateBuild: true, __typename: 'JobPermissions' },
__typename: 'CiJob',
};
export const cannotPlayScheduledJob = {
...scheduledJob,
userPermissions: {
readBuild: true,
readJobArtifacts: true,
updateBuild: false,
__typename: 'JobPermissions',
},
};

View File

@ -5,7 +5,8 @@ import {
GlDropdownSectionHeader,
GlSearchBoxByType,
} from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
@ -52,8 +53,7 @@ describe('NewProjectUrlSelect component', () => {
},
};
const localVue = createLocalVue();
localVue.use(VueApollo);
Vue.use(VueApollo);
const defaultProvide = {
namespaceFullPath: 'h5bp',
@ -64,17 +64,19 @@ describe('NewProjectUrlSelect component', () => {
userNamespaceId: '1',
};
let mockQueryResponse;
const mountComponent = ({
search = '',
queryResponse = data,
provide = defaultProvide,
mountFn = shallowMount,
} = {}) => {
const requestHandlers = [[searchQuery, jest.fn().mockResolvedValue({ data: queryResponse })]];
mockQueryResponse = jest.fn().mockResolvedValue({ data: queryResponse });
const requestHandlers = [[searchQuery, mockQueryResponse]];
const apolloProvider = createMockApollo(requestHandlers);
return mountFn(NewProjectUrlSelect, {
localVue,
apolloProvider,
provide,
data() {
@ -88,12 +90,19 @@ describe('NewProjectUrlSelect component', () => {
const findButtonLabel = () => wrapper.findComponent(GlButton);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findInput = () => wrapper.findComponent(GlSearchBoxByType);
const findHiddenInput = () => wrapper.find('input');
const findHiddenInput = () => wrapper.find('[name="project[namespace_id]"]');
const clickDropdownItem = async () => {
wrapper.findComponent(GlDropdownItem).vm.$emit('click');
await wrapper.vm.$nextTick();
};
const showDropdown = async () => {
findDropdown().vm.$emit('shown');
await wrapper.vm.$apollo.queries.currentUser.refetch();
jest.runOnlyPendingTimers();
};
afterEach(() => {
wrapper.destroy();
});
@ -141,20 +150,18 @@ describe('NewProjectUrlSelect component', () => {
it('focuses on the input when the dropdown is opened', async () => {
wrapper = mountComponent({ mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
const spy = jest.spyOn(findInput().vm, 'focusInput');
findDropdown().vm.$emit('shown');
await showDropdown();
expect(spy).toHaveBeenCalledTimes(1);
});
it('renders expected dropdown items', async () => {
wrapper = mountComponent({ mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
await showDropdown();
const listItems = wrapper.findAll('li');
@ -167,15 +174,36 @@ describe('NewProjectUrlSelect component', () => {
expect(listItems.at(5).text()).toBe(data.currentUser.namespace.fullPath);
});
describe('query fetching', () => {
describe('on component mount', () => {
it('does not fetch query', () => {
wrapper = mountComponent({ mountFn: mount });
expect(mockQueryResponse).not.toHaveBeenCalled();
});
});
describe('on dropdown shown', () => {
it('fetches query', async () => {
wrapper = mountComponent({ mountFn: mount });
await showDropdown();
expect(mockQueryResponse).toHaveBeenCalled();
});
});
});
describe('when selecting from a group template', () => {
const groupId = getIdFromGraphQLId(data.currentUser.groups.nodes[1].id);
const { fullPath, id } = data.currentUser.groups.nodes[1];
beforeEach(async () => {
wrapper = mountComponent({ mountFn: mount });
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
eventHub.$emit('select-template', groupId);
// Show dropdown to fetch projects
await showDropdown();
eventHub.$emit('select-template', getIdFromGraphQLId(id), fullPath);
});
it('filters the dropdown items to the selected group and children', async () => {
@ -188,7 +216,7 @@ describe('NewProjectUrlSelect component', () => {
});
it('sets the selection to the group', async () => {
expect(findDropdown().props('text')).toBe(data.currentUser.groups.nodes[1].fullPath);
expect(findDropdown().props('text')).toBe(fullPath);
});
});
@ -214,12 +242,13 @@ describe('NewProjectUrlSelect component', () => {
});
it('emits `update-visibility` event to update the visibility radio options', async () => {
wrapper = mountComponent();
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
wrapper = mountComponent({ mountFn: mount });
const spy = jest.spyOn(eventHub, '$emit');
// Show dropdown to fetch projects
await showDropdown();
await clickDropdownItem();
const namespace = data.currentUser.groups.nodes[0];
@ -233,16 +262,16 @@ describe('NewProjectUrlSelect component', () => {
});
it('updates hidden input with selected namespace', async () => {
wrapper = mountComponent();
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
wrapper = mountComponent({ mountFn: mount });
// Show dropdown to fetch projects
await showDropdown();
await clickDropdownItem();
expect(findHiddenInput().attributes()).toMatchObject({
name: 'project[namespace_id]',
value: getIdFromGraphQLId(data.currentUser.groups.nodes[0].id).toString(),
});
expect(findHiddenInput().attributes('value')).toBe(
getIdFromGraphQLId(data.currentUser.groups.nodes[0].id).toString(),
);
});
it('tracks clicking on the dropdown', () => {

View File

@ -41,6 +41,19 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher do
it_behaves_like 'match returned records'
end
context 'when intervalstyle setting is configured to "postgres"' do
it 'avoids nil durations' do
# ActiveRecord cannot parse the 'postgres' intervalstyle, it returns nil
# The setting is rolled back after the test case.
Analytics::CycleAnalytics::IssueStageEvent.connection.execute("SET LOCAL intervalstyle='postgres'")
records_fetcher.serialized_records do |relation|
durations = relation.map(&:total_time)
expect(durations).to all(be > 0)
end
end
end
context 'when sorting by end event ASC' do
let(:expected_issue_ids) { [issue_2.iid, issue_1.iid, issue_3.iid] }

View File

@ -1462,7 +1462,7 @@ RSpec.describe Issue do
it 'schedules rebalancing if there is no space left' do
lhs = build_stubbed(:issue, relative_position: 99, project: project)
to_move = build(:issue, project: project)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project_id, namespace_id)
expect(Issues::RebalancingWorker).to receive(:perform_async).with(nil, project_id, namespace_id)
expect { to_move.move_between(lhs, issue) }.to raise_error(RelativePositioning::NoSpaceLeft)
end

View File

@ -154,7 +154,7 @@ RSpec.describe Issues::CreateService do
end
it 'moves the issue to the end, in an asynchronous worker' do
expect(IssuePlacementWorker).to receive(:perform_async).with(be_nil, Integer)
expect(Issues::PlacementWorker).to receive(:perform_async).with(be_nil, Integer)
described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
end

View File

@ -319,7 +319,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id]
expect(IssueRebalancingWorker).not_to receive(:perform_async)
expect(Issues::RebalancingWorker).not_to receive(:perform_async)
update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
@ -335,7 +335,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id]
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, nil, project.root_namespace.id)
expect(Issues::RebalancingWorker).to receive(:perform_async).with(nil, nil, project.root_namespace.id)
update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
@ -349,7 +349,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id]
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, nil, project.root_namespace.id)
expect(Issues::RebalancingWorker).to receive(:perform_async).with(nil, nil, project.root_namespace.id)
update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
@ -363,7 +363,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id]
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, nil, project.root_namespace.id)
expect(Issues::RebalancingWorker).to receive(:perform_async).with(nil, nil, project.root_namespace.id)
update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)

View File

@ -35,7 +35,7 @@ RSpec.describe IssuePlacementWorker do
it 'schedules rebalancing if needed' do
issue_a.update!(relative_position: RelativePositioning::MAX_POSITION)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, nil, project.group.id)
expect(Issues::RebalancingWorker).to receive(:perform_async).with(nil, nil, project.group.id)
run_worker
end
@ -52,7 +52,7 @@ RSpec.describe IssuePlacementWorker do
.with(have_attributes(count: described_class::QUERY_LIMIT))
.and_call_original
expect(described_class).to receive(:perform_async).with(nil, project.id)
expect(Issues::PlacementWorker).to receive(:perform_async).with(nil, project.id)
run_worker
@ -101,7 +101,7 @@ RSpec.describe IssuePlacementWorker do
it 'anticipates the failure to place the issues, and schedules rebalancing' do
allow(Issue).to receive(:move_nulls_to_end) { raise RelativePositioning::NoSpaceLeft }
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, nil, project.group.id)
expect(Issues::RebalancingWorker).to receive(:perform_async).with(nil, nil, project.group.id)
expect(Gitlab::ErrorTracking)
.to receive(:log_exception)
.with(RelativePositioning::NoSpaceLeft, worker_arguments)

View File

@ -35,7 +35,7 @@ RSpec.describe Issues::PlacementWorker do
it 'schedules rebalancing if needed' do
issue_a.update!(relative_position: RelativePositioning::MAX_POSITION)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, nil, project.group.id)
expect(Issues::RebalancingWorker).to receive(:perform_async).with(nil, nil, project.group.id)
run_worker
end

View File

@ -35,6 +35,20 @@ RSpec.describe Issues::RebalancingWorker do
described_class.new.perform # all arguments are nil
end
it 'does not schedule a new rebalance if it finished under 1h ago' do
container_type = arguments.second.present? ? ::Gitlab::Issues::Rebalancing::State::PROJECT : ::Gitlab::Issues::Rebalancing::State::NAMESPACE
container_id = arguments.second || arguments.third
Gitlab::Redis::SharedState.with do |redis|
redis.set(::Gitlab::Issues::Rebalancing::State.send(:recently_finished_key, container_type, container_id), true)
end
expect(Issues::RelativePositionRebalancingService).not_to receive(:new)
expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
described_class.new.perform(*arguments)
end
end
shared_examples 'safely handles non-existent ids' do

View File

@ -10,15 +10,15 @@ RSpec.describe Issues::RescheduleStuckIssueRebalancesWorker, :clean_gitlab_redis
describe '#perform' do
it 'does not schedule a rebalance' do
expect(IssueRebalancingWorker).not_to receive(:perform_async)
expect(Issues::RebalancingWorker).not_to receive(:perform_async)
worker.perform
end
it 'schedules a rebalance in case there are any rebalances started' do
expect(::Gitlab::Issues::Rebalancing::State).to receive(:fetch_rebalancing_groups_and_projects).and_return([[group.id], [project.id]])
expect(IssueRebalancingWorker).to receive(:bulk_perform_async).with([[nil, nil, group.id]]).once
expect(IssueRebalancingWorker).to receive(:bulk_perform_async).with([[nil, project.id, nil]]).once
expect(Issues::RebalancingWorker).to receive(:bulk_perform_async).with([[nil, nil, group.id]]).once
expect(Issues::RebalancingWorker).to receive(:bulk_perform_async).with([[nil, project.id, nil]]).once
worker.perform
end