Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-18 18:10:13 +00:00
parent c8cc2fe990
commit dd6e32bf47
114 changed files with 790 additions and 7349 deletions

View File

@ -3,16 +3,12 @@ import Visibility from 'visibilityjs';
import Vue from 'vue';
import AccessorUtilities from '~/lib/utils/accessor';
import initProjectSelectDropdown from '~/project_select';
import initServerlessSurveyBanner from '~/serverless/survey_banner';
import createFlash from '../flash';
import Poll from '../lib/utils/poll';
import { s__, sprintf } from '../locale';
import { s__ } from '../locale';
import PersistentUserCallout from '../persistent_user_callout';
import initSettingsPanels from '../settings_panels';
import Applications from './components/applications.vue';
import RemoveClusterConfirmation from './components/remove_cluster_confirmation.vue';
import { APPLICATION_STATUS, CROSSPLANE, KNATIVE } from './constants';
import eventHub from './event_hub';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
@ -20,46 +16,20 @@ const Environments = () => import('ee_component/clusters/components/environments
Vue.use(GlToast);
/**
* Cluster page has 2 separate parts:
* Toggle button and applications section
*
* - Polling status while creating or scheduled
* - Update status area with the response result
*/
export default class Clusters {
constructor() {
const {
statusPath,
installHelmPath,
installIngressPath,
installCertManagerPath,
installRunnerPath,
installJupyterPath,
installKnativePath,
updateKnativePath,
installElasticStackPath,
installCrossplanePath,
installPrometheusPath,
managePrometheusPath,
clusterEnvironmentsPath,
hasRbac,
providerType,
preInstalledKnative,
clusterType,
clusterStatus,
clusterStatusReason,
helpPath,
helmHelpPath,
ingressHelpPath,
ingressDnsHelpPath,
environmentsHelpPath,
clustersHelpPath,
deployBoardsHelpPath,
cloudRunHelpPath,
clusterId,
ciliumHelpPath,
} = document.querySelector('.js-edit-cluster-form').dataset;
this.clusterId = clusterId;
@ -69,38 +39,19 @@ export default class Clusters {
this.store = new ClustersStore();
this.store.setHelpPaths({
helpPath,
helmHelpPath,
ingressHelpPath,
ingressDnsHelpPath,
environmentsHelpPath,
clustersHelpPath,
deployBoardsHelpPath,
cloudRunHelpPath,
ciliumHelpPath,
});
this.store.setManagePrometheusPath(managePrometheusPath);
this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason);
this.store.updateProviderType(providerType);
this.store.updatePreInstalledKnative(preInstalledKnative);
this.store.updateRbac(hasRbac);
this.service = new ClustersService({
endpoint: statusPath,
installHelmEndpoint: installHelmPath,
installIngressEndpoint: installIngressPath,
installCertManagerEndpoint: installCertManagerPath,
installCrossplaneEndpoint: installCrossplanePath,
installRunnerEndpoint: installRunnerPath,
installPrometheusEndpoint: installPrometheusPath,
installJupyterEndpoint: installJupyterPath,
installKnativeEndpoint: installKnativePath,
updateKnativeEndpoint: updateKnativePath,
installElasticStackEndpoint: installElasticStackPath,
clusterEnvironmentsEndpoint: clusterEnvironmentsPath,
});
this.installApplication = this.installApplication.bind(this);
this.errorContainer = document.querySelector('.js-cluster-error');
this.successContainer = document.querySelector('.js-cluster-success');
this.creatingContainer = document.querySelector('.js-cluster-creating');
@ -109,14 +60,12 @@ export default class Clusters {
'.js-cluster-authentication-failure',
);
this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
this.successApplicationContainer = document.querySelector('.js-cluster-application-notice');
this.tokenField = document.querySelector('.js-cluster-token');
initProjectSelectDropdown();
Clusters.initDismissableCallout();
initSettingsPanels();
this.initApplications(clusterType);
this.initEnvironments();
if (clusterEnvironmentsPath && this.environments) {
@ -143,38 +92,6 @@ export default class Clusters {
this.initRemoveClusterActions();
}
initApplications(type) {
const { store } = this;
const el = document.querySelector('#js-cluster-applications');
this.applications = new Vue({
el,
data() {
return {
state: store.state,
};
},
render(createElement) {
return createElement(Applications, {
props: {
type,
applications: this.state.applications,
helpPath: this.state.helpPath,
helmHelpPath: this.state.helmHelpPath,
ingressHelpPath: this.state.ingressHelpPath,
managePrometheusPath: this.state.managePrometheusPath,
ingressDnsHelpPath: this.state.ingressDnsHelpPath,
cloudRunHelpPath: this.state.cloudRunHelpPath,
providerType: this.state.providerType,
preInstalledKnative: this.state.preInstalledKnative,
rbac: this.state.rbac,
ciliumHelpPath: this.state.ciliumHelpPath,
},
});
},
});
}
initEnvironments() {
const { store } = this;
const el = document.querySelector('#js-cluster-environments');
@ -242,30 +159,11 @@ export default class Clusters {
}
addListeners() {
eventHub.$on('installApplication', this.installApplication);
eventHub.$on('updateApplication', (data) => this.updateApplication(data));
eventHub.$on('saveKnativeDomain', (data) => this.saveKnativeDomain(data));
eventHub.$on('setKnativeDomain', (data) => this.setKnativeDomain(data));
eventHub.$on('uninstallApplication', (data) => this.uninstallApplication(data));
eventHub.$on('setCrossplaneProviderStack', (data) => this.setCrossplaneProviderStack(data));
// Add event listener to all the banner close buttons
this.addBannerCloseHandler(this.unreachableContainer, 'unreachable');
this.addBannerCloseHandler(this.authenticationFailureContainer, 'authentication_failure');
}
removeListeners() {
eventHub.$off('installApplication', this.installApplication);
eventHub.$off('updateApplication', this.updateApplication);
// eslint-disable-next-line @gitlab/no-global-event-off
eventHub.$off('saveKnativeDomain');
// eslint-disable-next-line @gitlab/no-global-event-off
eventHub.$off('setKnativeDomain');
// eslint-disable-next-line @gitlab/no-global-event-off
eventHub.$off('setCrossplaneProviderStack');
// eslint-disable-next-line @gitlab/no-global-event-off
eventHub.$off('uninstallApplication');
}
initPolling(method, successCallback, errorCallback) {
this.poll = new Poll({
resource: this.service,
@ -305,16 +203,10 @@ export default class Clusters {
handleClusterStatusSuccess(data) {
const prevStatus = this.store.state.status;
const prevApplicationMap = { ...this.store.state.applications };
this.store.updateStateFromServer(data.data);
this.checkForNewInstalls(prevApplicationMap, this.store.state.applications);
this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason);
if (this.store.state.applications[KNATIVE]?.status === APPLICATION_STATUS.INSTALLED) {
initServerlessSurveyBanner();
}
}
hideAll() {
@ -325,31 +217,6 @@ export default class Clusters {
this.authenticationFailureContainer.classList.add('hidden');
}
checkForNewInstalls(prevApplicationMap, newApplicationMap) {
const appTitles = Object.keys(newApplicationMap)
.filter(
(appId) =>
newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED &&
prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED &&
prevApplicationMap[appId].status !== null,
)
.map((appId) => newApplicationMap[appId].title);
if (appTitles.length > 0) {
const text = sprintf(
s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'),
{
appList: appTitles.join(', '),
},
);
createFlash({
message: text,
type: 'notice',
parent: this.successApplicationContainer,
});
}
}
setBannerDismissedState(status, isDismissed) {
if (AccessorUtilities.isLocalStorageAccessSafe()) {
window.localStorage.setItem(this.clusterBannerDismissedKey, `${status}_${isDismissed}`);
@ -422,91 +289,9 @@ export default class Clusters {
}
}
installApplication({ id: appId, params }) {
return Clusters.validateInstallation(appId, params)
.then(() => {
this.store.updateAppProperty(appId, 'requestReason', null);
this.store.updateAppProperty(appId, 'statusReason', null);
this.store.installApplication(appId);
// eslint-disable-next-line promise/no-nesting
this.service.installApplication(appId, params).catch(() => {
this.store.notifyInstallFailure(appId);
this.store.updateAppProperty(
appId,
'requestReason',
s__('ClusterIntegration|Request to begin installing failed'),
);
});
})
.catch((error) => this.store.updateAppProperty(appId, 'validationError', error));
}
static validateInstallation(appId, params) {
return new Promise((resolve, reject) => {
if (appId === CROSSPLANE && !params.stack) {
reject(s__('ClusterIntegration|Select a stack to install Crossplane.'));
return;
}
if (appId === KNATIVE && !params.hostname && !params.pages_domain_id) {
reject(s__('ClusterIntegration|You must specify a domain before you can install Knative.'));
return;
}
resolve();
});
}
uninstallApplication({ id: appId }) {
this.store.updateAppProperty(appId, 'requestReason', null);
this.store.updateAppProperty(appId, 'statusReason', null);
this.store.uninstallApplication(appId);
return this.service.uninstallApplication(appId).catch(() => {
this.store.notifyUninstallFailure(appId);
this.store.updateAppProperty(
appId,
'requestReason',
s__('ClusterIntegration|Request to begin uninstalling failed'),
);
});
}
updateApplication({ id: appId, params }) {
this.store.updateApplication(appId);
this.service.installApplication(appId, params).catch(() => {
this.store.notifyUpdateFailure(appId);
});
}
saveKnativeDomain(data) {
const appId = data.id;
this.store.updateApplication(appId);
this.service.updateApplication(appId, data.params).catch(() => {
this.store.notifyUpdateFailure(appId);
});
}
setKnativeDomain({ id: appId, domain, domainId }) {
this.store.updateAppProperty(appId, 'isEditingDomain', true);
this.store.updateAppProperty(appId, 'hostname', domain);
this.store.updateAppProperty(appId, 'pagesDomain', domainId ? { id: domainId, domain } : null);
this.store.updateAppProperty(appId, 'validationError', null);
}
setCrossplaneProviderStack(data) {
const appId = data.id;
this.store.updateAppProperty(appId, 'stack', data.stack.code);
this.store.updateAppProperty(appId, 'validationError', null);
}
destroy() {
this.destroyed = true;
this.removeListeners();
if (this.poll) {
this.poll.stop();
}
@ -514,7 +299,5 @@ export default class Clusters {
if (this.environments) {
this.environments.$destroy();
}
this.applications.$destroy();
}
}

View File

@ -1,478 +0,0 @@
<script>
import { GlLink, GlModalDirective, GlSprintf, GlButton, GlAlert } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
import identicon from '../../vue_shared/components/identicon.vue';
import { APPLICATION_STATUS, ELASTIC_STACK } from '../constants';
import eventHub from '../event_hub';
import UninstallApplicationButton from './uninstall_application_button.vue';
import UninstallApplicationConfirmationModal from './uninstall_application_confirmation_modal.vue';
import UpdateApplicationConfirmationModal from './update_application_confirmation_modal.vue';
export default {
components: {
GlButton,
identicon,
GlLink,
GlAlert,
GlSprintf,
UninstallApplicationButton,
UninstallApplicationConfirmationModal,
UpdateApplicationConfirmationModal,
},
directives: {
GlModalDirective,
},
props: {
id: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
titleLink: {
type: String,
required: false,
default: '',
},
manageLink: {
type: String,
required: false,
default: '',
},
logoUrl: {
type: String,
required: false,
default: '',
},
disabled: {
type: Boolean,
required: false,
default: false,
},
installable: {
type: Boolean,
required: false,
default: true,
},
uninstallable: {
type: Boolean,
required: false,
default: false,
},
status: {
type: String,
required: false,
default: '',
},
statusReason: {
type: String,
required: false,
default: '',
},
requestReason: {
type: String,
required: false,
default: '',
},
installed: {
type: Boolean,
required: false,
default: false,
},
installFailed: {
type: Boolean,
required: false,
default: false,
},
version: {
type: String,
required: false,
default: '',
},
chartRepo: {
type: String,
required: false,
default: '',
},
updateAvailable: {
type: Boolean,
required: false,
},
updateable: {
type: Boolean,
default: true,
required: false,
},
updateSuccessful: {
type: Boolean,
required: false,
default: false,
},
updateFailed: {
type: Boolean,
required: false,
default: false,
},
uninstallFailed: {
type: Boolean,
required: false,
default: false,
},
uninstallSuccessful: {
type: Boolean,
required: false,
default: false,
},
installApplicationRequestParams: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
isUnknownStatus() {
return !this.isKnownStatus && this.status !== null;
},
isKnownStatus() {
return Object.values(APPLICATION_STATUS).includes(this.status);
},
isInstalling() {
return this.status === APPLICATION_STATUS.INSTALLING;
},
isExternallyInstalled() {
return this.status === APPLICATION_STATUS.EXTERNALLY_INSTALLED;
},
canInstall() {
return (
this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
this.status === APPLICATION_STATUS.INSTALLABLE ||
this.status === APPLICATION_STATUS.UNINSTALLED ||
this.isUnknownStatus
);
},
hasLogo() {
return Boolean(this.logoUrl);
},
identiconId() {
// generate a deterministic integer id for the identicon background
return this.id.charCodeAt(0);
},
rowJsClass() {
return `js-cluster-application-row-${this.id}`;
},
displayUninstallButton() {
return this.installed && this.uninstallable;
},
displayInstallButton() {
return !this.installed || !this.uninstallable;
},
installButtonLoading() {
return !this.status || this.isInstalling;
},
installButtonDisabled() {
// Applications installed through the management project can
// only be installed through the CI pipeline. Installation should
// be disable in all states.
if (!this.installable) return true;
// Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but
// we already made a request to install and are just waiting for the real-time
// to sync up.
if (this.isInstalling) return true;
if (!this.isKnownStatus) return false;
return (
this.status !== APPLICATION_STATUS.INSTALLABLE && this.status !== APPLICATION_STATUS.ERROR
);
},
installButtonLabel() {
let label;
if (this.canInstall) {
label = __('Install');
} else if (this.isInstalling) {
label = __('Installing');
} else if (this.installed) {
label = __('Installed');
} else if (this.isExternallyInstalled) {
label = __('Externally installed');
}
return label;
},
buttonGridCellClass() {
return this.showManageButton || this.status === APPLICATION_STATUS.EXTERNALLY_INSTALLED
? 'section-25'
: 'section-15';
},
showManageButton() {
return this.manageLink && this.status === APPLICATION_STATUS.INSTALLED;
},
manageButtonLabel() {
return __('Manage');
},
hasError() {
return this.installFailed || this.uninstallFailed;
},
generalErrorDescription() {
let errorDescription;
if (this.installFailed) {
errorDescription = s__('ClusterIntegration|Something went wrong while installing %{title}');
} else if (this.uninstallFailed) {
errorDescription = s__(
'ClusterIntegration|Something went wrong while uninstalling %{title}',
);
}
return sprintf(errorDescription, { title: this.title });
},
updateFailureDescription() {
return s__('ClusterIntegration|Update failed. Please check the logs and try again.');
},
updateSuccessDescription() {
return sprintf(s__('ClusterIntegration|%{title} updated successfully.'), {
title: this.title,
});
},
updateButtonLabel() {
let label;
if (this.updateAvailable && !this.updateFailed && !this.isUpdating) {
label = __('Update');
} else if (this.isUpdating) {
label = __('Updating');
} else if (this.updateFailed) {
label = __('Retry update');
}
return label;
},
updatingNeedsConfirmation() {
if (this.version) {
const majorVersion = parseInt(this.version.split('.')[0], 10);
if (!Number.isNaN(majorVersion)) {
return this.id === ELASTIC_STACK && majorVersion < 3;
}
}
return false;
},
isUpdating() {
// Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend
return this.status === APPLICATION_STATUS.UPDATING;
},
shouldShowUpdateDetails() {
// This method only returns true when;
// Update was successful OR Update failed
// AND new update is unavailable AND version information is present.
return (this.updateSuccessful || this.updateFailed) && !this.updateAvailable && this.version;
},
uninstallSuccessDescription() {
return sprintf(s__('ClusterIntegration|%{title} uninstalled successfully.'), {
title: this.title,
});
},
updateModalId() {
return `update-${this.id}`;
},
uninstallModalId() {
return `uninstall-${this.id}`;
},
},
watch: {
updateSuccessful(updateSuccessful) {
if (updateSuccessful) {
this.$toast.show(this.updateSuccessDescription);
}
},
uninstallSuccessful(uninstallSuccessful) {
if (uninstallSuccessful) {
this.$toast.show(this.uninstallSuccessDescription);
}
},
},
methods: {
installClicked() {
if (this.disabled || this.installButtonDisabled) return;
eventHub.$emit('installApplication', {
id: this.id,
params: this.installApplicationRequestParams,
});
},
updateConfirmed() {
if (this.isUpdating) return;
eventHub.$emit('updateApplication', {
id: this.id,
params: this.installApplicationRequestParams,
});
},
uninstallConfirmed() {
eventHub.$emit('uninstallApplication', {
id: this.id,
});
},
},
};
</script>
<template>
<div
:class="[
rowJsClass,
installed && 'cluster-application-installed',
disabled && 'cluster-application-disabled',
]"
class="cluster-application-row gl-responsive-table-row gl-responsive-table-row-col-span"
:data-qa-selector="id"
>
<div class="gl-responsive-table-row-layout" role="row">
<div class="table-section gl-mr-3 section-align-top" role="gridcell">
<img
v-if="hasLogo"
:src="logoUrl"
:alt="`${title} logo`"
class="cluster-application-logo avatar s40"
/>
<identicon v-else :entity-id="identiconId" :entity-name="title" size-class="s40" />
</div>
<div class="table-section cluster-application-description section-wrap" role="gridcell">
<strong>
<a
v-if="titleLink"
:href="titleLink"
target="_blank"
rel="noopener noreferrer"
class="js-cluster-application-title"
>{{ title }}</a
>
<span v-else class="js-cluster-application-title">{{ title }}</span>
</strong>
<slot name="installed-via"></slot>
<div>
<slot name="description"></slot>
</div>
<div v-if="hasError" class="cluster-application-error text-danger gl-mt-3">
<p class="js-cluster-application-general-error-message gl-mb-0">
{{ generalErrorDescription }}
</p>
<ul v-if="statusReason || requestReason">
<li v-if="statusReason" class="js-cluster-application-status-error-message">
{{ statusReason }}
</li>
<li v-if="requestReason" class="js-cluster-application-request-error-message">
{{ requestReason }}
</li>
</ul>
</div>
<div v-if="updateable">
<div
v-if="shouldShowUpdateDetails"
class="form-text text-muted label p-0 js-cluster-application-update-details"
>
<template v-if="updateFailed">{{ __('Update failed') }}</template>
<template v-else-if="isUpdating">{{ __('Updating') }}</template>
<template v-else>
<gl-sprintf :message="__('Updated to %{linkStart}chart v%{linkEnd}')">
<template #link="{ content }">
<gl-link
:href="chartRepo"
target="_blank"
class="js-cluster-application-update-version"
>{{ content }}{{ version }}</gl-link
>
</template>
</gl-sprintf>
</template>
</div>
<gl-alert
v-if="updateFailed && !isUpdating"
variant="danger"
:dismissible="false"
class="gl-mt-3 gl-mb-0 js-cluster-application-update-details"
>
{{ updateFailureDescription }}
</gl-alert>
<template v-if="updateAvailable || updateFailed || isUpdating">
<template v-if="updatingNeedsConfirmation">
<gl-button
v-gl-modal-directive="updateModalId"
class="js-cluster-application-update-button mt-2"
variant="info"
category="primary"
:loading="isUpdating"
:disabled="isUpdating"
data-qa-selector="update_button_with_confirmation"
:data-qa-application="id"
>
{{ updateButtonLabel }}
</gl-button>
<update-application-confirmation-modal
:application="id"
:application-title="title"
@confirm="updateConfirmed()"
/>
</template>
<gl-button
v-else
class="js-cluster-application-update-button mt-2"
variant="info"
category="primary"
:loading="isUpdating"
:disabled="isUpdating"
data-qa-selector="update_button"
:data-qa-application="id"
@click="updateConfirmed"
>
{{ updateButtonLabel }}
</gl-button>
</template>
</div>
</div>
<div
:class="[buttonGridCellClass, 'table-section', 'table-button-footer', 'section-align-top']"
role="gridcell"
>
<div v-if="showManageButton" class="btn-group table-action-buttons">
<a :href="manageLink" :class="{ disabled: disabled }" class="btn">{{
manageButtonLabel
}}</a>
</div>
<div class="btn-group table-action-buttons">
<gl-button
v-if="displayInstallButton"
:loading="installButtonLoading"
:disabled="disabled || installButtonDisabled"
class="js-cluster-application-install-button"
variant="default"
data-qa-selector="install_button"
:data-qa-application="id"
@click="installClicked"
>
{{ installButtonLabel }}
</gl-button>
<uninstall-application-button
v-if="displayUninstallButton"
v-gl-modal-directive="uninstallModalId"
:status="status"
data-qa-selector="uninstall_button"
:data-qa-application="id"
class="js-cluster-application-uninstall-button"
/>
<uninstall-application-confirmation-modal
:application="id"
:application-title="title"
@confirm="uninstallConfirmed()"
/>
</div>
</div>
</div>
</div>
</template>

View File

@ -1,662 +0,0 @@
<script>
import { GlLoadingIcon, GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
import certManagerLogo from 'images/cluster_app_logos/cert_manager.png';
import crossplaneLogo from 'images/cluster_app_logos/crossplane.png';
import elasticStackLogo from 'images/cluster_app_logos/elastic_stack.png';
import gitlabLogo from 'images/cluster_app_logos/gitlab.png';
import helmLogo from 'images/cluster_app_logos/helm.png';
import jupyterhubLogo from 'images/cluster_app_logos/jupyterhub.png';
import knativeLogo from 'images/cluster_app_logos/knative.png';
import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png';
import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import eventHub from '~/clusters/event_hub';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { CLUSTER_TYPE, PROVIDER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
import applicationRow from './application_row.vue';
import CrossplaneProviderStack from './crossplane_provider_stack.vue';
import KnativeDomainEditor from './knative_domain_editor.vue';
export default {
components: {
applicationRow,
clipboardButton,
GlLoadingIcon,
GlSprintf,
GlLink,
KnativeDomainEditor,
CrossplaneProviderStack,
GlAlert,
},
props: {
type: {
type: String,
required: false,
default: CLUSTER_TYPE.PROJECT,
},
applications: {
type: Object,
required: false,
default: () => ({}),
},
helpPath: {
type: String,
required: false,
default: '',
},
helmHelpPath: {
type: String,
required: false,
default: '',
},
ingressHelpPath: {
type: String,
required: false,
default: '',
},
ingressDnsHelpPath: {
type: String,
required: false,
default: '',
},
cloudRunHelpPath: {
type: String,
required: false,
default: '',
},
managePrometheusPath: {
type: String,
required: false,
default: '',
},
providerType: {
type: String,
required: false,
default: '',
},
preInstalledKnative: {
type: Boolean,
required: false,
default: false,
},
rbac: {
type: Boolean,
required: false,
default: false,
},
ciliumHelpPath: {
type: String,
required: false,
default: '',
},
},
computed: {
ingressId() {
return INGRESS;
},
ingressInstalled() {
return this.applications.ingress.status === APPLICATION_STATUS.INSTALLED;
},
ingressExternalEndpoint() {
return this.applications.ingress.externalIp || this.applications.ingress.externalHostname;
},
certManagerInstalled() {
return this.applications.cert_manager.status === APPLICATION_STATUS.INSTALLED;
},
jupyterInstalled() {
return this.applications.jupyter.status === APPLICATION_STATUS.INSTALLED;
},
jupyterHostname() {
return this.applications.jupyter.hostname;
},
knative() {
return this.applications.knative;
},
crossplane() {
return this.applications.crossplane;
},
cloudRun() {
return this.providerType === PROVIDER_TYPE.GCP && this.preInstalledKnative;
},
ingress() {
return this.applications.ingress;
},
},
methods: {
saveKnativeDomain() {
eventHub.$emit('saveKnativeDomain', {
id: 'knative',
params: {
hostname: this.applications.knative.hostname,
pages_domain_id: this.applications.knative.pagesDomain?.id,
},
});
},
setKnativeDomain({ domainId, domain }) {
eventHub.$emit('setKnativeDomain', {
id: 'knative',
domainId,
domain,
});
},
setCrossplaneProviderStack(stack) {
eventHub.$emit('setCrossplaneProviderStack', {
id: 'crossplane',
stack,
});
},
},
logos: {
gitlabLogo,
helmLogo,
jupyterhubLogo,
kubernetesLogo,
certManagerLogo,
crossplaneLogo,
knativeLogo,
prometheusLogo,
elasticStackLogo,
},
};
</script>
<template>
<section id="cluster-applications">
<p class="gl-mb-0">
{{
s__(`ClusterIntegration|Choose which applications to install on your Kubernetes cluster.`)
}}
<gl-link :href="helpPath">{{ __('More information') }}</gl-link>
</p>
<div class="cluster-application-list gl-mt-3">
<application-row
v-if="applications.helm.installed || applications.helm.uninstalling"
id="helm"
:logo-url="$options.logos.helmLogo"
:title="applications.helm.title"
:status="applications.helm.status"
:status-reason="applications.helm.statusReason"
:request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason"
:installed="applications.helm.installed"
:install-failed="applications.helm.installFailed"
:uninstallable="applications.helm.uninstallable"
:uninstall-successful="applications.helm.uninstallSuccessful"
:uninstall-failed="applications.helm.uninstallFailed"
title-link="https://v2.helm.sh/"
>
<template #description>
<p>
{{
s__(`ClusterIntegration|Can be safely removed. Prior to GitLab
13.2, GitLab used a remote Tiller server to manage the
applications. GitLab no longer uses this server.
Uninstalling this server will not affect your other
applications. This row will disappear afterwards.`)
}}
<gl-link :href="helmHelpPath">{{ __('More information') }}</gl-link>
</p>
</template>
</application-row>
<application-row
:id="ingressId"
:logo-url="$options.logos.kubernetesLogo"
:title="applications.ingress.title"
:status="applications.ingress.status"
:status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason"
:installed="applications.ingress.installed"
:install-failed="applications.ingress.installFailed"
:uninstallable="applications.ingress.uninstallable"
:uninstall-successful="applications.ingress.uninstallSuccessful"
:uninstall-failed="applications.ingress.uninstallFailed"
:updateable="false"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
>
<template #description>
<p>
{{
s__(`ClusterIntegration|Ingress gives you a way to route
requests to services based on the request host or path,
centralizing a number of services into a single entrypoint.`)
}}
</p>
<template v-if="ingressInstalled">
<div class="form-group">
<label for="ingress-endpoint">{{ s__('ClusterIntegration|Ingress Endpoint') }}</label>
<div class="input-group">
<template v-if="ingressExternalEndpoint">
<input
id="ingress-endpoint"
:value="ingressExternalEndpoint"
type="text"
class="form-control js-endpoint"
readonly
/>
<span class="input-group-append">
<clipboard-button
:text="ingressExternalEndpoint"
:title="s__('ClusterIntegration|Copy Ingress Endpoint')"
class="input-group-text js-clipboard-btn"
/>
</span>
</template>
<template v-else>
<input type="text" class="form-control js-endpoint" readonly />
<gl-loading-icon
class="position-absolute align-self-center ml-2 js-ingress-ip-loading-icon"
/>
</template>
</div>
<p class="form-text text-muted">
{{
s__(`ClusterIntegration|Point a wildcard DNS to this
generated endpoint in order to access
your application after it has been deployed.`)
}}
<gl-link :href="ingressDnsHelpPath" target="_blank">
{{ __('More information') }}
</gl-link>
</p>
</div>
<p v-if="!ingressExternalEndpoint" class="settings-message js-no-endpoint-message">
{{
s__(`ClusterIntegration|The endpoint is in
the process of being assigned. Please check your Kubernetes
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}}
<gl-link :href="ingressDnsHelpPath" target="_blank">
{{ __('More information') }}
</gl-link>
</p>
</template>
<template v-else>
<gl-alert variant="info" :dismissible="false">
<span data-testid="ingressCostWarning">
<gl-sprintf
:message="
s__(
'ClusterIntegration|Installing Ingress may incur additional costs. Learn more about %{linkStart}pricing%{linkEnd}.',
)
"
>
<template #link="{ content }">
<gl-link href="https://cloud.google.com/compute/pricing#lb" target="_blank">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</span>
</gl-alert>
</template>
</template>
</application-row>
<application-row
id="cert_manager"
:logo-url="$options.logos.certManagerLogo"
:title="applications.cert_manager.title"
:status="applications.cert_manager.status"
:status-reason="applications.cert_manager.statusReason"
:request-status="applications.cert_manager.requestStatus"
:request-reason="applications.cert_manager.requestReason"
:installed="applications.cert_manager.installed"
:install-failed="applications.cert_manager.installFailed"
:install-application-request-params="{ email: applications.cert_manager.email }"
:uninstallable="applications.cert_manager.uninstallable"
:uninstall-successful="applications.cert_manager.uninstallSuccessful"
:uninstall-failed="applications.cert_manager.uninstallFailed"
title-link="https://cert-manager.readthedocs.io/en/latest/#"
>
<template #description>
<p data-testid="certManagerDescription">
<gl-sprintf
:message="
s__(`ClusterIntegration|Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates.
Installing Cert-Manager on your cluster will issue a certificate by %{linkStart}Let's Encrypt%{linkEnd} and ensure that certificates
are valid and up-to-date.`)
"
>
<template #link="{ content }">
<gl-link href="https://letsencrypt.org/" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<div class="form-group">
<label for="cert-manager-issuer-email">
{{ s__('ClusterIntegration|Issuer Email') }}
</label>
<div class="input-group">
<!-- eslint-disable vue/no-mutating-props -->
<input
id="cert-manager-issuer-email"
v-model="applications.cert_manager.email"
:readonly="certManagerInstalled"
type="text"
class="form-control js-email"
/>
<!-- eslint-enable vue/no-mutating-props -->
</div>
<p class="form-text text-muted">
{{
s__(`ClusterIntegration|Issuers represent a certificate authority.
You must provide an email address for your Issuer.`)
}}
<gl-link
href="http://docs.cert-manager.io/en/latest/reference/issuers.html?highlight=email"
target="_blank"
>{{ __('More information') }}</gl-link
>
</p>
</div>
</template>
</application-row>
<application-row
id="prometheus"
:logo-url="$options.logos.prometheusLogo"
:title="applications.prometheus.title"
:manage-link="managePrometheusPath"
:status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason"
:request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason"
:installed="applications.prometheus.installed"
:install-failed="applications.prometheus.installFailed"
:uninstallable="applications.prometheus.uninstallable"
:uninstall-successful="applications.prometheus.uninstallSuccessful"
:uninstall-failed="applications.prometheus.uninstallFailed"
title-link="https://prometheus.io/docs/introduction/overview/"
>
<template #description>
<span data-testid="prometheusDescription">
<gl-sprintf
:message="
s__(`ClusterIntegration|Prometheus is an open-source monitoring system
with %{linkStart}GitLab Integration%{linkEnd} to monitor deployed applications.`)
"
>
<template #link="{ content }">
<gl-link
href="https://docs.gitlab.com/ee/user/project/integrations/prometheus.html"
target="_blank"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</span>
</template>
</application-row>
<application-row
id="runner"
:logo-url="$options.logos.gitlabLogo"
:title="applications.runner.title"
:status="applications.runner.status"
:status-reason="applications.runner.statusReason"
:request-status="applications.runner.requestStatus"
:request-reason="applications.runner.requestReason"
:version="applications.runner.version"
:chart-repo="applications.runner.chartRepo"
:update-available="applications.runner.updateAvailable"
:installed="applications.runner.installed"
:install-failed="applications.runner.installFailed"
:update-successful="applications.runner.updateSuccessful"
:update-failed="applications.runner.updateFailed"
:uninstallable="applications.runner.uninstallable"
:uninstall-successful="applications.runner.uninstallSuccessful"
:uninstall-failed="applications.runner.uninstallFailed"
title-link="https://docs.gitlab.com/runner/"
>
<template #description>
{{
s__(`ClusterIntegration|GitLab Runner connects to the
repository and executes CI/CD jobs,
pushing results back and deploying
applications to production.`)
}}
</template>
</application-row>
<application-row
id="crossplane"
:logo-url="$options.logos.crossplaneLogo"
:title="applications.crossplane.title"
:status="applications.crossplane.status"
:status-reason="applications.crossplane.statusReason"
:request-status="applications.crossplane.requestStatus"
:request-reason="applications.crossplane.requestReason"
:installed="applications.crossplane.installed"
:install-failed="applications.crossplane.installFailed"
:uninstallable="applications.crossplane.uninstallable"
:uninstall-successful="applications.crossplane.uninstallSuccessful"
:uninstall-failed="applications.crossplane.uninstallFailed"
:install-application-request-params="{ stack: applications.crossplane.stack }"
title-link="https://crossplane.io"
>
<template #description>
<p data-testid="crossplaneDescription">
<gl-sprintf
:message="
s__(
`ClusterIntegration|Crossplane enables declarative provisioning of managed services from your cloud of choice using %{codeStart}kubectl%{codeEnd} or %{linkStart}GitLab Integration%{linkEnd}.
Crossplane runs inside your Kubernetes cluster and supports secure connectivity and secrets management between app containers and the cloud services they depend on.`,
)
"
>
<template #code="{ content }">
<code>{{ content }}</code>
</template>
<template #link="{ content }">
<gl-link
href="https://docs.gitlab.com/ee/user/clusters/applications.html#crossplane"
target="_blank"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</p>
<div class="form-group">
<CrossplaneProviderStack :crossplane="crossplane" @set="setCrossplaneProviderStack" />
</div>
</template>
</application-row>
<application-row
id="jupyter"
:logo-url="$options.logos.jupyterhubLogo"
:title="applications.jupyter.title"
:status="applications.jupyter.status"
:status-reason="applications.jupyter.statusReason"
:request-status="applications.jupyter.requestStatus"
:request-reason="applications.jupyter.requestReason"
:installed="applications.jupyter.installed"
:install-failed="applications.jupyter.installFailed"
:uninstallable="applications.jupyter.uninstallable"
:uninstall-successful="applications.jupyter.uninstallSuccessful"
:uninstall-failed="applications.jupyter.uninstallFailed"
:install-application-request-params="{ hostname: applications.jupyter.hostname }"
title-link="https://jupyterhub.readthedocs.io/en/stable/"
>
<template #description>
<p>
{{
s__(`ClusterIntegration|JupyterHub, a multi-user Hub, spawns,
manages, and proxies multiple instances of the single-user
Jupyter notebook server. JupyterHub can be used to serve
notebooks to a class of students, a corporate data science group,
or a scientific research group.`)
}}
<gl-sprintf
:message="
s__(
'ClusterIntegration|%{boldStart}Note:%{boldEnd} Requires Ingress to be installed.',
)
"
>
<template #bold="{ content }">
<b>{{ content }}</b>
</template>
</gl-sprintf>
</p>
<template v-if="ingressExternalEndpoint">
<div class="form-group">
<label for="jupyter-hostname">{{ s__('ClusterIntegration|Jupyter Hostname') }}</label>
<div class="input-group">
<!-- eslint-disable vue/no-mutating-props -->
<input
id="jupyter-hostname"
v-model="applications.jupyter.hostname"
:readonly="jupyterInstalled"
type="text"
class="form-control js-hostname"
/>
<!-- eslint-enable vue/no-mutating-props -->
<span class="input-group-append">
<clipboard-button
:text="jupyterHostname"
:title="s__('ClusterIntegration|Copy Jupyter Hostname')"
class="js-clipboard-btn"
/>
</span>
</div>
<p v-if="ingressInstalled" class="form-text text-muted">
{{
s__(`ClusterIntegration|Replace this with your own hostname if you want.
If you do so, point hostname to Ingress IP Address from above.`)
}}
<gl-link :href="ingressDnsHelpPath" target="_blank">
{{ __('More information') }}
</gl-link>
</p>
</div>
</template>
</template>
</application-row>
<application-row
id="knative"
:logo-url="$options.logos.knativeLogo"
:title="applications.knative.title"
:status="applications.knative.status"
:status-reason="applications.knative.statusReason"
:request-status="applications.knative.requestStatus"
:request-reason="applications.knative.requestReason"
:installed="applications.knative.installed"
:install-failed="applications.knative.installFailed"
:install-application-request-params="{
hostname: applications.knative.hostname,
pages_domain_id: applications.knative.pagesDomain && applications.knative.pagesDomain.id,
}"
:uninstallable="applications.knative.uninstallable"
:uninstall-successful="applications.knative.uninstallSuccessful"
:uninstall-failed="applications.knative.uninstallFailed"
:updateable="false"
v-bind="applications.knative"
title-link="https://github.com/knative/docs"
>
<template #description>
<gl-alert v-if="!rbac" variant="info" class="rbac-notice gl-my-3" :dismissible="false">
{{
s__(`ClusterIntegration|You must have an RBAC-enabled cluster
to install Knative.`)
}}
<gl-link :href="helpPath" target="_blank">{{ __('More information') }}</gl-link>
</gl-alert>
<p>
{{
s__(`ClusterIntegration|Knative extends Kubernetes to provide
a set of middleware components that are essential to build modern,
source-centric, and container-based applications that can run
anywhere: on premises, in the cloud, or even in a third-party data center.`)
}}
</p>
<knative-domain-editor
v-if="(knative.installed || rbac) && !preInstalledKnative"
:knative="knative"
:ingress-dns-help-path="ingressDnsHelpPath"
@save="saveKnativeDomain"
@set="setKnativeDomain"
/>
</template>
<template v-if="cloudRun" #installed-via>
<span data-testid="installed-via">
<gl-sprintf
:message="s__('ClusterIntegration|installed via %{linkStart}Cloud Run%{linkEnd}')"
>
<template #link="{ content }">
<gl-link :href="cloudRunHelpPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
</template>
</application-row>
<application-row
id="elastic_stack"
:logo-url="$options.logos.elasticStackLogo"
:title="applications.elastic_stack.title"
:status="applications.elastic_stack.status"
:status-reason="applications.elastic_stack.statusReason"
:request-status="applications.elastic_stack.requestStatus"
:request-reason="applications.elastic_stack.requestReason"
:version="applications.elastic_stack.version"
:chart-repo="applications.elastic_stack.chartRepo"
:update-available="applications.elastic_stack.updateAvailable"
:installed="applications.elastic_stack.installed"
:install-failed="applications.elastic_stack.installFailed"
:update-successful="applications.elastic_stack.updateSuccessful"
:update-failed="applications.elastic_stack.updateFailed"
:uninstallable="applications.elastic_stack.uninstallable"
:uninstall-successful="applications.elastic_stack.uninstallSuccessful"
:uninstall-failed="applications.elastic_stack.uninstallFailed"
title-link="https://gitlab.com/gitlab-org/charts/elastic-stack"
>
<template #description>
<p>
{{
s__(
`ClusterIntegration|The elastic stack collects logs from all pods in your cluster`,
)
}}
</p>
</template>
</application-row>
<div class="gl-mt-7 gl-border-1 gl-border-t-solid gl-border-gray-100">
<!-- This empty div serves as a separator. The applications below can be externally installed using a cluster-management project. -->
</div>
<application-row
id="cilium"
:title="applications.cilium.title"
:logo-url="$options.logos.gitlabLogo"
:status="applications.cilium.status"
:status-reason="applications.cilium.statusReason"
:installable="applications.cilium.installable"
:uninstallable="applications.cilium.uninstallable"
:installed="applications.cilium.installed"
:install-failed="applications.cilium.installFailed"
:title-link="ciliumHelpPath"
>
<template #description>
<p data-testid="ciliumDescription">
<gl-sprintf
:message="
s__(
'ClusterIntegration|Protect your clusters with GitLab Container Network Policies by enforcing how pods communicate with each other and other network endpoints. %{linkStart}Learn more about configuring Network Policies here.%{linkEnd}',
)
"
>
<template #link="{ content }">
<gl-link :href="ciliumHelpPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</template>
</application-row>
</div>
</section>
</template>

View File

@ -1,93 +0,0 @@
<script>
import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { s__ } from '../../locale';
export default {
name: 'CrossplaneProviderStack',
components: {
GlDropdown,
GlDropdownItem,
GlIcon,
},
props: {
stacks: {
type: Array,
required: false,
default: () => [
{
name: s__('Google Cloud Platform'),
code: 'gcp',
},
{
name: s__('Amazon Web Services'),
code: 'aws',
},
{
name: s__('Microsoft Azure'),
code: 'azure',
},
{
name: s__('Rook'),
code: 'rook',
},
],
},
crossplane: {
type: Object,
required: true,
},
},
computed: {
dropdownText() {
const result = this.stacks.reduce((map, obj) => {
// eslint-disable-next-line no-param-reassign
map[obj.code] = obj.name;
return map;
}, {});
const { stack } = this.crossplane;
if (stack !== '') {
return result[stack];
}
return s__('Select Stack');
},
validationError() {
return this.crossplane.validationError;
},
},
methods: {
selectStack(stack) {
this.$emit('set', stack);
},
},
};
</script>
<template>
<div>
<label>
{{ s__('ClusterIntegration|Enabled stack') }}
</label>
<gl-dropdown
:disabled="crossplane.installed"
:text="dropdownText"
toggle-class="dropdown-menu-toggle gl-field-error-outline"
class="w-100"
:class="{ 'gl-show-field-errors': validationError }"
>
<gl-dropdown-item v-for="stack in stacks" :key="stack.code" @click="selectStack(stack)">
<span class="ml-1">{{ stack.name }}</span>
</gl-dropdown-item>
</gl-dropdown>
<span v-if="validationError" class="gl-field-error">{{ validationError }}</span>
<p class="form-text text-muted">
{{ s__(`You must select a stack for configuring your cloud provider. Learn more about`) }}
<a
href="https://crossplane.io/docs/master/stacks-guide.html"
target="_blank"
rel="noopener noreferrer"
>{{ __('Crossplane') }}
<gl-icon name="external-link" class="vertical-align-middle" />
</a>
</p>
</div>
</template>

View File

@ -1,232 +0,0 @@
<script>
import {
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
GlSprintf,
GlButton,
GlAlert,
} from '@gitlab/ui';
import { APPLICATION_STATUS } from '~/clusters/constants';
import { __, s__ } from '~/locale';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
export default {
components: {
GlButton,
ClipboardButton,
GlLoadingIcon,
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByType,
GlSprintf,
GlAlert,
},
props: {
knative: {
type: Object,
required: true,
},
ingressDnsHelpPath: {
type: String,
default: '',
required: false,
},
},
data() {
return {
searchQuery: '',
};
},
computed: {
saveButtonDisabled() {
return [UNINSTALLING, UPDATING].includes(this.knative.status);
},
saving() {
return [UPDATING].includes(this.knative.status);
},
saveButtonLabel() {
return this.saving ? __('Saving') : __('Save changes');
},
knativeInstalled() {
return this.knative.installed;
},
knativeExternalEndpoint() {
return this.knative.externalIp || this.knative.externalHostname;
},
knativeUpdateSuccessful() {
return this.knative.updateSuccessful;
},
knativeHostname: {
get() {
return this.knative.hostname;
},
set(hostname) {
this.selectCustomDomain(hostname);
},
},
domainDropdownText() {
return this.knativeHostname || s__('ClusterIntegration|Select existing domain or use new');
},
availableDomains() {
return this.knative.availableDomains || [];
},
filteredDomains() {
const query = this.searchQuery.toLowerCase();
return this.availableDomains.filter(({ domain }) => domain.toLowerCase().includes(query));
},
showDomainsDropdown() {
return this.availableDomains.length > 0;
},
validationError() {
return this.knative.validationError;
},
},
watch: {
knativeUpdateSuccessful(updateSuccessful) {
if (updateSuccessful) {
this.$toast.show(s__('ClusterIntegration|Knative domain name was updated successfully.'));
}
},
},
methods: {
selectDomain({ id, domain }) {
this.$emit('set', { domain, domainId: id });
},
selectCustomDomain(domain) {
this.$emit('set', { domain, domainId: null });
},
},
};
</script>
<template>
<div class="row">
<gl-alert
v-if="knative.updateFailed"
class="gl-mb-5 col-12 js-cluster-knative-domain-name-failure-message"
variant="danger"
>
{{ s__('ClusterIntegration|Something went wrong while updating Knative domain name.') }}
</gl-alert>
<div
:class="{ 'col-md-6': knativeInstalled, 'col-12': !knativeInstalled }"
class="form-group col-sm-12 mb-0"
>
<label for="knative-domainname">
<strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong>
</label>
<gl-dropdown
v-if="showDomainsDropdown"
:text="domainDropdownText"
toggle-class="dropdown-menu-toggle"
class="w-100 mb-2"
>
<gl-search-box-by-type
v-model.trim="searchQuery"
:placeholder="s__('ClusterIntegration|Search domains')"
/>
<gl-dropdown-item
v-for="domain in filteredDomains"
:key="domain.id"
@click="selectDomain(domain)"
>
<span class="ml-1">{{ domain.domain }}</span>
</gl-dropdown-item>
<template v-if="searchQuery">
<gl-dropdown-divider />
<gl-dropdown-item key="custom-domain" @click="selectCustomDomain(searchQuery)">
<span class="ml-1">
<gl-sprintf :message="s__('ClusterIntegration|Use %{query}')">
<template #query>
<code>{{ searchQuery }}</code>
</template>
</gl-sprintf>
</span>
</gl-dropdown-item>
</template>
</gl-dropdown>
<input
v-else
id="knative-domainname"
v-model="knativeHostname"
type="text"
class="form-control js-knative-domainname"
/>
<span v-if="validationError" class="gl-field-error">{{ validationError }}</span>
</div>
<template v-if="knativeInstalled">
<div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0">
<label for="knative-endpoint">
<strong>{{ s__('ClusterIntegration|Knative Endpoint:') }}</strong>
</label>
<div v-if="knativeExternalEndpoint" class="input-group">
<input
id="knative-endpoint"
:value="knativeExternalEndpoint"
type="text"
class="form-control js-knative-endpoint"
readonly
/>
<span class="input-group-append">
<clipboard-button
:text="knativeExternalEndpoint"
:title="s__('ClusterIntegration|Copy Knative Endpoint')"
class="input-group-text js-knative-endpoint-clipboard-btn"
/>
</span>
</div>
<div v-else class="input-group">
<input type="text" class="form-control js-endpoint" readonly />
<gl-loading-icon
class="position-absolute align-self-center ml-2 js-knative-ip-loading-icon"
/>
</div>
</div>
<p class="form-text text-muted col-12">
{{
s__(
`ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`,
)
}}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
__('More information')
}}</a>
</p>
<p
v-if="!knativeExternalEndpoint"
class="settings-message js-no-knative-endpoint-message mt-2 mr-3 mb-0 ml-3"
>
{{
s__(`ClusterIntegration|The endpoint is in
the process of being assigned. Please check your Kubernetes
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}}
</p>
<gl-button
class="js-knative-save-domain-button gl-mt-5 gl-ml-5"
variant="success"
category="primary"
:loading="saving"
:disabled="saveButtonDisabled"
@click="$emit('save')"
>
{{ saveButtonLabel }}
</gl-button>
</template>
</div>
</template>

View File

@ -1,36 +0,0 @@
<script>
import { GlButton } from '@gitlab/ui';
import { APPLICATION_STATUS } from '~/clusters/constants';
import { __ } from '~/locale';
const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
export default {
components: {
GlButton,
},
props: {
status: {
type: String,
required: true,
},
},
computed: {
disabled() {
return [UNINSTALLING, UPDATING].includes(this.status);
},
loading() {
return this.status === UNINSTALLING;
},
label() {
return this.loading ? __('Uninstalling') : __('Uninstall');
},
},
};
</script>
<template>
<gl-button :disabled="disabled" variant="default" :loading="loading">
{{ label }}
</gl-button>
</template>

View File

@ -1,101 +0,0 @@
<script>
import { GlModal, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import trackUninstallButtonClickMixin from 'ee_else_ce/clusters/mixins/track_uninstall_button_click';
import { sprintf, s__ } from '~/locale';
import {
HELM,
INGRESS,
CERT_MANAGER,
PROMETHEUS,
RUNNER,
KNATIVE,
JUPYTER,
ELASTIC_STACK,
} from '../constants';
const CUSTOM_APP_WARNING_TEXT = {
[HELM]: sprintf(
s__(
'ClusterIntegration|The associated Tiller pod will be deleted and cannot be restored. Your other applications will remain unaffected.',
),
{
gitlabManagedAppsNamespace: '<code>gitlab-managed-apps</code>',
},
false,
),
[INGRESS]: s__(
'ClusterIntegration|The associated load balancer and IP will be deleted and cannot be restored.',
),
[CERT_MANAGER]: s__(
'ClusterIntegration|The associated private key will be deleted and cannot be restored.',
),
[PROMETHEUS]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'),
[RUNNER]: s__('ClusterIntegration|Any running pipelines will be canceled.'),
[KNATIVE]: s__(
'ClusterIntegration|The associated IP and all deployed services will be deleted and cannot be restored. Uninstalling Knative will also remove Istio from your cluster. This will not effect any other applications.',
),
[JUPYTER]: s__(
'ClusterIntegration|All data not committed to GitLab will be deleted and cannot be restored.',
),
[ELASTIC_STACK]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'),
};
export default {
components: {
GlModal,
},
directives: {
SafeHtml,
},
mixins: [trackUninstallButtonClickMixin],
props: {
application: {
type: String,
required: true,
},
applicationTitle: {
type: String,
required: true,
},
},
computed: {
title() {
return sprintf(s__('ClusterIntegration|Uninstall %{appTitle}'), {
appTitle: this.applicationTitle,
});
},
warningText() {
return sprintf(
s__('ClusterIntegration|You are about to uninstall %{appTitle} from your cluster.'),
{
appTitle: this.applicationTitle,
},
);
},
customAppWarningText() {
return CUSTOM_APP_WARNING_TEXT[this.application];
},
modalId() {
return `uninstall-${this.application}`;
},
},
methods: {
confirmUninstall() {
this.trackUninstallButtonClick(this.application);
this.$emit('confirm');
},
},
};
</script>
<template>
<gl-modal
ok-variant="danger"
cancel-variant="light"
:ok-title="title"
:modal-id="modalId"
:title="title"
@ok="confirmUninstall()"
>
{{ warningText }} <span v-safe-html="customAppWarningText"></span>
</gl-modal>
</template>

View File

@ -1,66 +0,0 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlModal } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import { ELASTIC_STACK } from '../constants';
const CUSTOM_APP_WARNING_TEXT = {
[ELASTIC_STACK]: s__(
'ClusterIntegration|Your Elasticsearch cluster will be re-created during this upgrade. Your logs will be re-indexed, and you will lose historical logs from hosts terminated in the last 30 days.',
),
};
export default {
components: {
GlModal,
},
props: {
application: {
type: String,
required: true,
},
applicationTitle: {
type: String,
required: true,
},
},
computed: {
title() {
return sprintf(s__('ClusterIntegration|Update %{appTitle}'), {
appTitle: this.applicationTitle,
});
},
warningText() {
return sprintf(
s__('ClusterIntegration|You are about to update %{appTitle} on your cluster.'),
{
appTitle: this.applicationTitle,
},
);
},
customAppWarningText() {
return CUSTOM_APP_WARNING_TEXT[this.application];
},
modalId() {
return `update-${this.application}`;
},
},
methods: {
confirmUpdate() {
this.$emit('confirm');
},
},
};
</script>
<template>
<gl-modal
ok-variant="danger"
cancel-variant="light"
:ok-title="title"
:modal-id="modalId"
:title="title"
@ok="confirmUpdate()"
>
{{ warningText }} <span v-html="customAppWarningText"></span>
</gl-modal>
</template>

View File

@ -10,64 +10,7 @@ export const PROVIDER_TYPE = {
GCP: 'gcp',
};
// These need to match what is returned from the server
export const APPLICATION_STATUS = {
NO_STATUS: null,
NOT_INSTALLABLE: 'not_installable',
INSTALLABLE: 'installable',
SCHEDULED: 'scheduled',
INSTALLING: 'installing',
INSTALLED: 'installed',
UPDATING: 'updating',
UPDATED: 'updated',
UPDATE_ERRORED: 'update_errored',
UNINSTALLING: 'uninstalling',
UNINSTALL_ERRORED: 'uninstall_errored',
ERROR: 'errored',
PRE_INSTALLED: 'pre_installed',
UNINSTALLED: 'uninstalled',
EXTERNALLY_INSTALLED: 'externally_installed',
};
/*
* The application cannot be in any of the following states without
* not being installed.
*/
export const APPLICATION_INSTALLED_STATUSES = [
APPLICATION_STATUS.INSTALLED,
APPLICATION_STATUS.UPDATING,
APPLICATION_STATUS.UNINSTALLING,
APPLICATION_STATUS.PRE_INSTALLED,
];
// These are only used client-side
export const UPDATE_EVENT = 'update';
export const INSTALL_EVENT = 'install';
export const UNINSTALL_EVENT = 'uninstall';
export const HELM = 'helm';
export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative';
export const RUNNER = 'runner';
export const CERT_MANAGER = 'cert_manager';
export const CROSSPLANE = 'crossplane';
export const PROMETHEUS = 'prometheus';
export const ELASTIC_STACK = 'elastic_stack';
export const APPLICATIONS = [
HELM,
INGRESS,
JUPYTER,
KNATIVE,
RUNNER,
CERT_MANAGER,
PROMETHEUS,
ELASTIC_STACK,
];
export const INGRESS_DOMAIN_SUFFIX = '.nip.io';
export const LOGGING_MODE = 'logging';
export const BLOCKING_MODE = 'blocking';

View File

@ -1,250 +0,0 @@
import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT, UNINSTALL_EVENT } from '../constants';
const {
NO_STATUS,
SCHEDULED,
NOT_INSTALLABLE,
INSTALLABLE,
INSTALLING,
INSTALLED,
ERROR,
UPDATING,
UPDATED,
UPDATE_ERRORED,
UNINSTALLING,
UNINSTALL_ERRORED,
PRE_INSTALLED,
UNINSTALLED,
EXTERNALLY_INSTALLED,
} = APPLICATION_STATUS;
const applicationStateMachine = {
/* When the application initially loads, it will have `NO_STATUS`
* It will transition from `NO_STATUS` once the async backend call is completed
*/
[NO_STATUS]: {
on: {
[SCHEDULED]: {
target: INSTALLING,
},
[NOT_INSTALLABLE]: {
target: NOT_INSTALLABLE,
},
[INSTALLABLE]: {
target: INSTALLABLE,
},
[INSTALLING]: {
target: INSTALLING,
},
[INSTALLED]: {
target: INSTALLED,
},
[ERROR]: {
target: INSTALLABLE,
effects: {
installFailed: true,
},
},
[UPDATING]: {
target: UPDATING,
},
[UPDATED]: {
target: INSTALLED,
},
[UPDATE_ERRORED]: {
target: INSTALLED,
effects: {
updateFailed: true,
},
},
[UNINSTALLING]: {
target: UNINSTALLING,
},
[UNINSTALL_ERRORED]: {
target: INSTALLED,
effects: {
uninstallFailed: true,
},
},
[PRE_INSTALLED]: {
target: PRE_INSTALLED,
},
[UNINSTALLED]: {
target: UNINSTALLED,
},
[EXTERNALLY_INSTALLED]: {
target: EXTERNALLY_INSTALLED,
},
},
},
[NOT_INSTALLABLE]: {
on: {
[INSTALLABLE]: {
target: INSTALLABLE,
},
},
},
[INSTALLABLE]: {
on: {
[INSTALL_EVENT]: {
target: INSTALLING,
effects: {
installFailed: false,
},
},
[NOT_INSTALLABLE]: {
target: NOT_INSTALLABLE,
},
[INSTALLED]: {
target: INSTALLED,
effects: {
installFailed: false,
},
},
[UNINSTALLED]: {
target: UNINSTALLED,
effects: {
installFailed: false,
},
},
},
},
[INSTALLING]: {
on: {
[INSTALLED]: {
target: INSTALLED,
},
[ERROR]: {
target: INSTALLABLE,
effects: {
installFailed: true,
},
},
},
},
[INSTALLED]: {
on: {
[UPDATE_EVENT]: {
target: UPDATING,
effects: {
updateFailed: false,
updateSuccessful: false,
},
},
[NOT_INSTALLABLE]: {
target: NOT_INSTALLABLE,
},
[UNINSTALL_EVENT]: {
target: UNINSTALLING,
effects: {
uninstallFailed: false,
uninstallSuccessful: false,
},
},
[UNINSTALLED]: {
target: UNINSTALLED,
},
[ERROR]: {
target: INSTALLABLE,
effects: {
installFailed: true,
},
},
},
},
[PRE_INSTALLED]: {
on: {
[UPDATE_EVENT]: {
target: UPDATING,
effects: {
updateFailed: false,
updateSuccessful: false,
},
},
[NOT_INSTALLABLE]: {
target: NOT_INSTALLABLE,
},
[UNINSTALL_EVENT]: {
target: UNINSTALLING,
effects: {
uninstallFailed: false,
uninstallSuccessful: false,
},
},
},
},
[UPDATING]: {
on: {
[UPDATED]: {
target: INSTALLED,
effects: {
updateSuccessful: true,
},
},
[UPDATE_ERRORED]: {
target: INSTALLED,
effects: {
updateFailed: true,
},
},
},
},
[UNINSTALLING]: {
on: {
[INSTALLABLE]: {
target: INSTALLABLE,
effects: {
uninstallSuccessful: true,
},
},
[NOT_INSTALLABLE]: {
target: NOT_INSTALLABLE,
effects: {
uninstallSuccessful: true,
},
},
[UNINSTALL_ERRORED]: {
target: INSTALLED,
effects: {
uninstallFailed: true,
},
},
},
},
[UNINSTALLED]: {
on: {
[INSTALLED]: {
target: INSTALLED,
},
[ERROR]: {
target: INSTALLABLE,
effects: {
installFailed: true,
},
},
},
},
};
/**
* Determines an application new state based on the application current state
* and an event. If the application current state cannot handle a given event,
* the current state is returned.
*
* @param {*} application
* @param {*} event
*/
const transitionApplicationState = (application, event) => {
const stateMachine = applicationStateMachine[application.status];
const newState = stateMachine !== undefined ? stateMachine.on[event] : false;
return newState
? {
...application,
status: newState.target,
...newState.effects,
}
: application;
};
export default transitionApplicationState;

View File

@ -3,38 +3,12 @@ import axios from '../../lib/utils/axios_utils';
export default class ClusterService {
constructor(options = {}) {
this.options = options;
this.appInstallEndpointMap = {
helm: this.options.installHelmEndpoint,
ingress: this.options.installIngressEndpoint,
cert_manager: this.options.installCertManagerEndpoint,
crossplane: this.options.installCrossplaneEndpoint,
runner: this.options.installRunnerEndpoint,
prometheus: this.options.installPrometheusEndpoint,
jupyter: this.options.installJupyterEndpoint,
knative: this.options.installKnativeEndpoint,
elastic_stack: this.options.installElasticStackEndpoint,
};
this.appUpdateEndpointMap = {
knative: this.options.updateKnativeEndpoint,
};
}
fetchClusterStatus() {
return axios.get(this.options.endpoint);
}
installApplication(appId, params) {
return axios.post(this.appInstallEndpointMap[appId], params);
}
updateApplication(appId, params) {
return axios.patch(this.appUpdateEndpointMap[appId], params);
}
uninstallApplication(appId, params) {
return axios.delete(this.appInstallEndpointMap[appId], params);
}
fetchClusterEnvironments() {
return axios.get(this.options.clusterEnvironmentsEndpoint);
}

View File

@ -1,112 +1,16 @@
import { parseBoolean } from '../../lib/utils/common_utils';
import { s__ } from '../../locale';
import {
INGRESS,
JUPYTER,
KNATIVE,
CERT_MANAGER,
CROSSPLANE,
RUNNER,
APPLICATION_INSTALLED_STATUSES,
APPLICATION_STATUS,
INSTALL_EVENT,
UPDATE_EVENT,
UNINSTALL_EVENT,
ELASTIC_STACK,
} from '../constants';
import transitionApplicationState from '../services/application_state_machine';
const isApplicationInstalled = (appStatus) => APPLICATION_INSTALLED_STATUSES.includes(appStatus);
const applicationInitialState = {
status: null,
statusReason: null,
requestReason: null,
installable: true,
installed: false,
installFailed: false,
uninstallable: false,
uninstallFailed: false,
uninstallSuccessful: false,
validationError: null,
};
export default class ClusterStore {
constructor() {
this.state = {
helpPath: null,
helmHelpPath: null,
ingressHelpPath: null,
environmentsHelpPath: null,
clustersHelpPath: null,
deployBoardsHelpPath: null,
cloudRunHelpPath: null,
status: null,
providerType: null,
preInstalledKnative: false,
rbac: false,
statusReason: null,
applications: {
helm: {
...applicationInitialState,
title: s__('ClusterIntegration|Legacy Helm Tiller server'),
},
ingress: {
...applicationInitialState,
title: s__('ClusterIntegration|Ingress'),
externalIp: null,
externalHostname: null,
updateFailed: false,
updateAvailable: false,
},
cert_manager: {
...applicationInitialState,
title: s__('ClusterIntegration|Cert-Manager'),
email: null,
},
crossplane: {
...applicationInitialState,
title: s__('ClusterIntegration|Crossplane'),
stack: null,
},
runner: {
...applicationInitialState,
title: s__('ClusterIntegration|GitLab Runner'),
version: null,
chartRepo: 'https://gitlab.com/gitlab-org/charts/gitlab-runner',
updateAvailable: null,
updateSuccessful: false,
updateFailed: false,
},
prometheus: {
...applicationInitialState,
title: s__('ClusterIntegration|Prometheus'),
},
jupyter: {
...applicationInitialState,
title: s__('ClusterIntegration|JupyterHub'),
hostname: null,
},
knative: {
...applicationInitialState,
title: s__('ClusterIntegration|Knative'),
hostname: null,
isEditingDomain: false,
externalIp: null,
externalHostname: null,
updateSuccessful: false,
updateFailed: false,
},
elastic_stack: {
...applicationInitialState,
title: s__('ClusterIntegration|Elastic Stack'),
},
cilium: {
...applicationInitialState,
title: s__('ClusterIntegration|GitLab Container Network Policies'),
installable: false,
},
},
environments: [],
fetchingEnvironments: false,
};
@ -118,10 +22,6 @@ export default class ClusterStore {
});
}
setManagePrometheusPath(managePrometheusPath) {
this.state.managePrometheusPath = managePrometheusPath;
}
updateStatus(status) {
this.state.status = status;
}
@ -130,10 +30,6 @@ export default class ClusterStore {
this.state.providerType = providerType;
}
updatePreInstalledKnative(preInstalledKnative) {
this.state.preInstalledKnative = parseBoolean(preInstalledKnative);
}
updateRbac(rbac) {
this.state.rbac = parseBoolean(rbac);
}
@ -142,112 +38,9 @@ export default class ClusterStore {
this.state.statusReason = reason;
}
installApplication(appId) {
this.handleApplicationEvent(appId, INSTALL_EVENT);
}
notifyInstallFailure(appId) {
this.handleApplicationEvent(appId, APPLICATION_STATUS.ERROR);
}
updateApplication(appId) {
this.handleApplicationEvent(appId, UPDATE_EVENT);
}
notifyUpdateFailure(appId) {
this.handleApplicationEvent(appId, APPLICATION_STATUS.UPDATE_ERRORED);
}
uninstallApplication(appId) {
this.handleApplicationEvent(appId, UNINSTALL_EVENT);
}
notifyUninstallFailure(appId) {
this.handleApplicationEvent(appId, APPLICATION_STATUS.UNINSTALL_ERRORED);
}
handleApplicationEvent(appId, event) {
const currentAppState = this.state.applications[appId];
this.state.applications[appId] = transitionApplicationState(currentAppState, event);
}
updateAppProperty(appId, prop, value) {
this.state.applications[appId][prop] = value;
}
updateStateFromServer(serverState = {}) {
this.state.status = serverState.status;
this.state.statusReason = serverState.status_reason;
serverState.applications.forEach((serverAppEntry) => {
const {
name: appId,
status,
status_reason: statusReason,
version,
update_available: updateAvailable,
can_uninstall: uninstallable,
} = serverAppEntry;
const currentApplicationState = this.state.applications[appId] || {};
const nextApplicationState = transitionApplicationState(currentApplicationState, status);
this.state.applications[appId] = {
...currentApplicationState,
...nextApplicationState,
statusReason,
installed: isApplicationInstalled(nextApplicationState.status),
uninstallable,
};
if (appId === INGRESS) {
this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
this.state.applications.ingress.externalHostname = serverAppEntry.external_hostname;
this.state.applications.ingress.updateAvailable = updateAvailable;
} else if (appId === CERT_MANAGER) {
this.state.applications.cert_manager.email =
this.state.applications.cert_manager.email || serverAppEntry.email;
} else if (appId === CROSSPLANE) {
this.state.applications.crossplane.stack =
this.state.applications.crossplane.stack || serverAppEntry.stack;
} else if (appId === JUPYTER) {
this.state.applications.jupyter.hostname = this.updateHostnameIfUnset(
this.state.applications.jupyter.hostname,
serverAppEntry.hostname,
'jupyter',
);
} else if (appId === KNATIVE) {
if (serverAppEntry.available_domains) {
this.state.applications.knative.availableDomains = serverAppEntry.available_domains;
}
if (!this.state.applications.knative.isEditingDomain) {
this.state.applications.knative.pagesDomain =
serverAppEntry.pages_domain || this.state.applications.knative.pagesDomain;
this.state.applications.knative.hostname =
serverAppEntry.hostname || this.state.applications.knative.hostname;
}
this.state.applications.knative.externalIp =
serverAppEntry.external_ip || this.state.applications.knative.externalIp;
this.state.applications.knative.externalHostname =
serverAppEntry.external_hostname || this.state.applications.knative.externalHostname;
} else if (appId === RUNNER) {
this.state.applications.runner.version = version;
this.state.applications.runner.updateAvailable = updateAvailable;
} else if (appId === ELASTIC_STACK) {
this.state.applications.elastic_stack.version = version;
this.state.applications.elastic_stack.updateAvailable = updateAvailable;
}
});
}
updateHostnameIfUnset(current, updated, fallback) {
return (
current ||
updated ||
(this.state.applications.ingress.externalIp
? `${fallback}.${this.state.applications.ingress.externalIp}.nip.io`
: '')
);
}
toggleFetchEnvironments(isFetching) {

View File

@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import { mapInline, mapParallel } from 'ee_else_ce/diffs/components/diff_row_utils';
import { mapParallel } from 'ee_else_ce/diffs/components/diff_row_utils';
import DiffFileDrafts from '~/batch_comments/components/diff_file_drafts.vue';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import { diffViewerModes } from '~/ide/constants';
@ -9,7 +9,6 @@ import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue';
import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import NoteForm from '../../notes/components/note_form.vue';
import eventHub from '../../notes/event_hub';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
@ -18,14 +17,10 @@ import { getDiffMode } from '../store/utils';
import DiffDiscussions from './diff_discussions.vue';
import DiffView from './diff_view.vue';
import ImageDiffOverlay from './image_diff_overlay.vue';
import InlineDiffView from './inline_diff_view.vue';
import ParallelDiffView from './parallel_diff_view.vue';
export default {
components: {
GlLoadingIcon,
InlineDiffView,
ParallelDiffView,
DiffView,
DiffViewer,
NoteForm,
@ -36,7 +31,7 @@ export default {
userAvatarLink,
DiffFileDrafts,
},
mixins: [diffLineNoteFormMixin, draftCommentsMixin, glFeatureFlagsMixin()],
mixins: [diffLineNoteFormMixin, draftCommentsMixin],
props: {
diffFile: {
type: Object,
@ -52,7 +47,6 @@ export default {
...mapState('diffs', ['projectPath']),
...mapGetters('diffs', [
'isInlineView',
'isParallelView',
'getCommentFormForDiffFile',
'diffLines',
'fileLineCodequality',
@ -86,15 +80,8 @@ export default {
return this.getUserData;
},
mappedLines() {
if (this.glFeatures.unifiedDiffComponents) {
return this.diffLines(this.diffFile, true).map(mapParallel(this)) || [];
}
// TODO: Everything below this line can be deleted when unifiedDiffComponents FF is removed
if (this.isInlineView) {
return this.diffFile.highlighted_diff_lines.map(mapInline(this));
}
return this.diffLines(this.diffFile).map(mapParallel(this));
// TODO: Do this data generation when we recieve a response to save a computed property being created
return this.diffLines(this.diffFile).map(mapParallel(this)) || [];
},
},
updated() {
@ -126,7 +113,7 @@ export default {
<template>
<div class="diff-content">
<div class="diff-viewer">
<template v-if="isTextFile && glFeatures.unifiedDiffComponents">
<template v-if="isTextFile">
<diff-view
:diff-file="diffFile"
:diff-lines="mappedLines"
@ -135,21 +122,6 @@ export default {
/>
<gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" />
</template>
<template v-else-if="isTextFile">
<inline-diff-view
v-if="isInlineView"
:diff-file="diffFile"
:diff-lines="mappedLines"
:help-page-path="helpPagePath"
/>
<parallel-diff-view
v-else-if="isParallelView"
:diff-file="diffFile"
:diff-lines="mappedLines"
:help-page-path="helpPagePath"
/>
<gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" />
</template>
<not-diffable-viewer v-else-if="notDiffable" />
<no-preview-viewer v-else-if="noPreview" />
<diff-viewer

View File

@ -106,10 +106,7 @@ export default {
};
const getDiffLines = () => {
if (this.diffViewType === PARALLEL_DIFF_VIEW_TYPE) {
return this.diffLines(this.diffFile, this.glFeatures.unifiedDiffComponents).reduce(
combineSides,
[],
);
return this.diffLines(this.diffFile).reduce(combineSides, []);
}
return this.diffFile[INLINE_DIFF_LINES_KEY];

View File

@ -210,6 +210,7 @@ export default {
<template>
<div :class="classNameMap" class="diff-grid-row diff-tr line_holder">
<div
:id="line.left && line.left.line_code"
data-testid="left-side"
class="diff-grid-left left-side"
v-bind="interopLeftAttributes"
@ -293,7 +294,6 @@ export default {
/>
</div>
<div
:id="line.left.line_code"
:key="line.left.line_code"
:class="[parallelViewLeftLineType, { parallel: !inline }]"
class="diff-td line_content with-coverage left-side"
@ -334,6 +334,7 @@ export default {
</div>
<div
v-if="!inline"
:id="line.right && line.right.line_code"
data-testid="right-side"
class="diff-grid-right right-side"
v-bind="interopRightAttributes"
@ -409,7 +410,6 @@ export default {
/>
</div>
<div
:id="line.right.line_code"
:key="line.right.rich_text"
:class="[
line.right.type,

View File

@ -139,24 +139,3 @@ export const mapParallel = (content) => (line) => {
commentRowClasses: hasDiscussions(left) || hasDiscussions(right) ? '' : 'js-temp-notes-holder',
};
};
// TODO: Delete this function when unifiedDiffComponents FF is removed
export const mapInline = (content) => (line) => {
// Discussions/Comments
const renderCommentRow = line.hasForm || (line.discussions?.length && line.discussionsExpanded);
return {
...line,
renderDiscussion: Boolean(line.discussions?.length),
isMatchLine: isMatchLine(line.type),
commentRowClasses: line.discussions?.length ? '' : 'js-temp-notes-holder',
renderCommentRow,
hasDraft: content.shouldRenderDraftRow(content.diffFile.file_hash, line),
hasCommentForm: line.hasForm,
isMetaLine: isMetaLine(line.type),
isContextLine: isContextLine(line.type),
hasDiscussions: hasDiscussions(line),
lineHref: lineHref(line),
lineCode: lineCode(line),
};
};

View File

@ -1,204 +0,0 @@
<script>
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import { CONTEXT_LINE_CLASS_NAME } from '../constants';
import { getInteropInlineAttributes } from '../utils/interoperability';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import {
isHighlighted,
shouldShowCommentButton,
shouldRenderCommentButton,
classNameMapCell,
addCommentTooltip,
} from './diff_row_utils';
export default {
components: {
DiffGutterAvatars,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
SafeHtml,
},
props: {
fileHash: {
type: String,
required: true,
},
filePath: {
type: String,
required: true,
},
line: {
type: Object,
required: true,
},
isBottom: {
type: Boolean,
required: false,
default: false,
},
isCommented: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isHover: false,
};
},
computed: {
...mapGetters(['isLoggedIn']),
...mapGetters('diffs', ['fileLineCoverage']),
...mapState({
isHighlighted(state) {
return isHighlighted(state, this.line, this.isCommented);
},
}),
classNameMap() {
return [
this.line.type,
{
[CONTEXT_LINE_CLASS_NAME]: this.line.isContextLine,
},
];
},
inlineRowId() {
return this.line.line_code || `${this.fileHash}_${this.line.old_line}_${this.line.new_line}`;
},
coverageState() {
return this.fileLineCoverage(this.filePath, this.line.new_line);
},
classNameMapCell() {
return classNameMapCell({
line: this.line,
hll: this.isHighlighted,
isLoggedIn: this.isLoggedIn,
isHover: this.isHover,
});
},
addCommentTooltip() {
return addCommentTooltip(this.line);
},
shouldRenderCommentButton() {
return shouldRenderCommentButton(this.isLoggedIn, true);
},
shouldShowCommentButton() {
return shouldShowCommentButton(
this.isHover,
this.line.isContextLine,
this.line.isMetaLine,
this.line.hasDiscussions,
);
},
shouldShowAvatarsOnGutter() {
return this.line.hasDiscussions;
},
interopAttrs() {
return getInteropInlineAttributes(this.line);
},
},
mounted() {
this.scrollToLineIfNeededInline(this.line);
},
methods: {
...mapActions('diffs', [
'scrollToLineIfNeededInline',
'showCommentForm',
'setHighlightedRow',
'toggleLineDiscussions',
]),
handleMouseMove(e) {
// To show the comment icon on the gutter we need to know if we hover the line.
// Current table structure doesn't allow us to do this with CSS in both of the diff view types
this.isHover = e.type === 'mouseover';
},
handleCommentButton() {
this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
},
},
};
</script>
<template>
<tr
:id="inlineRowId"
:class="classNameMap"
class="line_holder"
v-bind="interopAttrs"
@mouseover="handleMouseMove"
@mouseout="handleMouseMove"
>
<td ref="oldTd" class="diff-line-num old_line" :class="classNameMapCell">
<span
v-if="shouldRenderCommentButton"
ref="addNoteTooltip"
v-gl-tooltip
class="add-diff-note tooltip-wrapper"
:title="addCommentTooltip"
>
<button
v-show="shouldShowCommentButton"
ref="addDiffNoteButton"
type="button"
class="add-diff-note note-button js-add-diff-note-button"
:disabled="line.commentsDisabled"
:aria-label="addCommentTooltip"
@click="handleCommentButton"
>
<gl-icon :size="12" name="comment" />
</button>
</span>
<a
v-if="line.old_line"
ref="lineNumberRefOld"
:data-linenumber="line.old_line"
:href="line.lineHref"
@click="setHighlightedRow(line.lineCode)"
>
</a>
<diff-gutter-avatars
v-if="shouldShowAvatarsOnGutter"
:discussions="line.discussions"
:discussions-expanded="line.discussionsExpanded"
@toggleLineDiscussions="
toggleLineDiscussions({
lineCode: line.lineCode,
fileHash,
expanded: !line.discussionsExpanded,
})
"
/>
</td>
<td ref="newTd" class="diff-line-num new_line" :class="classNameMapCell">
<a
v-if="line.new_line"
ref="lineNumberRefNew"
:data-linenumber="line.new_line"
:href="line.lineHref"
@click="setHighlightedRow(line.lineCode)"
>
</a>
</td>
<td
v-gl-tooltip.hover
:title="coverageState.text"
:class="[line.type, coverageState.class, { hll: isHighlighted }]"
class="line-coverage"
></td>
<td
:key="line.line_code"
v-safe-html="line.rich_text"
:class="[
line.type,
{
hll: isHighlighted,
},
]"
class="line_content with-coverage"
></td>
</tr>
</template>

View File

@ -1,117 +0,0 @@
<script>
import { mapGetters, mapState } from 'vuex';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
import inlineDiffTableRow from './inline_diff_table_row.vue';
export default {
components: {
DiffCommentCell,
inlineDiffTableRow,
DraftNote,
DiffExpansionCell,
},
mixins: [draftCommentsMixin, glFeatureFlagsMixin()],
props: {
diffFile: {
type: Object,
required: true,
},
diffLines: {
type: Array,
required: true,
},
helpPagePath: {
type: String,
required: false,
default: '',
},
},
computed: {
...mapGetters('diffs', ['commitId']),
...mapState({
selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition,
selectedCommentPositionHover: ({ notes }) => notes.selectedCommentPositionHover,
}),
diffLinesLength() {
return this.diffLines.length;
},
commentedLines() {
return getCommentedLines(
this.selectedCommentPosition || this.selectedCommentPositionHover,
this.diffLines,
);
},
},
userColorScheme: window.gon.user_color_scheme,
};
</script>
<template>
<table
:class="$options.userColorScheme"
:data-commit-id="commitId"
class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view"
>
<colgroup>
<col style="width: 50px" />
<col style="width: 50px" />
<col style="width: 8px" />
<col />
</colgroup>
<tbody>
<template v-for="(line, index) in diffLines">
<tr v-if="line.isMatchLine" :key="`expand-${index}`" class="line_expansion match">
<td colspan="4" class="text-center gl-font-regular">
<diff-expansion-cell
:file-hash="diffFile.file_hash"
:context-lines-path="diffFile.context_lines_path"
:line="line"
:is-top="index === 0"
:is-bottom="index + 1 === diffLinesLength"
/>
</td>
</tr>
<inline-diff-table-row
v-if="!line.isMatchLine"
:key="`${line.line_code || index}`"
:file-hash="diffFile.file_hash"
:file-path="diffFile.file_path"
:line="line"
:is-bottom="index + 1 === diffLinesLength"
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
/>
<tr
v-if="line.renderCommentRow"
:key="`icr-${line.line_code || index}`"
:class="line.commentRowClasses"
class="notes_holder"
>
<td class="notes-content" colspan="4">
<diff-comment-cell
:diff-file-hash="diffFile.file_hash"
:line="line"
:help-page-path="helpPagePath"
:has-draft="line.hasDraft"
/>
</td>
</tr>
<tr v-if="line.hasDraft" :key="`draft_${index}`" class="notes_holder js-temp-notes-holder">
<td class="notes-content" colspan="4">
<div class="content">
<draft-note
:draft="draftForLine(diffFile.file_hash, line)"
:diff-file="diffFile"
:line="line"
/>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</template>

View File

@ -1,310 +0,0 @@
<script>
import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import { CONTEXT_LINE_CLASS_NAME, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
import {
getInteropOldSideAttributes,
getInteropNewSideAttributes,
} from '../utils/interoperability';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
export default {
components: {
GlIcon,
DiffGutterAvatars,
},
directives: {
GlTooltip: GlTooltipDirective,
SafeHtml,
},
props: {
fileHash: {
type: String,
required: true,
},
filePath: {
type: String,
required: true,
},
line: {
type: Object,
required: true,
},
isBottom: {
type: Boolean,
required: false,
default: false,
},
isCommented: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isLeftHover: false,
isRightHover: false,
isCommentButtonRendered: false,
};
},
computed: {
...mapGetters('diffs', ['fileLineCoverage']),
...mapGetters(['isLoggedIn']),
...mapState({
isHighlighted(state) {
const line = this.line.left?.line_code ? this.line.left : this.line.right;
return utils.isHighlighted(state, line, this.isCommented);
},
}),
classNameMap() {
return {
[CONTEXT_LINE_CLASS_NAME]: this.line.isContextLineLeft,
[PARALLEL_DIFF_VIEW_TYPE]: true,
};
},
parallelViewLeftLineType() {
return utils.parallelViewLeftLineType(this.line, this.isHighlighted);
},
coverageState() {
return this.fileLineCoverage(this.filePath, this.line.right.new_line);
},
classNameMapCellLeft() {
return utils.classNameMapCell({
line: this.line.left,
hll: this.isHighlighted,
isLoggedIn: this.isLoggedIn,
isHover: this.isLeftHover,
});
},
classNameMapCellRight() {
return utils.classNameMapCell({
line: this.line.right,
hll: this.isHighlighted,
isLoggedIn: this.isLoggedIn,
isHover: this.isRightHover,
});
},
addCommentTooltipLeft() {
return utils.addCommentTooltip(this.line.left);
},
addCommentTooltipRight() {
return utils.addCommentTooltip(this.line.right);
},
shouldRenderCommentButton() {
return utils.shouldRenderCommentButton(this.isLoggedIn, this.isCommentButtonRendered);
},
shouldShowCommentButtonLeft() {
return utils.shouldShowCommentButton(
this.isLeftHover,
this.line.isContextLineLeft,
this.line.isMetaLineLeft,
this.line.hasDiscussionsLeft,
);
},
shouldShowCommentButtonRight() {
return utils.shouldShowCommentButton(
this.isRightHover,
this.line.isContextLineRight,
this.line.isMetaLineRight,
this.line.hasDiscussionsRight,
);
},
interopLeftAttributes() {
return getInteropOldSideAttributes(this.line.left);
},
interopRightAttributes() {
return getInteropNewSideAttributes(this.line.right);
},
},
mounted() {
this.scrollToLineIfNeededParallel(this.line);
this.unwatchShouldShowCommentButton = this.$watch(
(vm) => [vm.shouldShowCommentButtonLeft, vm.shouldShowCommentButtonRight].join(),
(newVal) => {
if (newVal) {
this.isCommentButtonRendered = true;
this.unwatchShouldShowCommentButton();
}
},
);
},
beforeDestroy() {
this.unwatchShouldShowCommentButton();
},
methods: {
...mapActions('diffs', [
'scrollToLineIfNeededParallel',
'showCommentForm',
'setHighlightedRow',
'toggleLineDiscussions',
]),
handleMouseMove(e) {
const isHover = e.type === 'mouseover';
const hoveringCell = e.target.closest('td');
const allCellsInHoveringRow = Array.from(e.currentTarget.children);
const hoverIndex = allCellsInHoveringRow.indexOf(hoveringCell);
if (hoverIndex >= 3) {
this.isRightHover = isHover;
} else {
this.isLeftHover = isHover;
}
},
// Prevent text selecting on both sides of parallel diff view
// Backport of the same code from legacy diff notes.
handleParallelLineMouseDown(e) {
const line = $(e.currentTarget);
const table = line.closest('table');
table.removeClass('left-side-selected right-side-selected');
const [lineClass] = ['left-side', 'right-side'].filter((name) => line.hasClass(name));
if (lineClass) {
table.addClass(`${lineClass}-selected`);
}
},
handleCommentButton(line) {
this.showCommentForm({ lineCode: line.line_code, fileHash: this.fileHash });
},
},
};
</script>
<template>
<tr
:class="classNameMap"
class="line_holder"
@mouseover="handleMouseMove"
@mouseout="handleMouseMove"
>
<template v-if="line.left && !line.isMatchLineLeft">
<td ref="oldTd" :class="classNameMapCellLeft" class="diff-line-num old_line">
<span
v-if="shouldRenderCommentButton"
ref="addNoteTooltipLeft"
v-gl-tooltip
class="add-diff-note tooltip-wrapper"
:title="addCommentTooltipLeft"
>
<button
v-show="shouldShowCommentButtonLeft"
ref="addDiffNoteButtonLeft"
type="button"
class="add-diff-note note-button js-add-diff-note-button"
:disabled="line.left.commentsDisabled"
:aria-label="addCommentTooltipLeft"
@click="handleCommentButton(line.left)"
>
<gl-icon :size="12" name="comment" />
</button>
</span>
<a
v-if="line.left.old_line"
ref="lineNumberRefOld"
:data-linenumber="line.left.old_line"
:href="line.lineHrefOld"
@click="setHighlightedRow(line.lineCode)"
>
</a>
<diff-gutter-avatars
v-if="line.hasDiscussionsLeft"
:discussions="line.left.discussions"
:discussions-expanded="line.left.discussionsExpanded"
@toggleLineDiscussions="
toggleLineDiscussions({
lineCode: line.left.line_code,
fileHash,
expanded: !line.left.discussionsExpanded,
})
"
/>
</td>
<td :class="parallelViewLeftLineType" class="line-coverage left-side"></td>
<td
:id="line.left.line_code"
:key="line.left.line_code"
v-safe-html="line.left.rich_text"
:class="parallelViewLeftLineType"
v-bind="interopLeftAttributes"
class="line_content with-coverage parallel left-side"
@mousedown="handleParallelLineMouseDown"
></td>
</template>
<template v-else>
<td class="diff-line-num old_line empty-cell"></td>
<td class="line-coverage left-side empty-cell"></td>
<td class="line_content with-coverage parallel left-side empty-cell"></td>
</template>
<template v-if="line.right && !line.isMatchLineRight">
<td ref="newTd" :class="classNameMapCellRight" class="diff-line-num new_line">
<span
v-if="shouldRenderCommentButton"
ref="addNoteTooltipRight"
v-gl-tooltip
class="add-diff-note tooltip-wrapper"
:title="addCommentTooltipRight"
>
<button
v-show="shouldShowCommentButtonRight"
ref="addDiffNoteButtonRight"
type="button"
class="add-diff-note note-button js-add-diff-note-button"
:disabled="line.right.commentsDisabled"
:aria-label="addCommentTooltipRight"
@click="handleCommentButton(line.right)"
>
<gl-icon :size="12" name="comment" />
</button>
</span>
<a
v-if="line.right.new_line"
ref="lineNumberRefNew"
:data-linenumber="line.right.new_line"
:href="line.lineHrefNew"
@click="setHighlightedRow(line.lineCode)"
>
</a>
<diff-gutter-avatars
v-if="line.hasDiscussionsRight"
:discussions="line.right.discussions"
:discussions-expanded="line.right.discussionsExpanded"
@toggleLineDiscussions="
toggleLineDiscussions({
lineCode: line.right.line_code,
fileHash,
expanded: !line.right.discussionsExpanded,
})
"
/>
</td>
<td
v-gl-tooltip.hover
:title="coverageState.text"
:class="[line.right.type, coverageState.class, { hll: isHighlighted }]"
class="line-coverage right-side"
></td>
<td
:id="line.right.line_code"
:key="line.right.rich_text"
v-safe-html="line.right.rich_text"
:class="[
line.right.type,
{
hll: isHighlighted,
},
]"
v-bind="interopRightAttributes"
class="line_content with-coverage parallel right-side"
@mousedown="handleParallelLineMouseDown"
></td>
</template>
<template v-else>
<td class="diff-line-num old_line empty-cell"></td>
<td class="line-coverage right-side empty-cell"></td>
<td class="line_content with-coverage parallel right-side empty-cell"></td>
</template>
</tr>
</template>

View File

@ -1,142 +0,0 @@
<script>
import { mapGetters, mapState } from 'vuex';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
import parallelDiffTableRow from './parallel_diff_table_row.vue';
export default {
components: {
DiffExpansionCell,
parallelDiffTableRow,
DiffCommentCell,
DraftNote,
},
mixins: [draftCommentsMixin],
props: {
diffFile: {
type: Object,
required: true,
},
diffLines: {
type: Array,
required: true,
},
helpPagePath: {
type: String,
required: false,
default: '',
},
},
computed: {
...mapGetters('diffs', ['commitId']),
...mapState({
selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition,
selectedCommentPositionHover: ({ notes }) => notes.selectedCommentPositionHover,
}),
diffLinesLength() {
return this.diffLines.length;
},
commentedLines() {
return getCommentedLines(
this.selectedCommentPosition || this.selectedCommentPositionHover,
this.diffLines,
);
},
},
userColorScheme: window.gon.user_color_scheme,
};
</script>
<template>
<table
:class="$options.userColorScheme"
:data-commit-id="commitId"
class="code diff-wrap-lines js-syntax-highlight text-file"
>
<colgroup>
<col style="width: 50px" />
<col style="width: 8px" />
<col />
<col style="width: 50px" />
<col style="width: 8px" />
<col />
</colgroup>
<tbody>
<template v-for="(line, index) in diffLines">
<tr
v-if="line.isMatchLineLeft || line.isMatchLineRight"
:key="`expand-${index}`"
class="line_expansion match"
>
<td colspan="6" class="text-center gl-font-regular">
<diff-expansion-cell
:file-hash="diffFile.file_hash"
:context-lines-path="diffFile.context_lines_path"
:line="line.left"
:is-top="index === 0"
:is-bottom="index + 1 === diffLinesLength"
/>
</td>
</tr>
<parallel-diff-table-row
:key="line.line_code"
:file-hash="diffFile.file_hash"
:file-path="diffFile.file_path"
:line="line"
:is-bottom="index + 1 === diffLinesLength"
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
/>
<tr
v-if="line.renderCommentRow"
:key="`dcr-${line.line_code || index}`"
:class="line.commentRowClasses"
class="notes_holder"
>
<td class="notes-content parallel old" colspan="3">
<diff-comment-cell
v-if="line.left"
:line="line.left"
:diff-file-hash="diffFile.file_hash"
:help-page-path="helpPagePath"
:has-draft="line.left.hasDraft"
line-position="left"
/>
</td>
<td class="notes-content parallel new" colspan="3">
<diff-comment-cell
v-if="line.right"
:line="line.right"
:diff-file-hash="diffFile.file_hash"
:line-index="index"
:help-page-path="helpPagePath"
:has-draft="line.right.hasDraft"
line-position="right"
/>
</td>
</tr>
<tr
v-if="shouldRenderParallelDraftRow(diffFile.file_hash, line)"
:key="`drafts-${index}`"
:class="line.draftRowClasses"
class="notes_holder"
>
<td class="notes_line old"></td>
<td class="notes-content parallel old" colspan="2">
<div v-if="line.left && line.left.lineDraft.isDraft" class="content">
<draft-note :draft="line.left.lineDraft" :line="line.left" />
</div>
</td>
<td class="notes_line new"></td>
<td class="notes-content parallel new" colspan="2">
<div v-if="line.right && line.right.lineDraft.isDraft" class="content">
<draft-note :draft="line.right.lineDraft" :line="line.right" />
</div>
</td>
</tr>
</template>
</tbody>
</table>
</template>

View File

@ -151,11 +151,7 @@ export const currentDiffIndex = (state) =>
state.diffFiles.findIndex((diff) => diff.file_hash === state.currentDiffFileId),
);
export const diffLines = (state) => (file, unifiedDiffComponents) => {
if (!unifiedDiffComponents && state.diffViewType === INLINE_DIFF_VIEW_TYPE) {
return null;
}
export const diffLines = (state) => (file) => {
return parallelizeDiffLines(
file.highlighted_diff_lines || [],
state.diffViewType === INLINE_DIFF_VIEW_TYPE,

View File

@ -215,35 +215,35 @@ export function urlQueryToFilter(query = '', options = {}) {
/**
* Returns array of token values from localStorage
* based on provided recentTokenValuesStorageKey
* based on provided recentSuggestionsStorageKey
*
* @param {String} recentTokenValuesStorageKey
* @param {String} recentSuggestionsStorageKey
* @returns
*/
export function getRecentlyUsedTokenValues(recentTokenValuesStorageKey) {
let recentlyUsedTokenValues = [];
export function getRecentlyUsedSuggestions(recentSuggestionsStorageKey) {
let recentlyUsedSuggestions = [];
if (AccessorUtilities.isLocalStorageAccessSafe()) {
recentlyUsedTokenValues = JSON.parse(localStorage.getItem(recentTokenValuesStorageKey)) || [];
recentlyUsedSuggestions = JSON.parse(localStorage.getItem(recentSuggestionsStorageKey)) || [];
}
return recentlyUsedTokenValues;
return recentlyUsedSuggestions;
}
/**
* Sets provided token value to recently used array
* within localStorage for provided recentTokenValuesStorageKey
* within localStorage for provided recentSuggestionsStorageKey
*
* @param {String} recentTokenValuesStorageKey
* @param {String} recentSuggestionsStorageKey
* @param {Object} tokenValue
*/
export function setTokenValueToRecentlyUsed(recentTokenValuesStorageKey, tokenValue) {
const recentlyUsedTokenValues = getRecentlyUsedTokenValues(recentTokenValuesStorageKey);
export function setTokenValueToRecentlyUsed(recentSuggestionsStorageKey, tokenValue) {
const recentlyUsedSuggestions = getRecentlyUsedSuggestions(recentSuggestionsStorageKey);
recentlyUsedTokenValues.splice(0, 0, { ...tokenValue });
recentlyUsedSuggestions.splice(0, 0, { ...tokenValue });
if (AccessorUtilities.isLocalStorageAccessSafe()) {
localStorage.setItem(
recentTokenValuesStorageKey,
JSON.stringify(uniqWith(recentlyUsedTokenValues, isEqual).slice(0, MAX_RECENT_TOKENS_SIZE)),
recentSuggestionsStorageKey,
JSON.stringify(uniqWith(recentlyUsedSuggestions, isEqual).slice(0, MAX_RECENT_TOKENS_SIZE)),
);
}
}

View File

@ -74,13 +74,13 @@ export default {
:config="config"
:value="value"
:active="active"
:tokens-list-loading="loading"
:token-values="authors"
:suggestions-loading="loading"
:suggestions="authors"
:fn-active-token-value="getActiveAuthor"
:default-token-values="defaultAuthors"
:preloaded-token-values="preloadedAuthors"
:recent-token-values-storage-key="config.recentTokenValuesStorageKey"
@fetch-token-values="fetchAuthorBySearchTerm"
:default-suggestions="defaultAuthors"
:preloaded-suggestions="preloadedAuthors"
:recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
@fetch-suggestions="fetchAuthorBySearchTerm"
v-on="$listeners"
>
<template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
@ -93,9 +93,9 @@ export default {
/>
<span>{{ activeTokenValue ? activeTokenValue.name : inputValue }}</span>
</template>
<template #token-values-list="{ tokenValues }">
<template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
v-for="author in tokenValues"
v-for="author in suggestions"
:key="author.username"
:value="author.username"
>

View File

@ -8,7 +8,7 @@ import {
} from '@gitlab/ui';
import { DEBOUNCE_DELAY } from '../constants';
import { getRecentlyUsedTokenValues, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
export default {
components: {
@ -31,12 +31,12 @@ export default {
type: Boolean,
required: true,
},
tokensListLoading: {
suggestionsLoading: {
type: Boolean,
required: false,
default: false,
},
tokenValues: {
suggestions: {
type: Array,
required: false,
default: () => [],
@ -44,21 +44,21 @@ export default {
fnActiveTokenValue: {
type: Function,
required: false,
default: (tokenValues, currentTokenValue) => {
return tokenValues.find(({ value }) => value === currentTokenValue);
default: (suggestions, currentTokenValue) => {
return suggestions.find(({ value }) => value === currentTokenValue);
},
},
defaultTokenValues: {
defaultSuggestions: {
type: Array,
required: false,
default: () => [],
},
preloadedTokenValues: {
preloadedSuggestions: {
type: Array,
required: false,
default: () => [],
},
recentTokenValuesStorageKey: {
recentSuggestionsStorageKey: {
type: String,
required: false,
default: '',
@ -77,21 +77,21 @@ export default {
data() {
return {
searchKey: '',
recentTokenValues: this.recentTokenValuesStorageKey
? getRecentlyUsedTokenValues(this.recentTokenValuesStorageKey)
recentSuggestions: this.recentSuggestionsStorageKey
? getRecentlyUsedSuggestions(this.recentSuggestionsStorageKey)
: [],
loading: false,
};
},
computed: {
isRecentTokenValuesEnabled() {
return Boolean(this.recentTokenValuesStorageKey);
isRecentSuggestionsEnabled() {
return Boolean(this.recentSuggestionsStorageKey);
},
recentTokenIds() {
return this.recentTokenValues.map((tokenValue) => tokenValue[this.valueIdentifier]);
return this.recentSuggestions.map((tokenValue) => tokenValue[this.valueIdentifier]);
},
preloadedTokenIds() {
return this.preloadedTokenValues.map((tokenValue) => tokenValue[this.valueIdentifier]);
return this.preloadedSuggestions.map((tokenValue) => tokenValue[this.valueIdentifier]);
},
currentTokenValue() {
if (this.fnCurrentTokenValue) {
@ -100,17 +100,17 @@ export default {
return this.value.data.toLowerCase();
},
activeTokenValue() {
return this.fnActiveTokenValue(this.tokenValues, this.currentTokenValue);
return this.fnActiveTokenValue(this.suggestions, this.currentTokenValue);
},
/**
* Return all the tokenValues when searchKey is present
* otherwise return only the tokenValues which aren't
* Return all the suggestions when searchKey is present
* otherwise return only the suggestions which aren't
* present in "Recently used"
*/
availableTokenValues() {
availableSuggestions() {
return this.searchKey
? this.tokenValues
: this.tokenValues.filter(
? this.suggestions
: this.suggestions.filter(
(tokenValue) =>
!this.recentTokenIds.includes(tokenValue[this.valueIdentifier]) &&
!this.preloadedTokenIds.includes(tokenValue[this.valueIdentifier]),
@ -121,8 +121,8 @@ export default {
active: {
immediate: true,
handler(newValue) {
if (!newValue && !this.tokenValues.length) {
this.$emit('fetch-token-values', this.value.data);
if (!newValue && !this.suggestions.length) {
this.$emit('fetch-suggestions', this.value.data);
}
},
},
@ -131,7 +131,7 @@ export default {
handleInput({ data }) {
this.searchKey = data;
setTimeout(() => {
if (!this.tokensListLoading) this.$emit('fetch-token-values', data);
if (!this.suggestionsLoading) this.$emit('fetch-suggestions', data);
}, DEBOUNCE_DELAY);
},
handleTokenValueSelected(activeTokenValue) {
@ -140,11 +140,11 @@ export default {
// 2. User has actually selected a value
// 3. Selected value is not part of preloaded list.
if (
this.isRecentTokenValuesEnabled &&
this.isRecentSuggestionsEnabled &&
activeTokenValue &&
!this.preloadedTokenIds.includes(activeTokenValue[this.valueIdentifier])
) {
setTokenValueToRecentlyUsed(this.recentTokenValuesStorageKey, activeTokenValue);
setTokenValueToRecentlyUsed(this.recentSuggestionsStorageKey, activeTokenValue);
}
},
},
@ -168,9 +168,9 @@ export default {
<slot name="view" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot>
</template>
<template #suggestions>
<template v-if="defaultTokenValues.length">
<template v-if="defaultSuggestions.length">
<gl-filtered-search-suggestion
v-for="token in defaultTokenValues"
v-for="token in defaultSuggestions"
:key="token.value"
:value="token.value"
>
@ -178,19 +178,19 @@ export default {
</gl-filtered-search-suggestion>
<gl-dropdown-divider />
</template>
<template v-if="isRecentTokenValuesEnabled && recentTokenValues.length && !searchKey">
<template v-if="isRecentSuggestionsEnabled && recentSuggestions.length && !searchKey">
<gl-dropdown-section-header>{{ __('Recently used') }}</gl-dropdown-section-header>
<slot name="token-values-list" :token-values="recentTokenValues"></slot>
<slot name="suggestions-list" :suggestions="recentSuggestions"></slot>
<gl-dropdown-divider />
</template>
<slot
v-if="preloadedTokenValues.length && !searchKey"
name="token-values-list"
:token-values="preloadedTokenValues"
v-if="preloadedSuggestions.length && !searchKey"
name="suggestions-list"
:suggestions="preloadedSuggestions"
></slot>
<gl-loading-icon v-if="tokensListLoading" />
<gl-loading-icon v-if="suggestionsLoading" />
<template v-else>
<slot name="token-values-list" :token-values="availableTokenValues"></slot>
<slot name="suggestions-list" :suggestions="availableSuggestions"></slot>
</template>
</template>
</gl-filtered-search-token>

View File

@ -96,12 +96,12 @@ export default {
:config="config"
:value="value"
:active="active"
:tokens-list-loading="loading"
:token-values="labels"
:suggestions-loading="loading"
:suggestions="labels"
:fn-active-token-value="getActiveLabel"
:default-token-values="defaultLabels"
:recent-token-values-storage-key="config.recentTokenValuesStorageKey"
@fetch-token-values="fetchLabelBySearchTerm"
:default-suggestions="defaultLabels"
:recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
@fetch-suggestions="fetchLabelBySearchTerm"
v-on="$listeners"
>
<template
@ -115,9 +115,9 @@ export default {
>~{{ activeTokenValue ? getLabelName(activeTokenValue) : inputValue }}</gl-token
>
</template>
<template #token-values-list="{ tokenValues }">
<template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
v-for="label in tokenValues"
v-for="label in suggestions"
:key="label.id"
:value="getLabelName(label)"
>

View File

@ -21,8 +21,6 @@ class Admin::CohortsController < Admin::ApplicationController
end
def track_cohorts_visit
if request.format.html? && request.headers['DNT'] != '1'
track_visit('i_analytics_cohorts')
end
track_visit('i_analytics_cohorts') if trackable_html_request?
end
end

View File

@ -12,12 +12,13 @@
# You can also pass custom conditions using `if:`, using the same format as with Rails callbacks.
# You can also pass an optional block that calculates and returns a custom id to track.
module RedisTracking
include Gitlab::Tracking::Helpers
extend ActiveSupport::Concern
class_methods do
def track_redis_hll_event(*controller_actions, name:, if: nil, &block)
custom_conditions = Array.wrap(binding.local_variable_get('if'))
conditions = [:trackable_request?, *custom_conditions]
conditions = [:trackable_html_request?, *custom_conditions]
after_action only: controller_actions, if: conditions do
track_unique_redis_hll_event(name, &block)
@ -37,10 +38,6 @@ module RedisTracking
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(event_name, values: unique_id)
end
def trackable_request?
request.format.html? && request.headers['DNT'] != '1'
end
def visitor_id
return cookies[:visitor_id] if cookies[:visitor_id].present?
return unless current_user

View File

@ -193,7 +193,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
end
def track_viewed_diffs_events
return if request.headers['DNT'] == '1'
return if dnt_enabled?
Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter
.track_mr_diffs_action(merge_request: @merge_request)

View File

@ -33,7 +33,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:approvals_commented_by, @project, default_enabled: true)
push_frontend_feature_flag(:merge_request_widget_graphql, @project, default_enabled: :yaml)
push_frontend_feature_flag(:drag_comment_selection, @project, default_enabled: true)
push_frontend_feature_flag(:unified_diff_components, @project, default_enabled: true)
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project, default_enabled: :yaml)
push_frontend_feature_flag(:core_security_mr_widget_counts, @project)
push_frontend_feature_flag(:diffs_gradual_load, @project, default_enabled: true)

View File

@ -2,6 +2,7 @@
module Analytics
module UniqueVisitsHelper
include Gitlab::Tracking::Helpers
extend ActiveSupport::Concern
def visitor_id
@ -21,7 +22,7 @@ module Analytics
class_methods do
def track_unique_visits(controller_actions, target_id:)
after_action only: controller_actions, if: -> { request.format.html? && request.headers['DNT'] != '1' } do
after_action only: controller_actions, if: -> { trackable_html_request? } do
track_visit(target_id)
end
end

View File

@ -136,6 +136,7 @@ module Ci
scope :eager_load_job_artifacts, -> { includes(:job_artifacts) }
scope :eager_load_job_artifacts_archive, -> { includes(:job_artifacts_archive) }
scope :eager_load_tags, -> { includes(:tags) }
scope :eager_load_everything, -> do
includes(
@ -759,6 +760,14 @@ module Ci
self.token && ActiveSupport::SecurityUtils.secure_compare(token, self.token)
end
def tag_list
if tags.loaded?
tags.map(&:name)
else
super
end
end
def has_tags?
tag_list.any?
end

View File

@ -2,6 +2,7 @@
module Packages
module Nuget
TEMPORARY_PACKAGE_NAME = 'NuGet.Temporary.Package'
TEMPORARY_SYMBOL_PACKAGE_NAME = 'NuGet.Temporary.SymbolPackage'
def self.table_name_prefix
'packages_nuget_'

View File

@ -8,6 +8,7 @@ module Packages
SERVICE_VERSIONS = {
download: %w[PackageBaseAddress/3.0.0],
search: %w[SearchQueryService SearchQueryService/3.0.0-beta SearchQueryService/3.0.0-rc],
symbol: %w[SymbolPackagePublish/4.9.0],
publish: %w[PackagePublish/2.0.0],
metadata: %w[RegistrationsBaseUrl RegistrationsBaseUrl/3.0.0-beta RegistrationsBaseUrl/3.0.0-rc]
}.freeze
@ -15,13 +16,14 @@ module Packages
SERVICE_COMMENTS = {
download: 'Get package content (.nupkg).',
search: 'Filter and search for packages by keyword.',
symbol: 'Push symbol packages.',
publish: 'Push and delete (or unlist) packages.',
metadata: 'Get package metadata.'
}.freeze
VERSION = '3.0.0'
PROJECT_LEVEL_SERVICES = %i[download publish].freeze
PROJECT_LEVEL_SERVICES = %i[download publish symbol].freeze
GROUP_LEVEL_SERVICES = %i[search metadata].freeze
def initialize(project_or_group)
@ -63,6 +65,8 @@ module Packages
download_service_url
when :search
search_service_url
when :symbol
symbol_service_url
when :metadata
metadata_service_url
when :publish
@ -124,6 +128,10 @@ module Packages
def publish_service_url
api_v4_projects_packages_nuget_path(id: @project_or_group.id)
end
def symbol_service_url
api_v4_projects_packages_nuget_symbolpackage_path(id: @project_or_group.id)
end
end
end
end

View File

@ -18,6 +18,9 @@ module Ci
build.ensure_scheduling_type!
reprocess!(build).tap do |new_build|
check_assignable_runners!(new_build)
next if new_build.failed?
Gitlab::OptimisticLocking.retry_lock(new_build, name: 'retry_build', &:enqueue)
AfterRequeueJobService.new(project, current_user).execute(build)
@ -54,6 +57,8 @@ module Ci
end
end
def check_assignable_runners!(build); end
def create_build!(attributes)
build = project.builds.new(attributes)
build.assign_attributes(::Gitlab::Ci::Pipeline::Seed::Build.environment_attributes_for(build))

View File

@ -13,8 +13,8 @@ module Ci
pipeline.ensure_scheduling_type!
pipeline.retryable_builds.preload_needs.find_each do |build|
next unless can?(current_user, :update_build, build)
builds_relation(pipeline).find_each do |build|
next unless can_be_retried?(build)
Ci::RetryBuildService.new(project, current_user)
.reprocess!(build)
@ -36,5 +36,17 @@ module Ci
.new(pipeline)
.execute
end
private
def builds_relation(pipeline)
pipeline.retryable_builds.preload_needs
end
def can_be_retried?(build)
can?(current_user, :update_build, build)
end
end
end
Ci::RetryPipelineService.prepend_mod_with('Ci::RetryPipelineService')

View File

@ -18,6 +18,7 @@ module Packages
XPATH_DEPENDENCIES = '//xmlns:package/xmlns:metadata/xmlns:dependencies/xmlns:dependency'
XPATH_DEPENDENCY_GROUPS = '//xmlns:package/xmlns:metadata/xmlns:dependencies/xmlns:group'
XPATH_TAGS = '//xmlns:package/xmlns:metadata/xmlns:tags'
XPATH_PACKAGE_TYPES = '//xmlns:package/xmlns:metadata/xmlns:packageTypes/xmlns:packageType'
MAX_FILE_SIZE = 4.megabytes.freeze
@ -57,6 +58,7 @@ module Packages
.tap do |metadata|
metadata[:package_dependencies] = extract_dependencies(doc)
metadata[:package_tags] = extract_tags(doc)
metadata[:package_types] = extract_package_types(doc)
end
end
@ -85,6 +87,10 @@ module Packages
}.compact
end
def extract_package_types(doc)
doc.xpath(XPATH_PACKAGE_TYPES).map { |node| node.attr('name') }.uniq
end
def extract_tags(doc)
tags = doc.xpath(XPATH_TAGS).text

View File

@ -8,6 +8,7 @@ module Packages
# used by ExclusiveLeaseGuard
DEFAULT_LEASE_TIMEOUT = 1.hour.to_i.freeze
SYMBOL_PACKAGE_IDENTIFIER = 'SymbolsPackage'
InvalidMetadataError = Class.new(StandardError)
@ -20,7 +21,13 @@ module Packages
try_obtain_lease do
@package_file.transaction do
package = existing_package ? link_to_existing_package : update_linked_package
if existing_package
package = link_to_existing_package
elsif symbol_package?
raise InvalidMetadataError, 'symbol package is invalid, matching package does not exist'
else
package = update_linked_package
end
update_package(package)
@ -39,6 +46,8 @@ module Packages
private
def update_package(package)
return if symbol_package?
::Packages::Nuget::SyncMetadatumService
.new(package, metadata.slice(:project_url, :license_url, :icon_url))
.execute
@ -103,6 +112,14 @@ module Packages
metadata.fetch(:package_tags, [])
end
def package_types
metadata.fetch(:package_types, [])
end
def symbol_package?
package_types.include?(SYMBOL_PACKAGE_IDENTIFIER)
end
def metadata
strong_memoize(:metadata) do
::Packages::Nuget::MetadataExtractionService.new(@package_file.id).execute
@ -110,7 +127,7 @@ module Packages
end
def package_filename
"#{package_name.downcase}.#{package_version.downcase}.nupkg"
"#{package_name.downcase}.#{package_version.downcase}.#{symbol_package? ? 'snupkg' : 'nupkg'}"
end
# used by ExclusiveLeaseGuard

View File

@ -1 +0,0 @@
.cluster-applications-table#js-cluster-applications

View File

@ -1,5 +0,0 @@
- active = params[:tab] == 'apps'
%li.nav-item{ role: 'presentation' }
%a#cluster-apps-tab.nav-link.qa-applications{ class: active_when(active), href: clusterable.cluster_path(@cluster.id, params: {tab: 'apps'}) }
%span= _('Applications')

View File

@ -2,21 +2,10 @@
- add_to_breadcrumbs _('Kubernetes Clusters'), clusterable.index_path
- breadcrumb_title @cluster.name
- page_title _('Kubernetes Cluster')
- manage_prometheus_path = edit_project_service_path(@cluster.project, 'prometheus') if @project
- cluster_environments_path = clusterable.environments_cluster_path(@cluster)
- status_path = clusterable.cluster_status_cluster_path(@cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster)
.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path,
install_helm_path: clusterable.install_applications_cluster_path(@cluster, :helm),
install_ingress_path: clusterable.install_applications_cluster_path(@cluster, :ingress),
install_cert_manager_path: clusterable.install_applications_cluster_path(@cluster, :cert_manager),
install_crossplane_path: clusterable.install_applications_cluster_path(@cluster, :crossplane),
install_prometheus_path: clusterable.install_applications_cluster_path(@cluster, :prometheus),
install_runner_path: clusterable.install_applications_cluster_path(@cluster, :runner),
install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter),
install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative),
update_knative_path: clusterable.update_applications_cluster_path(@cluster, :knative),
install_elastic_stack_path: clusterable.install_applications_cluster_path(@cluster, :elastic_stack),
cluster_environments_path: cluster_environments_path,
toggle_status: @cluster.enabled? ? 'true': 'false',
has_rbac: has_rbac_enabled?(@cluster) ? 'true': 'false',
@ -24,15 +13,11 @@
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
provider_type: @cluster.provider_type,
pre_installed_knative: @cluster.knative_pre_installed? ? 'true': 'false',
help_path: help_page_path('user/project/clusters/index.md'),
environments_help_path: help_page_path('ci/environments/index.md', anchor: 'create-a-static-environment'),
clusters_help_path: help_page_path('user/project/clusters/index.md', anchor: 'deploying-to-a-kubernetes-cluster'),
deploy_boards_help_path: help_page_path('user/project/deploy_boards.md', anchor: 'enabling-deploy-boards'),
cloud_run_help_path: help_page_path('user/project/clusters/add_gke_clusters.md', anchor: 'cloud-run-for-anthos'),
manage_prometheus_path: manage_prometheus_path,
cluster_id: @cluster.id,
cilium_help_path: help_page_path('user/clusters/applications.md', anchor: 'install-cilium-using-gitlab-cicd')} }
cluster_id: @cluster.id } }
.js-cluster-application-notice
.flash-container

View File

@ -1,8 +0,0 @@
---
name: ci_pipeline_status_omit_commit_sha_in_cache_key
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33712
rollout_issue_url:
milestone: '13.2'
type: development
group: group::pipeline execution
default_enabled: true

View File

@ -1,8 +1,8 @@
---
name: unified_diff_components
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44974
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/268039
name: ci_quota_check_on_retries
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62702
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/333765
milestone: '14.0'
type: development
group: group::code review
default_enabled: true
milestone: '13.6'
group: group::pipeline execution
default_enabled: false

View File

@ -2,7 +2,6 @@
name: runner_graphql_query
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59763
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/328700
milestone:
type: development
group: group::runner
default_enabled: false

View File

@ -735,11 +735,23 @@ For each Patroni instance on the secondary site:
```
1. Reconfigure GitLab for the changes to take effect.
This is required to bootstrap PostgreSQL users and settings:
This is required to bootstrap PostgreSQL users and settings.
```shell
gitlab-ctl reconfigure
```
- If this is a fresh installation of Patroni:
```shell
gitlab-ctl reconfigure
```
- If you are configuring a Patroni standby cluster on a site that previously had a working Patroni cluster:
```shell
gitlab-ctl stop patroni
rm -rf /var/opt/gitlab/postgresql/data
/opt/gitlab/embedded/bin/patronictl -c /var/opt/gitlab/patroni/patroni.yaml remove postgresql-ha
gitlab-ctl reconfigure
gitlab-ctl start patroni
```
### Migrating from repmgr to Patroni

View File

@ -6,22 +6,20 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Puma **(FREE SELF)**
NOTE:
Starting with GitLab 13.0, Puma
is the default web server and Unicorn has been
disabled by default. In GitLab 14.0, Unicorn was removed from the Linux package
and only Puma is available.
Puma is a simple, fast, multi-threaded, and highly concurrent HTTP 1.1 server for
Ruby applications. It's the default GitLab web server since GitLab 13.0
and has replaced Unicorn. From GitLab 14.0, Unicorn is no longer supported.
NOTE:
Starting with GitLab 13.0, Puma is the default web server and Unicorn has been disabled.
In GitLab 14.0, Unicorn was removed from the Linux package and only Puma is available.
## Configure Puma
To configure Puma:
1. Determine suitable Puma worker and thread [settings](../../install/requirements.md#puma-settings).
1. If you're swithcing from Unicorn, [convert any custom settings to Puma](#convert-unicorn-settings-to-puma).
1. If you're switching from Unicorn, [convert any custom settings to Puma](#convert-unicorn-settings-to-puma).
1. For multi-node deployments, configure the load balancer to use the
[readiness check](../load_balancer.md#readiness-check).
1. Reconfigure GitLab so the above changes take effect:
@ -30,7 +28,7 @@ To configure Puma:
sudo gitlab-ctl reconfigure
```
For Helm based deployments, see the
For Helm-based deployments, see the
[`webservice` chart documentation](https://docs.gitlab.com/charts/charts/gitlab/webservice/index.html).
For more details about the Puma configuration, see the
@ -38,9 +36,11 @@ For more details about the Puma configuration, see the
## Puma Worker Killer
By default, the [Puma Worker Killer](https://github.com/schneems/puma_worker_killer) will restart
a worker if it exceeds a [memory limit](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/cluster/puma_worker_killer_initializer.rb). Additionally, rolling restarts of
Puma workers are performed every 12 hours.
By default:
- The [Puma Worker Killer](https://github.com/schneems/puma_worker_killer) restarts a worker if it
exceeds a [memory limit](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/cluster/puma_worker_killer_initializer.rb).
- Rolling restarts of Puma workers are performed every 12 hours.
To change the memory limit setting:
@ -80,20 +80,22 @@ To change the worker timeout:
sudo gitlab-ctl reconfigure
```
## Running in memory-constrained environments
## Memory-constrained environments
In a memory-constrained environment with less than 4GB of RAM available, consider disabling Puma [Clustered mode](https://github.com/puma/puma#clustered-mode).
In a memory-constrained environment with less than 4GB of RAM available, consider disabling Puma
[Clustered mode](https://github.com/puma/puma#clustered-mode).
Configuring Puma by setting the amount of `workers` to `0` could reduce memory usage by hundreds of MB.
For details on Puma worker and thread settings, see the [Puma requirements](../../install/requirements.md#puma-settings).
Unlike in a Clustered mode, which is set up by default, only a single Puma process would serve the application.
The downside of running Puma with such configuration is the reduced throughput, and it could be considered as a fair tradeoff in a memory-constraint environment.
The downside of running Puma with such configuration is the reduced throughput, which could be
considered as a fair tradeoff in a memory-constraint environment.
When running Puma in Single mode, some features are not supported:
- Phased restart will not work: [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/300665)
- Phased restart do not work: [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/300665)
- [Phased restart](https://gitlab.com/gitlab-org/gitlab/-/issues/300665)
- [Puma Worker Killer](https://gitlab.com/gitlab-org/gitlab/-/issues/300664)
@ -142,7 +144,8 @@ and only Puma is available.
Puma has a multi-thread architecture which uses less memory than a multi-process
application server like Unicorn. On GitLab.com, we saw a 40% reduction in memory
consumption. Most Rails applications requests normally include a proportion of I/O wait time.
During I/O wait time MRI Ruby will release the GVL (Global VM Lock) to other threads.
During I/O wait time MRI Ruby releases the GVL (Global VM Lock) to other threads.
Multi-threaded Puma can therefore still serve more requests than a single process.
When switching to Puma, any Unicorn server configuration will _not_ carry over

View File

@ -200,7 +200,7 @@ related to using `only/except` above (or, consider moving to `rules`).
In [GitLab 13.7](https://gitlab.com/gitlab-org/gitlab/-/issues/201845) and later,
you can add `workflow:rules` to [switch from branch pipelines to merge request pipelines](../yaml/README.md#switch-between-branch-pipelines-and-merge-request-pipelines).
The pipeline switches to merge request pipelines this after a merge request is open on the branch.
After a merge request is open on the branch, the pipeline switches to a merge request pipeline.
### Two pipelines created when pushing an invalid CI configuration file

View File

@ -304,7 +304,6 @@ GitLab documentation should be clear and easy to understand. Avoid unnecessary w
- Be clear, concise, and stick to the goal of the topic.
- Write in US English with US grammar. (Tested in [`British.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/British.yml).)
- Use [inclusive language](#inclusive-language).
- Rewrite to avoid wordiness:
- there is
- there are
@ -385,80 +384,6 @@ references to user interface elements. For example:
- To sign in to product X, enter your credentials, and then select **Log in**.
### Inclusive language
We strive to create documentation that's inclusive. This section includes
guidance and examples for these categories:
- [Gender-specific wording](#avoid-gender-specific-wording).
(Tested in [`InclusionGender.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/InclusionGender.yml).)
- [Ableist language](#avoid-ableist-language).
(Tested in [`InclusionAbleism.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/InclusionAbleism.yml).)
- [Cultural sensitivity](#culturally-sensitive-language).
(Tested in [`InclusionCultural.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/InclusionCultural.yml).)
We write our developer documentation with inclusivity and diversity in mind. This
page is not an exhaustive reference, but describes some general guidelines and
examples that illustrate some best practices to follow.
#### Avoid gender-specific wording
When possible, use gender-neutral pronouns. For example, you can use a singular
[they](https://developers.google.com/style/pronouns#gender-neutral-pronouns) as
a gender-neutral pronoun.
Avoid the use of gender-specific pronouns, unless referring to a specific person.
<!-- vale gitlab.InclusionGender = NO -->
| Use | Avoid |
|-----------------------------------|---------------------------------|
| People, humanity | Mankind |
| GitLab Team Members | Manpower |
| You can install; They can install | He can install; She can install |
<!-- vale gitlab.InclusionGender = YES -->
If you need to set up [Fake user information](#fake-user-information), use
diverse or non-gendered names with common surnames.
#### Avoid ableist language
Avoid terms that are also used in negative stereotypes for different groups.
<!-- vale gitlab.InclusionAbleism = NO -->
| Use | Avoid |
|------------------------|----------------------|
| Check for completeness | Sanity check |
| Uncertain outliers | Crazy outliers |
| Slows the service | Cripples the service |
| Placeholder variable | Dummy variable |
| Active/Inactive | Enabled/Disabled |
| On/Off | Enabled/Disabled |
<!-- vale gitlab.InclusionAbleism = YES -->
Credit: [Avoid ableist language](https://developers.google.com/style/inclusive-documentation#ableist-language)
in the Google Developer Style Guide.
#### Culturally sensitive language
Avoid terms that reflect negative cultural stereotypes and history. In most
cases, you can replace terms such as `master` and `slave` with terms that are
more precise and functional, such as `primary` and `secondary`.
<!-- vale gitlab.InclusionCultural = NO -->
| Use | Avoid |
|----------------------|-----------------------|
| Primary / secondary | Master / slave |
| Allowlist / denylist | Blacklist / whitelist |
<!-- vale gitlab.InclusionCultural = YES -->
For more information see the [Internet Draft specification](https://tools.ietf.org/html/draft-knodel-terminology-02).
### Fake user information
You may need to include user information in entries such as a REST call or user profile.
@ -1512,25 +1437,6 @@ It renders on the GitLab documentation site as:
To maintain consistency through GitLab documentation, use these styles and terms.
### Merge requests (MRs)
Merge requests allow you to exchange changes you made to source code and
collaborate with other people on the same project.
- Use lowercase _merge requests_ regardless of whether referring to the feature
or individual merge requests.
As noted in the GitLab [Writing Style Guidelines](https://about.gitlab.com/handbook/communication/#writing-style-guidelines),
if you use the _MR_ acronym, expand it at least once per document page.
Typically, the first use would be phrased as _merge request (MR)_ with subsequent
instances being _MR_.
Examples:
- "We prefer GitLab merge requests".
- "Open a merge request to fix a broken link".
- "After you open a merge request (MR), submit your MR for review and approval".
### Describe UI elements
Follow these styles when you're describing user interface elements in an

View File

@ -32,6 +32,10 @@ Instead of **and/or**, use or or rewrite the sentence to spell out both options.
Try to avoid extra words when referring to an example or table in a documentation page, but if required, use **following** instead.
## blacklist
Do not use. Another option is **denylist**. ([Vale](../testing.md#vale) rule: [`InclusionCultural.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/InclusionCultural.yml))
## currently
Do not use when talking about the product or its features. The documentation describes the product as it is today. ([Vale](../testing.md#vale) rule: [`CurrentStatus.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/CurrentStatus.yml))
@ -43,6 +47,11 @@ to mean someone who is assigned the Developer role. Instead, write it out. "If y
Do not use "Developer permissions." A user who is assigned the Developer role has a set of associated permissions.
## disable
See [the Microsoft style guide](https://docs.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/d/disable-disabled) for guidance.
Use **inactive** or **off** instead. ([Vale](../testing.md#vale) rule: [`InclusionAbleism.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/InclusionAbleism.yml))
## easily
Do not use. If the user doesn't find the process to be these things, we lose their trust.
@ -51,6 +60,11 @@ Do not use. If the user doesn't find the process to be these things, we lose the
Do not use Latin abbreviations. Use **for example**, **such as**, **for instance**, or **like** instead. ([Vale](../testing.md#vale) rule: [`LatinTerms.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/LatinTerms.yml))
## enable
See [the Microsoft style guide](https://docs.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/e/enable-enables) for guidance.
Use **active** or **on** instead. ([Vale](../testing.md#vale) rule: [`InclusionAbleism.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/InclusionAbleism.yml))
## future tense
When possible, use present tense instead. For example, use `after you execute this command, GitLab displays the result` instead of `after you execute this command, GitLab will display the result`. ([Vale](../testing.md#vale) rule: [`FutureTense.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/FutureTense.yml))
@ -89,6 +103,18 @@ to mean someone who is assigned the Maintainer role. Instead, write it out. "If
Do not use "Maintainer permissions." A user who is assigned the Maintainer role has a set of associated permissions.
## mankind
Do not use. Use **people** or **humanity** instead. ([Vale](../testing.md#vale) rule: [`InclusionGender.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/InclusionGender.yml))
## manpower
Do not use. Use words like **workforce** or **GitLab team members**. ([Vale](../testing.md#vale) rule: [`InclusionGender.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/InclusionGender.yml))
## master
Do not use. Options are **primary** or **main**. ([Vale](../testing.md#vale) rule: [`InclusionCultural.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/InclusionCultural.yml))
## may, might
**Might** means something has the probability of occurring. **May** gives permission to do something. Consider **can** instead of **may**.
@ -97,6 +123,10 @@ Do not use "Maintainer permissions." A user who is assigned the Maintainer role
Do not use first-person singular. Use **you**, **we**, or **us** instead. ([Vale](../testing.md#vale) rule: [`FirstPerson.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/FirstPerson.yml))
## merge requests
Lowercase. If you use **MR** as the acronym, spell it out on first use.
## Owner
When writing about the Owner role, use a capital "O." Do not use the phrase, "if you are an owner"
@ -127,6 +157,10 @@ Do not use "Reporter permissions." A user who is assigned the Reporter role has
Do not use roles and permissions interchangeably. Each user is assigned a role. Each role includes a set of permissions.
## sanity check
Do not use. Use **check for completeness** instead. ([Vale](../testing.md#vale) rule: [`InclusionAbleism.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/InclusionAbleism.yml))
## scalability
Do not use when talking about increasing GitLab performance for additional users. The words scale or scaling are sometimes acceptable, but references to increasing GitLab performance for additional users should direct readers to the GitLab [reference architectures](../../../administration/reference_architectures/index.md) page.
@ -139,6 +173,10 @@ Do not use. If the user doesn't find the process to be these things, we lose the
Instead of **and/or**, use **or** or another sensible construction. This rule also applies to other slashes, like **follow/unfollow**. Some exceptions (like **CI/CD**) are allowed.
## slave
Do not use. Another option is **secondary**. ([Vale](../testing.md#vale) rule: [`InclusionCultural.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/InclusionCultural.yml))
## subgroup
Use instead of `sub-group`.
@ -147,6 +185,12 @@ Use instead of `sub-group`.
Do not use. Example: `the file that you save` can be `the file you save`.
## they
Avoid the use of gender-specific pronouns, unless referring to a specific person.
Use a singular [they](https://developers.google.com/style/pronouns#gender-neutral-pronouns) as
a gender-neutral pronoun.
## useful
Do not use. If the user doesn't find the process to be these things, we lose their trust.
@ -159,5 +203,9 @@ Do not use. Use **use** instead. It's more succinct and easier for non-native En
Do not use Latin abbreviations. Use **with**, **through**, or **by using** instead. ([Vale](../testing.md#vale) rule: [`LatinTerms.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/LatinTerms.yml))
## whitelist
Do not use. Another option is **allowlist**. ([Vale](../testing.md#vale) rule: [`InclusionCultural.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/InclusionCultural.yml))
<!-- vale on -->
<!-- markdownlint-enable -->

View File

@ -588,7 +588,7 @@ When there is no experiment data in the `window.gon.experiment` object for the g
NOTE:
We use the terms "enabled" and "disabled" here, even though it's against our
[documentation style guide recommendations](../documentation/styleguide/index.md#avoid-ableist-language)
[documentation style guide recommendations](../documentation/styleguide/word_list.md#enable)
because these are the terms that the feature flag documentation uses.
You may already be familiar with the concept of feature flags in GitLab, but using

View File

@ -85,11 +85,10 @@ The **Alert details** tab has two sections. The top section provides a short lis
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217768) in GitLab 13.2.
The **Metrics** tab displays a metrics chart for alerts coming from Prometheus. If the alert originated from any other tool, the **Metrics** tab is empty. To set up alerts for GitLab-managed Prometheus instances, see [Managed Prometheus instances](../metrics/alerts.md#managed-prometheus-instances). For externally-managed Prometheus instances, you must configure your alerting
rules to display a chart in the alert. For information about how to configure
The **Metrics** tab displays a metrics chart for alerts coming from Prometheus. If the alert originated from any other tool, the **Metrics** tab is empty.
For externally-managed Prometheus instances, you must configure your alerting rules to display a chart in the alert. For information about how to configure
your alerting rules, see [Embedding metrics based on alerts in incident issues](../metrics/embed.md#embedding-metrics-based-on-alerts-in-incident-issues). See
[External Prometheus instances](../metrics/alerts.md#external-prometheus-instances)
for information about setting up alerts for your self-managed Prometheus
[External Prometheus instances](../metrics/alerts.md#external-prometheus-instances) for information about setting up alerts for your self-managed Prometheus
instance.
To view the metrics for an alert:
@ -201,19 +200,6 @@ add a to-do item:
Select the **To-Do List** **{todo-done}** in the navigation bar to view your current to-do list.
## Link runbooks to alerts
> Runbook URLs [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39315) in GitLab 13.3.
When creating alerts from the metrics dashboard for
[managed Prometheus instances](../metrics/alerts.md#managed-prometheus-instances),
you can link a runbook. When the alert triggers, you can access the runbook through
the [chart context menu](../metrics/dashboards/index.md#chart-context-menu) in the
upper-right corner of the metrics chart, making it easy for you to locate and access
the correct runbook:
![Linked Runbook in charts](img/link_runbooks_to_alerts_v13_5.png)
## View the environment that generated the alert
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232492) in GitLab 13.5 behind a feature flag, disabled by default.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

@ -9,54 +9,10 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/42640) to GitLab Free in 12.10.
After [configuring metrics for your CI/CD environment](index.md), you can set up
alerting for Prometheus metrics depending on the location of your instances, and
alerting for Prometheus metrics, and
[trigger actions from alerts](#trigger-actions-from-alerts) to notify
your team when environment performance falls outside of the boundaries you set.
## Managed Prometheus instances
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6590) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.2 for [custom metrics](index.md#adding-custom-metrics), and GitLab 11.3 for [library metrics](../../user/project/integrations/prometheus_library/index.md).
WARNING:
Managed Prometheus on Kubernetes is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/327796)
and scheduled for [removal in GitLab 14.0](https://gitlab.com/groups/gitlab-org/-/epics/4280).
For managed Prometheus instances using auto configuration, you can
[configure alerts for metrics](index.md#adding-custom-metrics) directly in the
[metrics dashboard](index.md). To set an alert:
1. In your project, navigate to **Monitor > Metrics**,
1. Identify the metric you want to create the alert for, and click the
**ellipsis** **{ellipsis_v}** icon in the top right corner of the metric.
1. Choose **Alerts**.
1. Set threshold and operator.
1. (Optional) Add a Runbook URL.
1. Click **Add** to save and activate the alert.
![Adding an alert](img/prometheus_alert.png)
To remove the alert, click back on the alert icon for the desired metric, and click **Delete**.
### Link runbooks to alerts
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39315) in GitLab 13.3.
> - [Deprecated](https://gitlab.com/groups/gitlab-org/-/epics/5877) in GitLab 13.11.
> - [Removed](https://gitlab.com/groups/gitlab-org/-/epics/4280) in GitLab 14.0.
WARNING:
Linking runbooks to alerts through the alerts UI is [deprecated](https://gitlab.com/groups/gitlab-org/-/epics/5877)
and scheduled for [removal in GitLab 14.0](https://gitlab.com/groups/gitlab-org/-/epics/4280).
However, you can still add runbooks to your alert payload. They show up in the alert UI when the
alert is triggered.
When creating alerts from the metrics dashboard for [managed Prometheus instances](#managed-prometheus-instances),
you can also link a runbook. When the alert triggers, the
[chart context menu](dashboards/index.md#chart-context-menu) on the metrics chart
links to the runbook, making it easy for you to locate and access the correct runbook
as soon as the alert fires:
![Linked Runbook in charts](img/linked_runbooks_on_charts.png)
## Prometheus cluster integrations
Alerts are not currently supported for [Prometheus cluster integrations](../../user/clusters/integrations.md).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -123,7 +123,7 @@ You can take action related to a chart's data by clicking the
**{ellipsis_v}** **More actions** dropdown box above the upper right corner of
any chart on a dashboard:
![Context Menu](img/panel_context_menu_v13_3.png)
![Context Menu](img/panel_context_menu_v14_0.png)
The options are:
@ -135,10 +135,6 @@ The options are:
feature, logs narrow down to the selected time range. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/122013) in GitLab 12.8.)
- **Download CSV** - Data from Prometheus charts on the metrics dashboard can be downloaded as CSV.
- [Copy link to chart](../embed.md#embedding-gitlab-managed-kubernetes-metrics)
- **Alerts** - Display any [alerts](../alerts.md) configured for this metric.
- **View Runbook** - Displays the runbook for an alert. For information about configuring
runbooks, read [Set up alerts for Prometheus metrics](../alerts.md).
([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211844) in GitLab 13.3.)
### Timeline zoom and URL sharing

View File

@ -103,7 +103,8 @@ To enable or disable Auto DevOps at the group level:
Even when disabled at the instance level, group owners and project maintainers
can still enable Auto DevOps at the group and project level, respectively.
1. As an administrator, go to **Admin Area > Settings > CI/CD > Continuous Integration and Deployment**.
1. As an administrator, on the top bar, select **Menu >** **{admin}** **Admin**.
1. Go to **Settings > CI/CD > Continuous Integration and Deployment**.
1. Select **Default to Auto DevOps pipeline for all projects** to enable it.
1. (Optional) You can set up the Auto DevOps [base domain](#auto-devops-base-domain),
for Auto Deploy and Auto Review Apps to use.
@ -222,13 +223,13 @@ The Auto DevOps base domain is required to use
[Auto Monitoring](stages.md#auto-monitoring). You can define the base domain in
any of the following places:
- either under the cluster's settings, whether for an instance,
- Either under the cluster's settings, whether for an instance,
[projects](../../user/project/clusters/index.md#base-domain) or
[groups](../../user/group/clusters/index.md#base-domain)
- or at the project level as a variable: `KUBE_INGRESS_BASE_DOMAIN`
- or at the group level as a variable: `KUBE_INGRESS_BASE_DOMAIN`
- or as an instance-wide fallback in **Admin Area > Settings > CI/CD** under the
**Continuous Integration and Delivery** section
- Or at the project level as a variable: `KUBE_INGRESS_BASE_DOMAIN`
- Or at the group level as a variable: `KUBE_INGRESS_BASE_DOMAIN`
- Or as an instance-wide fallback in **Menu >** **{admin}** **Admin >**
**Settings > CI/CD** under the **Continuous Integration and Delivery** section.
The base domain variable `KUBE_INGRESS_BASE_DOMAIN` follows the same order of precedence
as other environment [variables](../../ci/variables/README.md#cicd-variable-precedence).

View File

@ -743,7 +743,7 @@ DAST can be [configured](#customizing-the-dast-settings) using CI/CD variables.
| `DAST_SUBMIT_FIELD` | selector | A selector describing the element that when clicked submits the login form, or the password form of a multi-page login process. Example: `xpath://input[@value='Login']`. [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9894) in GitLab 12.4. |
| `DAST_FIRST_SUBMIT_FIELD` | selector | A selector describing the element that when clicked submits the username form of a multi-page login process. Example: `.submit`. [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9894) in GitLab 12.4. |
| `DAST_ZAP_CLI_OPTIONS` | string | ZAP server command-line options. For example, `-Xmx3072m` would set the Java maximum memory allocation pool size. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |
| `DAST_ZAP_LOG_CONFIGURATION` | string | Set to a semicolon-separated list of additional log4j properties for the ZAP Server. For example, `log4j.logger.org.parosproxy.paros.network.HttpSender=DEBUG;log4j.logger.com.crawljax=DEBUG` |
| `DAST_ZAP_LOG_CONFIGURATION` | string | Set to a semicolon-separated list of additional log4j properties for the ZAP Server. |
| `DAST_AGGREGATE_VULNERABILITIES` | boolean | Vulnerability aggregation is set to `true` by default. To disable this feature and see each vulnerability individually set to `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/254043) in GitLab 14.0. |
| `DAST_MAX_URLS_PER_VULNERABILITY` | number | The maximum number of URLs reported for a single vulnerability. `DAST_MAX_URLS_PER_VULNERABILITY` is set to `50` by default. To list all the URLs set to `0`. [Introduced](https://gitlab.com/gitlab-org/security-products/dast/-/merge_requests/433) in GitLab 13.12. |
| `DAST_AUTH_REPORT` | boolean | Used in combination with exporting the `gl-dast-debug-auth-report.html` artifact to aid in debugging authentication issues. |
@ -880,6 +880,8 @@ Debug mode of the ZAP server can be enabled using the `DAST_ZAP_LOG_CONFIGURATIO
The following table outlines examples of values that can be set and the effect that they have on the output that is logged.
Multiple values can be specified, separated by semicolons.
For example, `log4j.logger.org.parosproxy.paros.network.HttpSender=DEBUG;log4j.logger.com.crawljax=DEBUG`.
| Log configuration value | Effect |
|-------------------------------------------------- | ----------------------------------------------------------------- |
| `log4j.rootLogger=DEBUG` | Enable all debug logging statements. |

View File

@ -45,12 +45,11 @@ To use a cluster management project for a cluster:
To select a cluster management project to use:
1. Navigate to the appropriate configuration page. For a:
- [Project-level cluster](../project/clusters/index.md), navigate to your project's
- [Project-level cluster](../project/clusters/index.md), go to your project's
**Infrastructure > Kubernetes clusters** page.
- [Group-level cluster](../group/clusters/index.md), navigate to your group's **Kubernetes**
page.
- [Instance-level cluster](../instance/clusters/index.md), navigate to Admin Area's **Kubernetes**
- [Group-level cluster](../group/clusters/index.md), go to your group's **Kubernetes**
page.
- [Instance-level cluster](../instance/clusters/index.md), go to **Menu >** **{admin}** **Admin > Kubernetes** page.
1. Select the project using **Cluster management project field** in the **Advanced settings**
section.

View File

@ -10,33 +10,47 @@ type: reference, howto
GitLab encourages communication through comments, threads, and
[code suggestions](../project/merge_requests/reviews/suggestions.md).
For example, you can create a comment in the following places:
There are two types of comments:
- Issues
- A standard comment.
- A comment in a thread, which has to be resolved.
In a comment, you can enter [Markdown](../markdown.md) and use [quick actions](../project/quick_actions.md).
You can [suggest code changes](../project/merge_requests/reviews/suggestions.md) in your commit diff comment,
which the user can accept through the user interface.
## Where you can create comments
You can create comments in places like:
- Commit diffs
- Commits
- Designs
- Epics
- Issues
- Merge requests
- Snippets
- Commits
- Commit diffs
There are standard comments, and you also have the option to create a comment
in the form of a thread. A comment can also be [turned into a thread](#start-a-thread-by-replying-to-a-standard-comment)
when it receives a reply.
Each object can have as many as 5,000 comments.
The comment area supports [Markdown](../markdown.md) and [quick actions](../project/quick_actions.md).
You can [suggest code changes](../project/merge_requests/reviews/suggestions.md) in your comment,
which the user can accept through the user interface. You can edit your own
comment at any time, and anyone with the [Maintainer role](../permissions.md) or
## Reply to a comment by sending email
If you have ["reply by email"](../../administration/reply_by_email.md) configured,
you can reply to comments by sending an email.
- When you reply to a standard comment, another standard comment is created.
- When you reply to a threaded comment, it creates a reply in the thread.
You can use [Markdown](../markdown.md) and [quick actions](../project/quick_actions.md) in your email replies.
## Who can edit comments
You can edit your own comment at any time.
Anyone with the [Maintainer role](../permissions.md) or
higher can also edit a comment made by someone else.
You can also reply to a comment notification email to reply to the comment if
[Reply by email](../../administration/reply_by_email.md) is configured for your GitLab instance. Replying to a standard comment
creates another standard comment. Replying to a threaded comment creates a reply in the thread. Email replies support
[Markdown](../markdown.md) and [quick actions](../project/quick_actions.md), just as if you replied from the web.
NOTE:
There is a limit of 5,000 comments for every object, for example: issue, epic, and merge request.
## Resolvable comments and threads
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5022) in GitLab 8.11.

View File

@ -8,12 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3090) in GitLab 12.2 for subgroups.
With Contribution Analytics you can get an overview of the following activity in your
group:
- Issues
- Merge requests
- Push events
With Contribution Analytics you can get an overview of the [contribution actions](../../../api/events.md#action-types) in your
group.
To view the Contribution Analytics, go to your group and select **Analytics > Contribution**.

View File

@ -8,6 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/20050) in GitLab Premium 12.8.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Free in 13.3.
> - Symbol package support [added](https://gitlab.com/gitlab-org/gitlab/-/issues/262081) in GitLab 14.1.
Publish NuGet packages in your project's Package Registry. Then, install the
packages whenever you need to use them as a dependency.
@ -394,6 +395,24 @@ dotnet add package <package_id> \
- `<package_id>` is the package ID.
- `<package_version>` is the package version. Optional.
## Symbol packages
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/262081) in GitLab 14.1.
If you push a `.nupkg`, symbol package files in the `.snupkg` format are uploaded automatically. You
can also push them manually:
```shell
nuget push My.Package.snupkg -Source <source_name>
```
Consuming symbol packages is not yet guaranteed using clients such as Visual Studio or
dotnet-symbol. The `.snupkg` files are available for download through the UI or the
[API](../../../api/packages/nuget.md#download-a-package-file).
Follow the [NuGet symbol package issue](https://gitlab.com/gitlab-org/gitlab/-/issues/262081)
for further updates.
## Supported CLI commands
The GitLab NuGet repository supports the following commands for the NuGet CLI (`nuget`) and the .NET

View File

@ -98,7 +98,7 @@ To add links to other accounts:
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/14078) in GitLab 11.3.
In the user contribution calendar graph and recent activity list, you can show contributions to private projects.
In the user contribution calendar graph and recent activity list, you can see your [contribution actions](../../api/events.md#action-types) to private projects.
To show private contributions:

View File

@ -16,6 +16,7 @@ module API
feature_category :package_registry
PACKAGE_FILENAME = 'package.nupkg'
SYMBOL_PACKAGE_FILENAME = 'package.snupkg'
default_format :json
@ -33,6 +34,10 @@ module API
end
helpers do
params :file_params do
requires :package, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
end
def project_or_group
authorized_user_project
end
@ -40,6 +45,49 @@ module API
def snowplow_gitlab_standard_context
{ project: authorized_user_project, namespace: authorized_user_project.namespace }
end
def authorize_nuget_upload
authorize_workhorse!(
subject: project_or_group,
has_length: false,
maximum_size: project_or_group.actual_limits.nuget_max_file_size
)
end
def temp_file_name(symbol_package)
return ::Packages::Nuget::TEMPORARY_SYMBOL_PACKAGE_NAME if symbol_package
::Packages::Nuget::TEMPORARY_PACKAGE_NAME
end
def file_name(symbol_package)
return SYMBOL_PACKAGE_FILENAME if symbol_package
PACKAGE_FILENAME
end
def upload_nuget_package_file(symbol_package: false)
authorize_upload!(project_or_group)
bad_request!('File is too large') if project_or_group.actual_limits.exceeded?(:nuget_max_file_size, params[:package].size)
file_params = params.merge(
file: params[:package],
file_name: file_name(symbol_package)
)
package = ::Packages::CreateTemporaryPackageService.new(
project_or_group, current_user, declared_params.merge(build: current_authenticated_job)
).execute(:nuget, name: temp_file_name(symbol_package))
package_file = ::Packages::CreatePackageFileService.new(package, file_params.merge(build: current_authenticated_job))
.execute
yield(package) if block_given?
::Packages::Nuget::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker
created!
end
end
params do
@ -55,40 +103,45 @@ module API
end
params do
requires :package, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
use :file_params
end
put do
authorize_upload!(project_or_group)
bad_request!('File is too large') if project_or_group.actual_limits.exceeded?(:nuget_max_file_size, params[:package].size)
file_params = params.merge(
file: params[:package],
file_name: PACKAGE_FILENAME
)
package = ::Packages::CreateTemporaryPackageService.new(
project_or_group, current_user, declared_params.merge(build: current_authenticated_job)
).execute(:nuget, name: ::Packages::Nuget::TEMPORARY_PACKAGE_NAME)
package_file = ::Packages::CreatePackageFileService.new(package, file_params.merge(build: current_authenticated_job))
.execute
track_package_event('push_package', :nuget, category: 'API::NugetPackages', user: current_user, project: package.project, namespace: package.project.namespace)
::Packages::Nuget::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker
created!
upload_nuget_package_file do |package|
track_package_event(
'push_package',
:nuget,
category: 'API::NugetPackages',
user: current_user,
project: package.project,
namespace: package.project.namespace
)
end
rescue ObjectStorage::RemoteStoreError => e
Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
forbidden!
end
put 'authorize' do
authorize_workhorse!(
subject: project_or_group,
has_length: false,
maximum_size: project_or_group.actual_limits.nuget_max_file_size
)
authorize_nuget_upload
end
# https://docs.microsoft.com/en-us/nuget/api/symbol-package-publish-resource
desc 'The NuGet Symbol Package Publish endpoint' do
detail 'This feature was introduced in GitLab 14.1'
end
params do
use :file_params
end
put 'symbolpackage' do
upload_nuget_package_file(symbol_package: true)
rescue ObjectStorage::RemoteStoreError => e
Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
forbidden!
end
put 'symbolpackage/authorize' do
authorize_nuget_upload
end
# https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource
@ -115,14 +168,22 @@ module API
requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX
requires :package_filename, type: String, desc: 'The NuGet package filename', regexp: API::NO_SLASH_URL_PART_REGEX
end
get '*package_version/*package_filename', format: :nupkg do
get '*package_version/*package_filename', format: [:nupkg, :snupkg] do
filename = "#{params[:package_filename]}.#{params[:format]}"
package_file = ::Packages::PackageFileFinder.new(find_package(params[:package_name], params[:package_version]), filename, with_file_name_like: true)
.execute
not_found!('Package') unless package_file
track_package_event('pull_package', :nuget, category: 'API::NugetPackages', project: package_file.project, namespace: package_file.project.namespace)
if params[:format] == 'nupkg'
track_package_event(
'pull_package',
:nuget,
category: 'API::NugetPackages',
project: package_file.project,
namespace: package_file.project.namespace
)
end
# nuget and dotnet don't support 302 Moved status codes, supports_direct_download has to be set to false
present_carrierwave_file!(package_file.file, supports_direct_download: false)

View File

@ -50,8 +50,6 @@ module Gitlab
def load_status
return if loaded?
return unless Gitlab::Ci::Features.pipeline_status_omit_commit_sha_in_cache_key?(project) || commit
if has_cache?
load_from_cache
else
@ -119,11 +117,7 @@ module Gitlab
end
def cache_key
if Gitlab::Ci::Features.pipeline_status_omit_commit_sha_in_cache_key?(project)
"#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:project:#{project.id}:pipeline_status"
else
"#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:project:#{project.id}:pipeline_status:#{commit&.sha}"
end
"#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:project:#{project.id}:pipeline_status"
end
def commit

View File

@ -14,10 +14,6 @@ module Gitlab
::Feature.enabled?(:ci_pipeline_latest, default_enabled: true)
end
def self.pipeline_status_omit_commit_sha_in_cache_key?(project)
Feature.enabled?(:ci_pipeline_status_omit_commit_sha_in_cache_key, project, default_enabled: true)
end
# NOTE: The feature flag `disallow_to_create_merge_request_pipelines_in_target_project`
# is a safe switch to disable the feature for a particular project when something went wrong,
# therefore it's not supposed to be enabled by default.

View File

@ -11,6 +11,7 @@ module Gitlab
module Experimentation
module ControllerConcern
include ::Gitlab::Experimentation::GroupTypes
include Gitlab::Tracking::Helpers
extend ActiveSupport::Concern
included do
@ -101,10 +102,6 @@ module Gitlab
private
def dnt_enabled?
Gitlab::Utils.to_boolean(request.headers['DNT'])
end
def experimentation_subject_id
cookies.signed[:experimentation_subject_id]
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Gitlab
module Tracking
module Helpers
def dnt_enabled?
Gitlab::Utils.to_boolean(request.headers['DNT'])
end
def trackable_html_request?
request.format.html? && !dnt_enabled?
end
end
end
end

View File

@ -1,7 +1,5 @@
# frozen_string_literal: true
return if Rails.env.production?
namespace :gitlab do
namespace :sidekiq do
def write_yaml(path, banner, object)
@ -31,9 +29,13 @@ namespace :gitlab do
end
end
task :not_production do
raise 'This task cannot be run in the production environment' if Rails.env.production?
end
namespace :all_queues_yml do
desc 'GitLab | Sidekiq | Generate all_queues.yml based on worker definitions'
task generate: :environment do
task generate: ['gitlab:sidekiq:not_production', :environment] do
banner = <<~BANNER
# This file is generated automatically by
# bin/rake gitlab:sidekiq:all_queues_yml:generate
@ -51,7 +53,7 @@ namespace :gitlab do
end
desc 'GitLab | Sidekiq | Validate that all_queues.yml matches worker definitions'
task check: :environment do
task check: ['gitlab:sidekiq:not_production', :environment] do
if Gitlab::SidekiqConfig.all_queues_yml_outdated?
raise <<~MSG
Changes in worker queues found, please update the metadata by running:
@ -70,7 +72,7 @@ namespace :gitlab do
namespace :sidekiq_queues_yml do
desc 'GitLab | Sidekiq | Generate sidekiq_queues.yml based on worker definitions'
task generate: :environment do
task generate: ['gitlab:sidekiq:not_production', :environment] do
banner = <<~BANNER
# This file is generated automatically by
# bin/rake gitlab:sidekiq:sidekiq_queues_yml:generate
@ -104,7 +106,7 @@ namespace :gitlab do
end
desc 'GitLab | Sidekiq | Validate that sidekiq_queues.yml matches worker definitions'
task check: :environment do
task check: ['gitlab:sidekiq:not_production', :environment] do
if Gitlab::SidekiqConfig.sidekiq_queues_yml_outdated?
raise <<~MSG
Changes in worker queues found, please update the metadata by running:

View File

@ -3405,9 +3405,6 @@ msgstr ""
msgid "Amazon EKS integration allows you to provision EKS clusters from GitLab."
msgstr ""
msgid "Amazon Web Services"
msgstr ""
msgid "Amazon Web Services Logo"
msgstr ""
@ -6928,21 +6925,9 @@ msgstr ""
msgid "ClusterApplicationsRemoved|One-click application management was removed in GitLab 14.0. Your applications are still installed in your cluster, and integrations continue working."
msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|%{boldStart}Note:%{boldEnd} Requires Ingress to be installed."
msgstr ""
msgid "ClusterIntegration|%{linkStart}More information%{linkEnd}"
msgstr ""
msgid "ClusterIntegration|%{title} uninstalled successfully."
msgstr ""
msgid "ClusterIntegration|%{title} updated successfully."
msgstr ""
msgid "ClusterIntegration|A cluster management project can be used to run deployment jobs with Kubernetes %{code_open}cluster-admin%{code_close} privileges."
msgstr ""
@ -6976,12 +6961,6 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes clusters integration"
msgstr ""
msgid "ClusterIntegration|All data not committed to GitLab will be deleted and cannot be restored."
msgstr ""
msgid "ClusterIntegration|All data will be deleted and cannot be restored."
msgstr ""
msgid "ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster. %{linkStart}More information%{linkEnd}"
msgstr ""
@ -7015,9 +6994,6 @@ msgstr ""
msgid "ClusterIntegration|Any project namespaces"
msgstr ""
msgid "ClusterIntegration|Any running pipelines will be canceled."
msgstr ""
msgid "ClusterIntegration|Apply for credit"
msgstr ""
@ -7036,15 +7012,6 @@ msgstr ""
msgid "ClusterIntegration|CA Certificate"
msgstr ""
msgid "ClusterIntegration|Can be safely removed. Prior to GitLab 13.2, GitLab used a remote Tiller server to manage the applications. GitLab no longer uses this server. Uninstalling this server will not affect your other applications. This row will disappear afterwards."
msgstr ""
msgid "ClusterIntegration|Cert-Manager"
msgstr ""
msgid "ClusterIntegration|Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert-Manager on your cluster will issue a certificate by %{linkStart}Let's Encrypt%{linkEnd} and ensure that certificates are valid and up-to-date."
msgstr ""
msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
msgstr ""
@ -7066,9 +7033,6 @@ msgstr ""
msgid "ClusterIntegration|Choose the worker node %{linkStart}instance type%{linkEnd}."
msgstr ""
msgid "ClusterIntegration|Choose which applications to install on your Kubernetes cluster."
msgstr ""
msgid "ClusterIntegration|Choose which of your environments will use this cluster."
msgstr ""
@ -7108,15 +7072,6 @@ msgstr ""
msgid "ClusterIntegration|Copy CA Certificate"
msgstr ""
msgid "ClusterIntegration|Copy Ingress Endpoint"
msgstr ""
msgid "ClusterIntegration|Copy Jupyter Hostname"
msgstr ""
msgid "ClusterIntegration|Copy Knative Endpoint"
msgstr ""
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
@ -7165,12 +7120,6 @@ msgstr ""
msgid "ClusterIntegration|Creating Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|Crossplane"
msgstr ""
msgid "ClusterIntegration|Crossplane enables declarative provisioning of managed services from your cloud of choice using %{codeStart}kubectl%{codeEnd} or %{linkStart}GitLab Integration%{linkEnd}. Crossplane runs inside your Kubernetes cluster and supports secure connectivity and secrets management between app containers and the cloud services they depend on."
msgstr ""
msgid "ClusterIntegration|Deletes all GitLab resources attached to this cluster during removal"
msgstr ""
@ -7186,9 +7135,6 @@ msgstr ""
msgid "ClusterIntegration|Elastic Kubernetes Service"
msgstr ""
msgid "ClusterIntegration|Elastic Stack"
msgstr ""
msgid "ClusterIntegration|Enable Cloud Run for Anthos"
msgstr ""
@ -7204,9 +7150,6 @@ msgstr ""
msgid "ClusterIntegration|Enable this setting if using role-based access control (RBAC)."
msgstr ""
msgid "ClusterIntegration|Enabled stack"
msgstr ""
msgid "ClusterIntegration|Enter new Service Token"
msgstr ""
@ -7252,18 +7195,9 @@ msgstr ""
msgid "ClusterIntegration|GitLab Agent managed clusters"
msgstr ""
msgid "ClusterIntegration|GitLab Container Network Policies"
msgstr ""
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
msgid "ClusterIntegration|GitLab Runner"
msgstr ""
msgid "ClusterIntegration|GitLab Runner connects to the repository and executes CI/CD jobs, pushing results back and deploying applications to production."
msgstr ""
msgid "ClusterIntegration|GitLab failed to authenticate."
msgstr ""
@ -7300,18 +7234,6 @@ msgstr ""
msgid "ClusterIntegration|In order to view the health of your cluster, you must first enable Prometheus in the Integrations tab."
msgstr ""
msgid "ClusterIntegration|Ingress"
msgstr ""
msgid "ClusterIntegration|Ingress Endpoint"
msgstr ""
msgid "ClusterIntegration|Ingress gives you a way to route requests to services based on the request host or path, centralizing a number of services into a single entrypoint."
msgstr ""
msgid "ClusterIntegration|Installing Ingress may incur additional costs. Learn more about %{linkStart}pricing%{linkEnd}."
msgstr ""
msgid "ClusterIntegration|Instance cluster"
msgstr ""
@ -7333,39 +7255,9 @@ msgstr ""
msgid "ClusterIntegration|Integrations allow you to use applications installed in your cluster as part of your GitLab workflow."
msgstr ""
msgid "ClusterIntegration|Issuer Email"
msgstr ""
msgid "ClusterIntegration|Issuers represent a certificate authority. You must provide an email address for your Issuer."
msgstr ""
msgid "ClusterIntegration|Jupyter Hostname"
msgstr ""
msgid "ClusterIntegration|JupyterHub"
msgstr ""
msgid "ClusterIntegration|JupyterHub, a multi-user Hub, spawns, manages, and proxies multiple instances of the single-user Jupyter notebook server. JupyterHub can be used to serve notebooks to a class of students, a corporate data science group, or a scientific research group."
msgstr ""
msgid "ClusterIntegration|Key pair name"
msgstr ""
msgid "ClusterIntegration|Knative"
msgstr ""
msgid "ClusterIntegration|Knative Domain Name:"
msgstr ""
msgid "ClusterIntegration|Knative Endpoint:"
msgstr ""
msgid "ClusterIntegration|Knative domain name was updated successfully."
msgstr ""
msgid "ClusterIntegration|Knative extends Kubernetes to provide a set of middleware components that are essential to build modern, source-centric, and container-based applications that can run anywhere: on premises, in the cloud, or even in a third-party data center."
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster is being created..."
msgstr ""
@ -7399,9 +7291,6 @@ msgstr ""
msgid "ClusterIntegration|Learn more about instance Kubernetes clusters"
msgstr ""
msgid "ClusterIntegration|Legacy Helm Tiller server"
msgstr ""
msgid "ClusterIntegration|Loading IAM Roles"
msgstr ""
@ -7492,9 +7381,6 @@ msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
msgid "ClusterIntegration|Point a wildcard DNS to this generated endpoint in order to access your application after it has been deployed."
msgstr ""
msgid "ClusterIntegration|Project cluster"
msgstr ""
@ -7504,15 +7390,6 @@ msgstr ""
msgid "ClusterIntegration|Project namespace prefix (optional, unique)"
msgstr ""
msgid "ClusterIntegration|Prometheus"
msgstr ""
msgid "ClusterIntegration|Prometheus is an open-source monitoring system with %{linkStart}GitLab Integration%{linkEnd} to monitor deployed applications."
msgstr ""
msgid "ClusterIntegration|Protect your clusters with GitLab Container Network Policies by enforcing how pods communicate with each other and other network endpoints. %{linkStart}Learn more about configuring Network Policies here.%{linkEnd}"
msgstr ""
msgid "ClusterIntegration|Provider details"
msgstr ""
@ -7549,15 +7426,6 @@ msgstr ""
msgid "ClusterIntegration|Removes cluster from project but keeps associated resources"
msgstr ""
msgid "ClusterIntegration|Replace this with your own hostname if you want. If you do so, point hostname to Ingress IP Address from above."
msgstr ""
msgid "ClusterIntegration|Request to begin installing failed"
msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
@ -7570,9 +7438,6 @@ msgstr ""
msgid "ClusterIntegration|Search VPCs"
msgstr ""
msgid "ClusterIntegration|Search domains"
msgstr ""
msgid "ClusterIntegration|Search instance types"
msgstr ""
@ -7621,15 +7486,9 @@ msgstr ""
msgid "ClusterIntegration|Select a region to choose a VPC"
msgstr ""
msgid "ClusterIntegration|Select a stack to install Crossplane."
msgstr ""
msgid "ClusterIntegration|Select a zone to choose a network"
msgstr ""
msgid "ClusterIntegration|Select existing domain or use new"
msgstr ""
msgid "ClusterIntegration|Select machine type"
msgstr ""
@ -7672,15 +7531,6 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong while creating your Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|Something went wrong while installing %{title}"
msgstr ""
msgid "ClusterIntegration|Something went wrong while uninstalling %{title}"
msgstr ""
msgid "ClusterIntegration|Something went wrong while updating Knative domain name."
msgstr ""
msgid "ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{linkStart}Auto DevOps.%{linkEnd} The domain should have a wildcard DNS configured matching the domain. "
msgstr ""
@ -7696,24 +7546,6 @@ msgstr ""
msgid "ClusterIntegration|The URL used to access the Kubernetes API."
msgstr ""
msgid "ClusterIntegration|The associated IP and all deployed services will be deleted and cannot be restored. Uninstalling Knative will also remove Istio from your cluster. This will not effect any other applications."
msgstr ""
msgid "ClusterIntegration|The associated Tiller pod will be deleted and cannot be restored. Your other applications will remain unaffected."
msgstr ""
msgid "ClusterIntegration|The associated load balancer and IP will be deleted and cannot be restored."
msgstr ""
msgid "ClusterIntegration|The associated private key will be deleted and cannot be restored."
msgstr ""
msgid "ClusterIntegration|The elastic stack collects logs from all pods in your cluster"
msgstr ""
msgid "ClusterIntegration|The endpoint is in the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time."
msgstr ""
msgid "ClusterIntegration|The namespace associated with your project. This will be used for deploy boards, logs, and Web terminals."
msgstr ""
@ -7744,9 +7576,6 @@ msgstr ""
msgid "ClusterIntegration|This will permanently delete the following resources:"
msgstr ""
msgid "ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint."
msgstr ""
msgid "ClusterIntegration|To create a cluster, first create a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}."
msgstr ""
@ -7768,21 +7597,9 @@ msgstr ""
msgid "ClusterIntegration|Unable to Connect"
msgstr ""
msgid "ClusterIntegration|Uninstall %{appTitle}"
msgstr ""
msgid "ClusterIntegration|Unknown Error"
msgstr ""
msgid "ClusterIntegration|Update %{appTitle}"
msgstr ""
msgid "ClusterIntegration|Update failed. Please check the logs and try again."
msgstr ""
msgid "ClusterIntegration|Use %{query}"
msgstr ""
msgid "ClusterIntegration|Uses the Cloud Run, Istio, and HTTP Load Balancing addons for this cluster."
msgstr ""
@ -7807,27 +7624,12 @@ msgstr ""
msgid "ClusterIntegration|You are about to remove your cluster integration."
msgstr ""
msgid "ClusterIntegration|You are about to uninstall %{appTitle} from your cluster."
msgstr ""
msgid "ClusterIntegration|You are about to update %{appTitle} on your cluster."
msgstr ""
msgid "ClusterIntegration|You must grant access to your organizations AWS resources in order to create a new EKS cluster. To grant access, create a provision role using the account and external ID below and provide us the ARN."
msgstr ""
msgid "ClusterIntegration|You must have an RBAC-enabled cluster to install Knative."
msgstr ""
msgid "ClusterIntegration|You must specify a domain before you can install Knative."
msgstr ""
msgid "ClusterIntegration|You should select at least two subnets"
msgstr ""
msgid "ClusterIntegration|Your Elasticsearch cluster will be re-created during this upgrade. Your logs will be re-indexed, and you will lose historical logs from hosts terminated in the last 30 days."
msgstr ""
msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr ""
@ -7843,9 +7645,6 @@ msgstr ""
msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|installed via %{linkStart}Cloud Run%{linkEnd}"
msgstr ""
msgid "ClusterIntegration|meets the requirements"
msgstr ""
@ -9654,9 +9453,6 @@ msgstr ""
msgid "Cron time zone"
msgstr ""
msgid "Crossplane"
msgstr ""
msgid "Crowd"
msgstr ""
@ -13452,9 +13248,6 @@ msgstr ""
msgid "ExternalWikiService|https://example.com/xxx/wiki/..."
msgstr ""
msgid "Externally installed"
msgstr ""
msgid "Facebook"
msgstr ""
@ -15403,9 +15196,6 @@ msgstr ""
msgid "Goal of the changes and what reviewers should be aware of"
msgstr ""
msgid "Google Cloud Platform"
msgstr ""
msgid "Google authentication is not %{link_start}properly configured%{link_end}. Ask your GitLab administrator if you want to use this service."
msgstr ""
@ -17579,9 +17369,6 @@ msgstr ""
msgid "Insights|This project is filtered out in the insights.yml file (see the projects.only config for more information)."
msgstr ""
msgid "Install"
msgstr ""
msgid "Install GitLab Runner and ensure it's running."
msgstr ""
@ -17594,12 +17381,6 @@ msgstr ""
msgid "Installation"
msgstr ""
msgid "Installed"
msgstr ""
msgid "Installing"
msgstr ""
msgid "Instance"
msgid_plural "Instances"
msgstr[0] ""
@ -19979,9 +19760,6 @@ msgstr ""
msgid "Makes this issue confidential."
msgstr ""
msgid "Manage"
msgstr ""
msgid "Manage Web IDE features."
msgstr ""
@ -21148,9 +20926,6 @@ msgstr ""
msgid "Mi"
msgstr ""
msgid "Microsoft Azure"
msgstr ""
msgid "Middleman project with Static Site Editor support"
msgstr ""
@ -28044,9 +27819,6 @@ msgstr ""
msgid "Retry this job in order to create the necessary resources."
msgstr ""
msgid "Retry update"
msgstr ""
msgid "Retry verification"
msgstr ""
@ -28153,9 +27925,6 @@ msgstr ""
msgid "Rollback"
msgstr ""
msgid "Rook"
msgstr ""
msgid "Ruby"
msgstr ""
@ -29324,9 +29093,6 @@ msgstr ""
msgid "Select Page"
msgstr ""
msgid "Select Stack"
msgstr ""
msgid "Select a branch"
msgstr ""
@ -31623,6 +31389,9 @@ msgstr ""
msgid "SuperSonics|Activate"
msgstr ""
msgid "SuperSonics|Activate cloud license"
msgstr ""
msgid "SuperSonics|Activate subscription"
msgstr ""
@ -34869,12 +34638,6 @@ msgstr ""
msgid "Unhappy?"
msgstr ""
msgid "Uninstall"
msgstr ""
msgid "Uninstalling"
msgstr ""
msgid "Units|ms"
msgstr ""
@ -35100,9 +34863,6 @@ msgstr ""
msgid "Updated %{updated_at} by %{updated_by}"
msgstr ""
msgid "Updated to %{linkStart}chart v%{linkEnd}"
msgstr ""
msgid "Updates"
msgstr ""
@ -37599,9 +37359,6 @@ msgstr ""
msgid "You must provide your current password in order to change it."
msgstr ""
msgid "You must select a stack for configuring your cloud provider. Learn more about"
msgstr ""
msgid "You must solve the CAPTCHA in order to submit"
msgstr ""

View File

@ -77,7 +77,9 @@ module QA
reload: true,
skip_finished_loading_check_on_refresh: true
) do
page.has_no_content?('Importing 1 repository')
# TODO: Refactor to explicitly wait for specific project import successful status
# This check can create false positive if main importing message appears with delay and check exits early
page.has_no_content?('Importing 1 repository', wait: 3)
end
end

View File

@ -6,10 +6,6 @@ module QA
module Infrastructure
module Kubernetes
class Show < Page::Base
view 'app/assets/javascripts/clusters/components/applications.vue' do
element :ingress_ip_address, 'id="ingress-endpoint"' # rubocop:disable QA/ElementWithPattern
end
view 'app/assets/javascripts/clusters/forms/components/integration_form.vue' do
element :integration_status_toggle, required: true
element :base_domain_field, required: true
@ -20,15 +16,6 @@ module QA
element :details, required: true
end
view 'app/views/clusters/clusters/_applications_tab.html.haml' do
element :applications, required: true
end
view 'app/assets/javascripts/clusters/components/application_row.vue' do
element :install_button
element :uninstall_button
end
view 'app/views/clusters/clusters/_health.html.haml' do
element :cluster_health_section
end
@ -42,36 +29,6 @@ module QA
click_element :details
end
def open_applications
has_element?(:applications, wait: 30)
click_element :applications
end
def install!(application_name)
within_element(application_name) do
has_element?(:install_button, application: application_name, wait: 30)
click_element :install_button
end
end
def await_installed(application_name)
within_element(application_name) do
has_element?(:uninstall_button, application: application_name, wait: 300, skip_finished_loading_check: true)
end
end
def has_application_installed?(application_name)
within_element(application_name) do
has_element?(:uninstall_button, application: application_name, wait: 300)
end
end
def ingress_ip
# We need to wait longer since it can take some time before the
# ip address is assigned for the ingress controller
page.find('#ingress-endpoint', wait: 1200).value
end
def set_domain(domain)
fill_element :base_domain_field, domain
end

View File

@ -3,6 +3,8 @@
module QA
module Resource
module KubernetesCluster
# TODO: This resource is currently broken, since one-click apps have been removed.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/333818
class ProjectCluster < Base
attr_writer :cluster,
:install_ingress, :install_prometheus, :install_runner, :domain
@ -40,6 +42,8 @@ module QA
# We must wait a few seconds for permissions to be set up correctly for new cluster
sleep 25
# TODO: These steps do not work anymore, see https://gitlab.com/gitlab-org/gitlab/-/issues/333818
# Open applications tab
show.open_applications

View File

@ -162,6 +162,12 @@ FactoryBot.define do
pkg.nuget_metadatum = build(:nuget_metadatum)
end
end
trait(:with_symbol_package) do
after :create do |package|
create :package_file, :snupkg, package: package, file_name: "#{package.name}.#{package.version}.snupkg"
end
end
end
factory :pypi_package do

View File

@ -271,6 +271,14 @@ FactoryBot.define do
size { 300.kilobytes }
end
trait(:snupkg) do
package
file_fixture { 'spec/fixtures/packages/nuget/package.snupkg' }
file_name { 'package.snupkg' }
file_sha1 { '5fe852b2a6abd96c22c11fa1ff2fb19d9ce58b57' }
size { 300.kilobytes }
end
trait(:gem) do
package
file_fixture { 'spec/fixtures/packages/rubygems/package-0.0.1.gem' }

View File

@ -259,8 +259,8 @@ RSpec.describe 'Merge request > Batch comments', :js do
end
def write_parallel_comment(line, **params)
find("td[id='#{line}']").hover
find(".is-over button").click
find("div[id='#{line}']").hover
find(".js-add-diff-note-button").click
write_comment(selector: "form[data-line-code='#{line}']", **params)
end

View File

@ -132,7 +132,7 @@ RSpec.describe 'User comments on a diff', :js do
# In `files/ruby/popen.rb`
it 'allows comments for changes involving both sides' do
# click +15, select -13 add and verify comment
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .new_line a[data-linenumber="15"]').find(:xpath, '../..'), 'right')
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .right-side a[data-linenumber="15"]').find(:xpath, '../../..'), 'right')
add_comment('-13', '+15')
end
@ -141,7 +141,7 @@ RSpec.describe 'User comments on a diff', :js do
page.within('[data-path="files/ruby/popen.rb"]') do
all('.js-unfold-all')[0].click
end
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .old_line a[data-linenumber="9"]').find(:xpath, '../..'), 'left')
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .left-side a[data-linenumber="9"]').find(:xpath, '../..'), 'left')
add_comment('1', '-9')
end
@ -150,7 +150,7 @@ RSpec.describe 'User comments on a diff', :js do
page.within('[data-path="files/ruby/popen.rb"]') do
all('.js-unfold-all')[1].click
end
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .old_line a[data-linenumber="21"]').find(:xpath, '../..'), 'left')
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .left-side a[data-linenumber="21"]').find(:xpath, '../..'), 'left')
add_comment('18', '21')
end
@ -159,7 +159,7 @@ RSpec.describe 'User comments on a diff', :js do
page.within('[data-path="files/ruby/popen.rb"]') do
all('.js-unfold-down')[1].click
end
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .old_line a[data-linenumber="30"]').find(:xpath, '../..'), 'left')
click_diff_line(find('div[data-path="files/ruby/popen.rb"] .left-side a[data-linenumber="30"]').find(:xpath, '../..'), 'left')
add_comment('+28', '37')
end
end

View File

@ -16,14 +16,14 @@ RSpec.describe 'Batch diffs', :js do
wait_for_requests
# Add discussion to first line of first file
click_diff_line(find('.diff-file.file-holder:first-of-type tr.line_holder.new:first-of-type'))
click_diff_line(find('.diff-file.file-holder:first-of-type .line_holder .left-side:first-of-type'))
page.within('.js-discussion-note-form') do
fill_in('note_note', with: 'First Line Comment')
click_button('Add comment now')
end
# Add discussion to first line of last file
click_diff_line(find('.diff-file.file-holder:last-of-type tr.line_holder.new:first-of-type'))
click_diff_line(find('.diff-file.file-holder:last-of-type .line_holder .left-side:first-of-type'))
page.within('.js-discussion-note-form') do
fill_in('note_note', with: 'Last Line Comment')
click_button('Add comment now')

View File

@ -10,7 +10,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
let(:user) { project.creator }
let(:comment_button_class) { '.add-diff-note' }
let(:notes_holder_input_class) { 'js-temp-notes-holder' }
let(:notes_holder_input_xpath) { './following-sibling::*[contains(concat(" ", @class, " "), " notes_holder ")]' }
let(:notes_holder_input_xpath) { '..//following-sibling::*[contains(concat(" ", @class, " "), " notes_holder ")]' }
let(:test_note_comment) { 'this is a test note!' }
before do
@ -27,7 +27,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
context 'with an old line on the left and no line on the right' do
it 'allows commenting on the left side' do
should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'left')
should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]'), 'left')
end
it 'does not allow commenting on the right side' do
@ -67,7 +67,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
context 'with a match line' do
it 'does not allow commenting' do
line_holder = find('.match', match: :first).find(:xpath, '..')
line_holder = find('.match', match: :first)
match_should_not_allow_commenting(line_holder)
end
end
@ -81,17 +81,13 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
wait_for_requests
end
# The first `.js-unfold` unfolds upwards, therefore the first
# `.line_holder` will be an unfolded line.
let(:line_holder) { first('#a5cc2925ca8258af241be7e5b0381edf30266302 .line_holder') }
it 'allows commenting on the left side' do
should_allow_commenting(line_holder, 'left')
should_allow_commenting(first('#a5cc2925ca8258af241be7e5b0381edf30266302 .line_holder [data-testid="left-side"]'))
end
it 'allows commenting on the right side' do
# Automatically shifts comment box to left side.
should_allow_commenting(line_holder, 'right')
should_allow_commenting(first('#a5cc2925ca8258af241be7e5b0381edf30266302 .line_holder [data-testid="right-side"]'))
end
end
end
@ -149,7 +145,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
# The first `.js-unfold` unfolds upwards, therefore the first
# `.line_holder` will be an unfolded line.
let(:line_holder) { first('.line_holder[id="a5cc2925ca8258af241be7e5b0381edf30266302_1_1"]') }
let(:line_holder) { first('[id="a5cc2925ca8258af241be7e5b0381edf30266302_1_1"]') }
it 'allows commenting' do
should_allow_commenting line_holder

View File

@ -30,8 +30,8 @@ RSpec.describe 'Merge request > User sees versions', :js do
line_code = "#{file_id}_#{line_code}"
page.within(diff_file_selector) do
find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover
find(".line_holder[id='#{line_code}'] button").click
first("[id='#{line_code}']").hover
first("[id='#{line_code}'] [role='button']").click
page.within("form[data-line-code='#{line_code}']") do
fill_in "note[note]", with: comment

View File

@ -24,7 +24,7 @@ RSpec.describe 'User views diffs', :js do
page.within('.file-holder[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd"]') do
expect(find('.text-file')).to have_content('fileutils')
expect(page).to have_selector('.new_line [data-linenumber="1"]', count: 1)
expect(page).to have_selector('[data-interop-type="new"] [data-linenumber="1"]')
end
end
@ -32,8 +32,8 @@ RSpec.describe 'User views diffs', :js do
page.within('.file-holder[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd"]') do
all('.js-unfold-all')[1].click
expect(page).to have_selector('.new_line [data-linenumber="24"]', count: 1)
expect(page).not_to have_selector('.new_line [data-linenumber="1"]')
expect(page).to have_selector('[data-interop-type="new"] [data-linenumber="24"]', count: 1)
expect(page).not_to have_selector('[data-interop-type="new"] [data-linenumber="1"]')
end
end

Binary file not shown.

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata>
<id>Test.Package</id>
<version>3.5.2</version>
<authors>Test Author</authors>
<owners>Test Owner</owners>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Package Description</description>
<packageTypes>
<packageType name="SymbolsPackage" />
</packageTypes>
</metadata>
</package>

View File

@ -2,15 +2,12 @@ import MockAdapter from 'axios-mock-adapter';
import { loadHTMLFixture } from 'helpers/fixtures';
import { setTestTimeout } from 'helpers/timeout';
import Clusters from '~/clusters/clusters_bundle';
import { APPLICATION_STATUS, APPLICATIONS, RUNNER } from '~/clusters/constants';
import axios from '~/lib/utils/axios_utils';
import initProjectSelectDropdown from '~/project_select';
jest.mock('~/lib/utils/poll');
jest.mock('~/project_select');
const { INSTALLING, INSTALLABLE, INSTALLED, UNINSTALLING } = APPLICATION_STATUS;
describe('Clusters', () => {
setTestTimeout(1000);
@ -57,67 +54,6 @@ describe('Clusters', () => {
});
});
describe('checkForNewInstalls', () => {
const INITIAL_APP_MAP = {
helm: { status: null, title: 'Helm Tiller' },
ingress: { status: null, title: 'Ingress' },
runner: { status: null, title: 'GitLab Runner' },
};
it('does not show alert when things transition from initial null state to something', () => {
cluster.checkForNewInstalls(INITIAL_APP_MAP, {
...INITIAL_APP_MAP,
helm: { status: INSTALLABLE, title: 'Helm Tiller' },
});
const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
expect(flashMessage).toBeNull();
});
it('shows an alert when something gets newly installed', () => {
cluster.checkForNewInstalls(
{
...INITIAL_APP_MAP,
helm: { status: INSTALLING, title: 'Helm Tiller' },
},
{
...INITIAL_APP_MAP,
helm: { status: INSTALLED, title: 'Helm Tiller' },
},
);
const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
expect(flashMessage).not.toBeNull();
expect(flashMessage.textContent.trim()).toEqual(
'Helm Tiller was successfully installed on your Kubernetes cluster',
);
});
it('shows an alert when multiple things gets newly installed', () => {
cluster.checkForNewInstalls(
{
...INITIAL_APP_MAP,
helm: { status: INSTALLING, title: 'Helm Tiller' },
ingress: { status: INSTALLABLE, title: 'Ingress' },
},
{
...INITIAL_APP_MAP,
helm: { status: INSTALLED, title: 'Helm Tiller' },
ingress: { status: INSTALLED, title: 'Ingress' },
},
);
const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
expect(flashMessage).not.toBeNull();
expect(flashMessage.textContent.trim()).toEqual(
'Helm Tiller, Ingress was successfully installed on your Kubernetes cluster',
);
});
});
describe('updateContainer', () => {
const { location } = window;
@ -237,77 +173,6 @@ describe('Clusters', () => {
});
});
describe('installApplication', () => {
it.each(APPLICATIONS)('tries to install %s', (applicationId, done) => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValue();
cluster.store.state.applications[applicationId].status = INSTALLABLE;
const params = {};
if (applicationId === 'knative') {
params.hostname = 'test-example.com';
}
// eslint-disable-next-line promise/valid-params
cluster
.installApplication({ id: applicationId, params })
.then(() => {
expect(cluster.store.state.applications[applicationId].status).toEqual(INSTALLING);
expect(cluster.store.state.applications[applicationId].requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith(applicationId, params);
done();
})
.catch();
});
it('sets error request status when the request fails', () => {
jest
.spyOn(cluster.service, 'installApplication')
.mockRejectedValueOnce(new Error('STUBBED ERROR'));
cluster.store.state.applications.helm.status = INSTALLABLE;
const promise = cluster.installApplication({ id: 'helm' });
return promise.then(() => {
expect(cluster.store.state.applications.helm.status).toEqual(INSTALLABLE);
expect(cluster.store.state.applications.helm.installFailed).toBe(true);
expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
});
});
});
describe('uninstallApplication', () => {
it.each(APPLICATIONS)('tries to uninstall %s', (applicationId) => {
jest.spyOn(cluster.service, 'uninstallApplication').mockResolvedValueOnce();
cluster.store.state.applications[applicationId].status = INSTALLED;
cluster.uninstallApplication({ id: applicationId });
expect(cluster.store.state.applications[applicationId].status).toEqual(UNINSTALLING);
expect(cluster.store.state.applications[applicationId].requestReason).toEqual(null);
expect(cluster.service.uninstallApplication).toHaveBeenCalledWith(applicationId);
});
it('sets error request status when the uninstall request fails', () => {
jest
.spyOn(cluster.service, 'uninstallApplication')
.mockRejectedValueOnce(new Error('STUBBED ERROR'));
cluster.store.state.applications.helm.status = INSTALLED;
const promise = cluster.uninstallApplication({ id: 'helm' });
return promise.then(() => {
expect(cluster.store.state.applications.helm.status).toEqual(INSTALLED);
expect(cluster.store.state.applications.helm.uninstallFailed).toBe(true);
expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
});
});
});
describe('fetch cluster environments success', () => {
beforeEach(() => {
jest.spyOn(cluster.store, 'toggleFetchEnvironments').mockReturnThis();
@ -328,7 +193,6 @@ describe('Clusters', () => {
describe('handleClusterStatusSuccess', () => {
beforeEach(() => {
jest.spyOn(cluster.store, 'updateStateFromServer').mockReturnThis();
jest.spyOn(cluster, 'checkForNewInstalls').mockReturnThis();
jest.spyOn(cluster, 'updateContainer').mockReturnThis();
cluster.handleClusterStatusSuccess({ data: {} });
});
@ -337,38 +201,8 @@ describe('Clusters', () => {
expect(cluster.store.updateStateFromServer).toHaveBeenCalled();
});
it('checks for new installable apps', () => {
expect(cluster.checkForNewInstalls).toHaveBeenCalled();
});
it('updates message containers', () => {
expect(cluster.updateContainer).toHaveBeenCalled();
});
});
describe('updateApplication', () => {
const params = { version: '1.0.0' };
let storeUpdateApplication;
let installApplication;
beforeEach(() => {
storeUpdateApplication = jest.spyOn(cluster.store, 'updateApplication');
installApplication = jest.spyOn(cluster.service, 'installApplication');
cluster.updateApplication({ id: RUNNER, params });
});
afterEach(() => {
storeUpdateApplication.mockRestore();
installApplication.mockRestore();
});
it('calls store updateApplication method', () => {
expect(storeUpdateApplication).toHaveBeenCalledWith(RUNNER);
});
it('sends installApplication request', () => {
expect(installApplication).toHaveBeenCalledWith(RUNNER, params);
});
});
});

View File

@ -1,105 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Applications Cert-Manager application shows the correct description 1`] = `
<p
data-testid="certManagerDescription"
>
Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert-Manager on your cluster will issue a certificate by
<a
class="gl-link"
href="https://letsencrypt.org/"
rel="noopener noreferrer"
target="_blank"
>
Let's Encrypt
</a>
and ensure that certificates are valid and up-to-date.
</p>
`;
exports[`Applications Cilium application shows the correct description 1`] = `
<p
data-testid="ciliumDescription"
>
Protect your clusters with GitLab Container Network Policies by enforcing how pods communicate with each other and other network endpoints.
<a
class="gl-link"
href="cilium-help-path"
rel="noopener"
target="_blank"
>
Learn more about configuring Network Policies here.
</a>
</p>
`;
exports[`Applications Crossplane application shows the correct description 1`] = `
<p
data-testid="crossplaneDescription"
>
Crossplane enables declarative provisioning of managed services from your cloud of choice using
<code>
kubectl
</code>
or
<a
class="gl-link"
href="https://docs.gitlab.com/ee/user/clusters/applications.html#crossplane"
rel="noopener noreferrer"
target="_blank"
>
GitLab Integration
</a>
. Crossplane runs inside your Kubernetes cluster and supports secure connectivity and secrets management between app containers and the cloud services they depend on.
</p>
`;
exports[`Applications Ingress application shows the correct warning message 1`] = `
<span
data-testid="ingressCostWarning"
>
Installing Ingress may incur additional costs. Learn more about
<a
class="gl-link"
href="https://cloud.google.com/compute/pricing#lb"
rel="noopener noreferrer"
target="_blank"
>
pricing
</a>
.
</span>
`;
exports[`Applications Knative application shows the correct description 1`] = `
<span
data-testid="installed-via"
>
installed via
<a
class="gl-link"
href=""
rel="noopener"
target="_blank"
>
Cloud Run
</a>
</span>
`;
exports[`Applications Prometheus application shows the correct description 1`] = `
<span
data-testid="prometheusDescription"
>
Prometheus is an open-source monitoring system with
<a
class="gl-link"
href="https://docs.gitlab.com/ee/user/project/integrations/prometheus.html"
rel="noopener noreferrer"
target="_blank"
>
GitLab Integration
</a>
to monitor deployed applications.
</span>
`;

View File

@ -1,505 +0,0 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ApplicationRow from '~/clusters/components/application_row.vue';
import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue';
import UpdateApplicationConfirmationModal from '~/clusters/components/update_application_confirmation_modal.vue';
import { APPLICATION_STATUS, ELASTIC_STACK } from '~/clusters/constants';
import eventHub from '~/clusters/event_hub';
import { DEFAULT_APPLICATION_STATE } from '../services/mock_data';
describe('Application Row', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
});
const mountComponent = (data) => {
wrapper = shallowMount(ApplicationRow, {
stubs: { GlSprintf },
propsData: {
...DEFAULT_APPLICATION_STATE,
...data,
},
});
};
describe('Title', () => {
it('shows title', () => {
mountComponent({ titleLink: null });
const title = wrapper.find('.js-cluster-application-title');
expect(title.element).toBeInstanceOf(HTMLSpanElement);
expect(title.text()).toEqual(DEFAULT_APPLICATION_STATE.title);
});
it('shows title link', () => {
expect(DEFAULT_APPLICATION_STATE.titleLink).toBeDefined();
mountComponent();
const title = wrapper.find('.js-cluster-application-title');
expect(title.element).toBeInstanceOf(HTMLAnchorElement);
expect(title.text()).toEqual(DEFAULT_APPLICATION_STATE.title);
});
});
describe('Install button', () => {
const button = () => wrapper.find('.js-cluster-application-install-button');
const checkButtonState = (label, loading, disabled) => {
expect(button().text()).toEqual(label);
expect(button().props('loading')).toEqual(loading);
expect(button().props('disabled')).toEqual(disabled);
};
it('has indeterminate state on page load', () => {
mountComponent({ status: null });
expect(button().text()).toBe('');
});
it('has install button', () => {
mountComponent();
expect(button().exists()).toBe(true);
});
it('has disabled "Install" when APPLICATION_STATUS.NOT_INSTALLABLE', () => {
mountComponent({ status: APPLICATION_STATUS.NOT_INSTALLABLE });
checkButtonState('Install', false, true);
});
it('has enabled "Install" when APPLICATION_STATUS.INSTALLABLE', () => {
mountComponent({ status: APPLICATION_STATUS.INSTALLABLE });
checkButtonState('Install', false, false);
});
it('has loading "Installing" when APPLICATION_STATUS.INSTALLING', () => {
mountComponent({ status: APPLICATION_STATUS.INSTALLING });
checkButtonState('Installing', true, true);
});
it('has disabled "Install" when APPLICATION_STATUS.UNINSTALLED', () => {
mountComponent({ status: APPLICATION_STATUS.UNINSTALLED });
checkButtonState('Install', false, true);
});
it('has disabled "Externally installed" when APPLICATION_STATUS.EXTERNALLY_INSTALLED', () => {
mountComponent({ status: APPLICATION_STATUS.EXTERNALLY_INSTALLED });
checkButtonState('Externally installed', false, true);
});
it('has disabled "Installed" when application is installed and not uninstallable', () => {
mountComponent({
status: APPLICATION_STATUS.INSTALLED,
installed: true,
uninstallable: false,
});
checkButtonState('Installed', false, true);
});
it('hides when application is installed and uninstallable', () => {
mountComponent({
status: APPLICATION_STATUS.INSTALLED,
installed: true,
uninstallable: true,
});
expect(button().exists()).toBe(false);
});
it('has enabled "Install" when install fails', () => {
mountComponent({
status: APPLICATION_STATUS.INSTALLABLE,
installFailed: true,
});
checkButtonState('Install', false, false);
});
it('has disabled "Install" when installation disabled', () => {
mountComponent({
status: APPLICATION_STATUS.INSTALLABLE,
installable: false,
});
checkButtonState('Install', false, true);
});
it('has enabled "Install" when REQUEST_FAILURE (so you can try installing again)', () => {
mountComponent({ status: APPLICATION_STATUS.INSTALLABLE });
checkButtonState('Install', false, false);
});
it('clicking install button emits event', () => {
const spy = jest.spyOn(eventHub, '$emit');
mountComponent({ status: APPLICATION_STATUS.INSTALLABLE });
button().vm.$emit('click');
expect(spy).toHaveBeenCalledWith('installApplication', {
id: DEFAULT_APPLICATION_STATE.id,
params: {},
});
});
it('clicking install button when installApplicationRequestParams are provided emits event', () => {
const spy = jest.spyOn(eventHub, '$emit');
mountComponent({
status: APPLICATION_STATUS.INSTALLABLE,
installApplicationRequestParams: { hostname: 'jupyter' },
});
button().vm.$emit('click');
expect(spy).toHaveBeenCalledWith('installApplication', {
id: DEFAULT_APPLICATION_STATE.id,
params: { hostname: 'jupyter' },
});
});
it('clicking disabled install button emits nothing', () => {
const spy = jest.spyOn(eventHub, '$emit');
mountComponent({ status: APPLICATION_STATUS.INSTALLING });
expect(button().props('disabled')).toEqual(true);
button().vm.$emit('click');
expect(spy).not.toHaveBeenCalled();
});
});
describe('Uninstall button', () => {
it('displays button when app is installed and uninstallable', () => {
mountComponent({
installed: true,
uninstallable: true,
status: APPLICATION_STATUS.NOT_INSTALLABLE,
});
const uninstallButton = wrapper.find('.js-cluster-application-uninstall-button');
expect(uninstallButton.exists()).toBe(true);
});
it('displays a success toast message if application uninstall was successful', async () => {
mountComponent({
title: 'GitLab Runner',
uninstallSuccessful: false,
});
wrapper.vm.$toast = { show: jest.fn() };
wrapper.setProps({ uninstallSuccessful: true });
await wrapper.vm.$nextTick();
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
'GitLab Runner uninstalled successfully.',
);
});
});
describe('when confirmation modal triggers confirm event', () => {
it('triggers uninstallApplication event', () => {
jest.spyOn(eventHub, '$emit');
mountComponent();
wrapper.find(UninstallApplicationConfirmationModal).vm.$emit('confirm');
expect(eventHub.$emit).toHaveBeenCalledWith('uninstallApplication', {
id: DEFAULT_APPLICATION_STATE.id,
});
});
});
describe('Update button', () => {
const button = () => wrapper.find('.js-cluster-application-update-button');
it('has indeterminate state on page load', () => {
mountComponent();
expect(button().exists()).toBe(false);
});
it('has enabled "Update" when "updateAvailable" is true', () => {
mountComponent({ updateAvailable: true });
expect(button().exists()).toBe(true);
expect(button().text()).toContain('Update');
});
it('has enabled "Retry update" when update process fails', () => {
mountComponent({
status: APPLICATION_STATUS.INSTALLED,
updateFailed: true,
});
expect(button().exists()).toBe(true);
expect(button().text()).toContain('Retry update');
});
it('has disabled "Updating" when APPLICATION_STATUS.UPDATING', () => {
mountComponent({ status: APPLICATION_STATUS.UPDATING });
expect(button().exists()).toBe(true);
expect(button().text()).toContain('Updating');
});
it('clicking update button emits event', () => {
const spy = jest.spyOn(eventHub, '$emit');
mountComponent({
status: APPLICATION_STATUS.INSTALLED,
updateAvailable: true,
});
button().vm.$emit('click');
expect(spy).toHaveBeenCalledWith('updateApplication', {
id: DEFAULT_APPLICATION_STATE.id,
params: {},
});
});
it('clicking disabled update button emits nothing', () => {
const spy = jest.spyOn(eventHub, '$emit');
mountComponent({ status: APPLICATION_STATUS.UPDATING });
button().vm.$emit('click');
expect(spy).not.toHaveBeenCalled();
});
it('displays an error message if application update failed', () => {
mountComponent({
title: 'GitLab Runner',
status: APPLICATION_STATUS.INSTALLED,
updateFailed: true,
});
const failureMessage = wrapper.find('.js-cluster-application-update-details');
expect(failureMessage.exists()).toBe(true);
expect(failureMessage.text()).toContain(
'Update failed. Please check the logs and try again.',
);
});
it('displays a success toast message if application update was successful', async () => {
mountComponent({
title: 'GitLab Runner',
updateSuccessful: false,
});
wrapper.vm.$toast = { show: jest.fn() };
wrapper.setProps({ updateSuccessful: true });
await wrapper.vm.$nextTick();
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('GitLab Runner updated successfully.');
});
describe('when updating does not require confirmation', () => {
beforeEach(() => mountComponent({ updateAvailable: true }));
it('the modal is not rendered', () => {
expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(false);
});
it('the correct button is rendered', () => {
expect(wrapper.find("[data-qa-selector='update_button']").exists()).toBe(true);
});
});
describe('when updating requires confirmation', () => {
beforeEach(() => {
mountComponent({
updateAvailable: true,
id: ELASTIC_STACK,
version: '1.1.2',
});
});
it('displays a modal', () => {
expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(true);
});
it('the correct button is rendered', () => {
expect(wrapper.find("[data-qa-selector='update_button_with_confirmation']").exists()).toBe(
true,
);
});
it('triggers updateApplication event', () => {
jest.spyOn(eventHub, '$emit');
wrapper.find(UpdateApplicationConfirmationModal).vm.$emit('confirm');
expect(eventHub.$emit).toHaveBeenCalledWith('updateApplication', {
id: ELASTIC_STACK,
params: {},
});
});
});
describe('updating Elastic Stack special case', () => {
it('needs confirmation if version is lower than 3.0.0', () => {
mountComponent({
updateAvailable: true,
id: ELASTIC_STACK,
version: '1.1.2',
});
expect(wrapper.find("[data-qa-selector='update_button_with_confirmation']").exists()).toBe(
true,
);
expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(true);
});
it('does not need confirmation is version is 3.0.0', () => {
mountComponent({
updateAvailable: true,
id: ELASTIC_STACK,
version: '3.0.0',
});
expect(wrapper.find("[data-qa-selector='update_button']").exists()).toBe(true);
expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(false);
});
it('does not need confirmation if version is higher than 3.0.0', () => {
mountComponent({
updateAvailable: true,
id: ELASTIC_STACK,
version: '5.2.1',
});
expect(wrapper.find("[data-qa-selector='update_button']").exists()).toBe(true);
expect(wrapper.find(UpdateApplicationConfirmationModal).exists()).toBe(false);
});
});
});
describe('Version', () => {
const updateDetails = () => wrapper.find('.js-cluster-application-update-details');
const versionEl = () => wrapper.find('.js-cluster-application-update-version');
it('displays a version number if application has been updated', () => {
const version = '0.1.45';
mountComponent({
status: APPLICATION_STATUS.INSTALLED,
updateSuccessful: true,
version,
});
expect(updateDetails().text()).toBe(`Updated to chart v${version}`);
});
it('contains a link to the chart repo if application has been updated', () => {
const version = '0.1.45';
const chartRepo = 'https://gitlab.com/gitlab-org/charts/gitlab-runner';
mountComponent({
status: APPLICATION_STATUS.INSTALLED,
updateSuccessful: true,
chartRepo,
version,
});
expect(versionEl().attributes('href')).toEqual(chartRepo);
expect(versionEl().props('target')).toEqual('_blank');
});
it('does not display a version number if application update failed', () => {
const version = '0.1.45';
mountComponent({
status: APPLICATION_STATUS.INSTALLED,
updateFailed: true,
version,
});
expect(updateDetails().text()).toBe('Update failed');
expect(versionEl().exists()).toBe(false);
});
it('displays updating when the application update is currently updating', () => {
mountComponent({
status: APPLICATION_STATUS.UPDATING,
updateSuccessful: true,
version: '1.2.3',
});
expect(updateDetails().text()).toBe('Updating');
expect(versionEl().exists()).toBe(false);
});
});
describe('Error block', () => {
const generalErrorMessage = () => wrapper.find('.js-cluster-application-general-error-message');
describe('when nothing fails', () => {
it('does not show error block', () => {
mountComponent();
expect(generalErrorMessage().exists()).toBe(false);
});
});
describe('when install or uninstall fails', () => {
const statusReason = 'We broke it 0.0';
const requestReason = 'We broke the request 0.0';
beforeEach(() => {
mountComponent({
status: APPLICATION_STATUS.ERROR,
statusReason,
requestReason,
installFailed: true,
});
});
it('shows status reason if it is available', () => {
const statusErrorMessage = wrapper.find('.js-cluster-application-status-error-message');
expect(statusErrorMessage.text()).toEqual(statusReason);
});
it('shows request reason if it is available', () => {
const requestErrorMessage = wrapper.find('.js-cluster-application-request-error-message');
expect(requestErrorMessage.text()).toEqual(requestReason);
});
});
describe('when install fails', () => {
beforeEach(() => {
mountComponent({
status: APPLICATION_STATUS.ERROR,
installFailed: true,
});
});
it('shows a general message indicating the installation failed', () => {
expect(generalErrorMessage().text()).toEqual(
`Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`,
);
});
});
describe('when uninstall fails', () => {
beforeEach(() => {
mountComponent({
status: APPLICATION_STATUS.ERROR,
uninstallFailed: true,
});
});
it('shows a general message indicating the uninstalling failed', () => {
expect(generalErrorMessage().text()).toEqual(
`Something went wrong while uninstalling ${DEFAULT_APPLICATION_STATE.title}`,
);
});
});
});
});

View File

@ -1,510 +0,0 @@
import { shallowMount, mount } from '@vue/test-utils';
import ApplicationRow from '~/clusters/components/application_row.vue';
import Applications from '~/clusters/components/applications.vue';
import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue';
import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
import { CLUSTER_TYPE, PROVIDER_TYPE } from '~/clusters/constants';
import eventHub from '~/clusters/event_hub';
import { APPLICATIONS_MOCK_STATE } from '../services/mock_data';
describe('Applications', () => {
let wrapper;
beforeEach(() => {
gon.features = gon.features || {};
});
const createComponent = ({ applications, type, propsData } = {}, isShallow) => {
const mountMethod = isShallow ? shallowMount : mount;
wrapper = mountMethod(Applications, {
stubs: { ApplicationRow },
propsData: {
type,
applications: { ...APPLICATIONS_MOCK_STATE, ...applications },
...propsData,
},
});
};
const createShallowComponent = (options) => createComponent(options, true);
const findByTestId = (id) => wrapper.find(`[data-testid="${id}"]`);
afterEach(() => {
wrapper.destroy();
});
describe('Project cluster applications', () => {
beforeEach(() => {
createComponent({ type: CLUSTER_TYPE.PROJECT });
});
it('renders a row for Ingress', () => {
expect(wrapper.find('.js-cluster-application-row-ingress').exists()).toBe(true);
});
it('renders a row for Cert-Manager', () => {
expect(wrapper.find('.js-cluster-application-row-cert_manager').exists()).toBe(true);
});
it('renders a row for Crossplane', () => {
expect(wrapper.find('.js-cluster-application-row-crossplane').exists()).toBe(true);
});
it('renders a row for Prometheus', () => {
expect(wrapper.find('.js-cluster-application-row-prometheus').exists()).toBe(true);
});
it('renders a row for GitLab Runner', () => {
expect(wrapper.find('.js-cluster-application-row-runner').exists()).toBe(true);
});
it('renders a row for Jupyter', () => {
expect(wrapper.find('.js-cluster-application-row-jupyter').exists()).toBe(true);
});
it('renders a row for Knative', () => {
expect(wrapper.find('.js-cluster-application-row-knative').exists()).toBe(true);
});
it('renders a row for Elastic Stack', () => {
expect(wrapper.find('.js-cluster-application-row-elastic_stack').exists()).toBe(true);
});
it('renders a row for Cilium', () => {
expect(wrapper.find('.js-cluster-application-row-cilium').exists()).toBe(true);
});
});
describe('Group cluster applications', () => {
beforeEach(() => {
createComponent({ type: CLUSTER_TYPE.GROUP });
});
it('renders a row for Ingress', () => {
expect(wrapper.find('.js-cluster-application-row-ingress').exists()).toBe(true);
});
it('renders a row for Cert-Manager', () => {
expect(wrapper.find('.js-cluster-application-row-cert_manager').exists()).toBe(true);
});
it('renders a row for Crossplane', () => {
expect(wrapper.find('.js-cluster-application-row-crossplane').exists()).toBe(true);
});
it('renders a row for Prometheus', () => {
expect(wrapper.find('.js-cluster-application-row-prometheus').exists()).toBe(true);
});
it('renders a row for GitLab Runner', () => {
expect(wrapper.find('.js-cluster-application-row-runner').exists()).toBe(true);
});
it('renders a row for Jupyter', () => {
expect(wrapper.find('.js-cluster-application-row-jupyter').exists()).toBe(true);
});
it('renders a row for Knative', () => {
expect(wrapper.find('.js-cluster-application-row-knative').exists()).toBe(true);
});
it('renders a row for Elastic Stack', () => {
expect(wrapper.find('.js-cluster-application-row-elastic_stack').exists()).toBe(true);
});
it('renders a row for Cilium', () => {
expect(wrapper.find('.js-cluster-application-row-cilium').exists()).toBe(true);
});
});
describe('Instance cluster applications', () => {
beforeEach(() => {
createComponent({ type: CLUSTER_TYPE.INSTANCE });
});
it('renders a row for Ingress', () => {
expect(wrapper.find('.js-cluster-application-row-ingress').exists()).toBe(true);
});
it('renders a row for Cert-Manager', () => {
expect(wrapper.find('.js-cluster-application-row-cert_manager').exists()).toBe(true);
});
it('renders a row for Crossplane', () => {
expect(wrapper.find('.js-cluster-application-row-crossplane').exists()).toBe(true);
});
it('renders a row for Prometheus', () => {
expect(wrapper.find('.js-cluster-application-row-prometheus').exists()).toBe(true);
});
it('renders a row for GitLab Runner', () => {
expect(wrapper.find('.js-cluster-application-row-runner').exists()).toBe(true);
});
it('renders a row for Jupyter', () => {
expect(wrapper.find('.js-cluster-application-row-jupyter').exists()).toBe(true);
});
it('renders a row for Knative', () => {
expect(wrapper.find('.js-cluster-application-row-knative').exists()).toBe(true);
});
it('renders a row for Elastic Stack', () => {
expect(wrapper.find('.js-cluster-application-row-elastic_stack').exists()).toBe(true);
});
it('renders a row for Cilium', () => {
expect(wrapper.find('.js-cluster-application-row-cilium').exists()).toBe(true);
});
});
describe('Helm application', () => {
it('does not render a row for Helm Tiller', () => {
createComponent();
expect(wrapper.find('.js-cluster-application-row-helm').exists()).toBe(false);
});
});
describe('Ingress application', () => {
it('shows the correct warning message', () => {
createComponent();
expect(findByTestId('ingressCostWarning').element).toMatchSnapshot();
});
describe('when installed', () => {
describe('with ip address', () => {
it('renders ip address with a clipboard button', () => {
createComponent({
applications: {
ingress: {
title: 'Ingress',
status: 'installed',
externalIp: '0.0.0.0',
},
},
});
expect(wrapper.find('.js-endpoint').element.value).toEqual('0.0.0.0');
expect(wrapper.find('.js-clipboard-btn').attributes('data-clipboard-text')).toEqual(
'0.0.0.0',
);
});
});
describe('with hostname', () => {
it('renders hostname with a clipboard button', () => {
createComponent({
applications: {
ingress: {
title: 'Ingress',
status: 'installed',
externalHostname: 'localhost.localdomain',
},
cert_manager: { title: 'Cert-Manager' },
crossplane: { title: 'Crossplane', stack: '' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
knative: { title: 'Knative', hostname: '' },
elastic_stack: { title: 'Elastic Stack' },
cilium: { title: 'GitLab Container Network Policies' },
},
});
expect(wrapper.find('.js-endpoint').element.value).toEqual('localhost.localdomain');
expect(wrapper.find('.js-clipboard-btn').attributes('data-clipboard-text')).toEqual(
'localhost.localdomain',
);
});
});
describe('without ip address', () => {
it('renders an input text with a loading icon and an alert text', () => {
createComponent({
applications: {
ingress: {
title: 'Ingress',
status: 'installed',
},
},
});
expect(wrapper.find('.js-ingress-ip-loading-icon').exists()).toBe(true);
expect(wrapper.find('.js-no-endpoint-message').exists()).toBe(true);
});
});
});
describe('before installing', () => {
it('does not render the IP address', () => {
createComponent();
expect(wrapper.text()).not.toContain('Ingress IP Address');
expect(wrapper.find('.js-endpoint').exists()).toBe(false);
});
});
});
describe('Cert-Manager application', () => {
it('shows the correct description', () => {
createComponent();
expect(findByTestId('certManagerDescription').element).toMatchSnapshot();
});
describe('when not installed', () => {
it('renders email & allows editing', () => {
createComponent({
applications: {
cert_manager: {
title: 'Cert-Manager',
email: 'before@example.com',
status: 'installable',
},
},
});
expect(wrapper.find('.js-email').element.value).toEqual('before@example.com');
expect(wrapper.find('.js-email').attributes('readonly')).toBe(undefined);
});
});
describe('when installed', () => {
it('renders email in readonly', () => {
createComponent({
applications: {
cert_manager: {
title: 'Cert-Manager',
email: 'after@example.com',
status: 'installed',
},
},
});
expect(wrapper.find('.js-email').element.value).toEqual('after@example.com');
expect(wrapper.find('.js-email').attributes('readonly')).toEqual('readonly');
});
});
});
describe('Jupyter application', () => {
describe('with ingress installed with ip & jupyter installable', () => {
it('renders hostname active input', () => {
createComponent({
applications: {
ingress: {
title: 'Ingress',
status: 'installed',
externalIp: '1.1.1.1',
},
},
});
expect(
wrapper.find('.js-cluster-application-row-jupyter .js-hostname').attributes('readonly'),
).toEqual(undefined);
});
});
describe('with ingress installed without external ip', () => {
it('does not render hostname input', () => {
createComponent({
applications: {
ingress: { title: 'Ingress', status: 'installed' },
},
});
expect(wrapper.find('.js-cluster-application-row-jupyter .js-hostname').exists()).toBe(
false,
);
});
});
describe('with ingress & jupyter installed', () => {
it('renders readonly input', () => {
createComponent({
applications: {
ingress: {
title: 'Ingress',
status: 'installed',
externalIp: '1.1.1.1',
},
jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' },
},
});
expect(
wrapper.find('.js-cluster-application-row-jupyter .js-hostname').attributes('readonly'),
).toEqual('readonly');
});
});
describe('without ingress installed', () => {
beforeEach(() => {
createComponent();
});
it('does not render input', () => {
expect(wrapper.find('.js-cluster-application-row-jupyter .js-hostname').exists()).toBe(
false,
);
});
});
});
describe('Prometheus application', () => {
it('shows the correct description', () => {
createComponent();
expect(findByTestId('prometheusDescription').element).toMatchSnapshot();
});
});
describe('Knative application', () => {
const availableDomain = {
id: 4,
domain: 'newhostname.com',
};
const propsData = {
applications: {
knative: {
title: 'Knative',
hostname: 'example.com',
status: 'installed',
externalIp: '1.1.1.1',
installed: true,
availableDomains: [availableDomain],
pagesDomain: null,
},
},
};
let knativeDomainEditor;
beforeEach(() => {
createShallowComponent(propsData);
jest.spyOn(eventHub, '$emit');
knativeDomainEditor = wrapper.find(KnativeDomainEditor);
});
it('shows the correct description', async () => {
createComponent();
wrapper.setProps({
providerType: PROVIDER_TYPE.GCP,
preInstalledKnative: true,
});
await wrapper.vm.$nextTick();
expect(findByTestId('installed-via').element).toMatchSnapshot();
});
it('emits saveKnativeDomain event when knative domain editor emits save event', () => {
propsData.applications.knative.hostname = availableDomain.domain;
propsData.applications.knative.pagesDomain = availableDomain;
knativeDomainEditor.vm.$emit('save');
expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', {
id: 'knative',
params: {
hostname: availableDomain.domain,
pages_domain_id: availableDomain.id,
},
});
});
it('emits saveKnativeDomain event when knative domain editor emits save event with custom domain', () => {
const newHostName = 'someothernewhostname.com';
propsData.applications.knative.hostname = newHostName;
propsData.applications.knative.pagesDomain = null;
knativeDomainEditor.vm.$emit('save');
expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', {
id: 'knative',
params: {
hostname: newHostName,
pages_domain_id: undefined,
},
});
});
it('emits setKnativeHostname event when knative domain editor emits change event', () => {
wrapper.find(KnativeDomainEditor).vm.$emit('set', {
domain: availableDomain.domain,
domainId: availableDomain.id,
});
expect(eventHub.$emit).toHaveBeenCalledWith('setKnativeDomain', {
id: 'knative',
domain: availableDomain.domain,
domainId: availableDomain.id,
});
});
});
describe('Crossplane application', () => {
const propsData = {
applications: {
crossplane: {
title: 'Crossplane',
stack: {
code: '',
},
},
},
};
beforeEach(() => createShallowComponent(propsData));
it('renders the correct Component', () => {
const crossplane = wrapper.find(CrossplaneProviderStack);
expect(crossplane.exists()).toBe(true);
});
it('shows the correct description', () => {
createComponent();
expect(findByTestId('crossplaneDescription').element).toMatchSnapshot();
});
});
describe('Elastic Stack application', () => {
describe('with elastic stack installable', () => {
it('renders the install button enabled', () => {
createComponent();
expect(
wrapper
.find(
'.js-cluster-application-row-elastic_stack .js-cluster-application-install-button',
)
.attributes('disabled'),
).toBeUndefined();
});
});
describe('elastic stack installed', () => {
it('renders uninstall button', () => {
createComponent({
applications: {
elastic_stack: { title: 'Elastic Stack', status: 'installed' },
},
});
expect(
wrapper
.find(
'.js-cluster-application-row-elastic_stack .js-cluster-application-install-button',
)
.attributes('disabled'),
).toEqual('disabled');
});
});
});
describe('Cilium application', () => {
it('shows the correct description', () => {
createComponent({ propsData: { ciliumHelpPath: 'cilium-help-path' } });
expect(findByTestId('ciliumDescription').element).toMatchSnapshot();
});
});
});

View File

@ -1,179 +0,0 @@
import { GlDropdownItem, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
import { APPLICATION_STATUS } from '~/clusters/constants';
const { UPDATING } = APPLICATION_STATUS;
describe('KnativeDomainEditor', () => {
let wrapper;
let knative;
const createComponent = (props = {}) => {
wrapper = shallowMount(KnativeDomainEditor, {
propsData: { ...props },
});
};
beforeEach(() => {
knative = {
title: 'Knative',
hostname: 'example.com',
installed: true,
};
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('knative has an assigned IP address', () => {
beforeEach(() => {
knative.externalIp = '1.1.1.1';
createComponent({ knative });
});
it('renders ip address with a clipboard button', () => {
expect(wrapper.find('.js-knative-endpoint').exists()).toBe(true);
expect(wrapper.find('.js-knative-endpoint').element.value).toEqual(knative.externalIp);
});
it('displays ip address clipboard button', () => {
expect(wrapper.find('.js-knative-endpoint-clipboard-btn').attributes('text')).toEqual(
knative.externalIp,
);
});
it('renders domain & allows editing', () => {
const domainNameInput = wrapper.find('.js-knative-domainname');
expect(domainNameInput.element.value).toEqual(knative.hostname);
expect(domainNameInput.attributes('readonly')).toBeFalsy();
});
it('renders an update/save Knative domain button', () => {
expect(wrapper.find('.js-knative-save-domain-button').exists()).toBe(true);
});
});
describe('knative without ip address', () => {
beforeEach(() => {
knative.externalIp = null;
createComponent({ knative });
});
it('renders an input text with a loading icon', () => {
expect(wrapper.find('.js-knative-ip-loading-icon').exists()).toBe(true);
});
it('renders message indicating there is not IP address assigned', () => {
expect(wrapper.find('.js-no-knative-endpoint-message').exists()).toBe(true);
});
});
describe('clicking save changes button', () => {
beforeEach(() => {
createComponent({ knative });
});
it('triggers save event and pass current knative hostname', () => {
wrapper.find(GlButton).vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('save').length).toEqual(1);
});
});
});
describe('when knative domain name was saved successfully', () => {
beforeEach(() => {
createComponent({ knative });
});
it('displays toast indicating a successful update', () => {
wrapper.vm.$toast = { show: jest.fn() };
wrapper.setProps({ knative: { updateSuccessful: true, ...knative } });
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
'Knative domain name was updated successfully.',
);
});
});
});
describe('when knative domain name input changes', () => {
it('emits "set" event with updated domain name', () => {
const newDomain = {
id: 4,
domain: 'newhostname.com',
};
createComponent({ knative: { ...knative, availableDomains: [newDomain] } });
jest.spyOn(wrapper.vm, 'selectDomain');
wrapper.find(GlDropdownItem).vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.selectDomain).toHaveBeenCalledWith(newDomain);
expect(wrapper.emitted('set')[0]).toEqual([
{
domain: newDomain.domain,
domainId: newDomain.id,
},
]);
});
});
it('emits "set" event with updated custom domain name', () => {
const newHostname = 'newhostname.com';
createComponent({ knative });
jest.spyOn(wrapper.vm, 'selectCustomDomain');
wrapper.setData({ knativeHostname: newHostname });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.selectCustomDomain).toHaveBeenCalledWith(newHostname);
expect(wrapper.emitted('set')[0]).toEqual([
{
domain: newHostname,
domainId: null,
},
]);
});
});
});
describe('when updating knative domain name failed', () => {
beforeEach(() => {
createComponent({ knative });
});
it('displays an error banner indicating the operation failure', () => {
wrapper.setProps({ knative: { updateFailed: true, ...knative } });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find('.js-cluster-knative-domain-name-failure-message').exists()).toBe(true);
});
});
});
describe(`when knative status is ${UPDATING}`, () => {
beforeEach(() => {
createComponent({ knative: { status: UPDATING, ...knative } });
});
it('renders loading spinner in save button', () => {
expect(wrapper.find(GlButton).props('loading')).toBe(true);
});
it('renders disabled save button', () => {
expect(wrapper.find(GlButton).props('disabled')).toBe(true);
});
it('renders save button with "Saving" label', () => {
expect(wrapper.find(GlButton).text()).toBe('Saving');
});
});
});

View File

@ -1,39 +0,0 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import UninstallApplicationButton from '~/clusters/components/uninstall_application_button.vue';
import { APPLICATION_STATUS } from '~/clusters/constants';
const { INSTALLED, UPDATING, UNINSTALLING } = APPLICATION_STATUS;
describe('UninstallApplicationButton', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(UninstallApplicationButton, {
propsData: { ...props },
});
};
afterEach(() => {
wrapper.destroy();
});
describe.each`
status | loading | disabled | text
${INSTALLED} | ${false} | ${false} | ${'Uninstall'}
${UPDATING} | ${false} | ${true} | ${'Uninstall'}
${UNINSTALLING} | ${true} | ${true} | ${'Uninstalling'}
`('when app status is $status', ({ loading, disabled, status, text }) => {
beforeEach(() => {
createComponent({ status });
});
it(`renders a button with loading=${loading} and disabled=${disabled}`, () => {
expect(wrapper.find(GlButton).props()).toMatchObject({ loading, disabled });
});
it(`renders a button with text="${text}"`, () => {
expect(wrapper.find(GlButton).text()).toBe(text);
});
});
});

View File

@ -1,57 +0,0 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue';
import { INGRESS } from '~/clusters/constants';
describe('UninstallApplicationConfirmationModal', () => {
let wrapper;
const appTitle = 'Ingress';
const createComponent = (props = {}) => {
wrapper = shallowMount(UninstallApplicationConfirmationModal, {
propsData: { ...props },
});
};
afterEach(() => {
wrapper.destroy();
});
beforeEach(() => {
createComponent({ application: INGRESS, applicationTitle: appTitle });
});
it(`renders a modal with a title "Uninstall ${appTitle}"`, () => {
expect(wrapper.find(GlModal).attributes('title')).toEqual(`Uninstall ${appTitle}`);
});
it(`renders a modal with an ok button labeled "Uninstall ${appTitle}"`, () => {
expect(wrapper.find(GlModal).attributes('ok-title')).toEqual(`Uninstall ${appTitle}`);
});
describe('when ok button is clicked', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm, 'trackUninstallButtonClick');
wrapper.find(GlModal).vm.$emit('ok');
});
it('emits confirm event', () =>
wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('confirm')).toBeTruthy();
}));
it('calls track uninstall button click mixin', () => {
expect(wrapper.vm.trackUninstallButtonClick).toHaveBeenCalledWith(INGRESS);
});
});
it('displays a warning text indicating the app will be uninstalled', () => {
expect(wrapper.text()).toContain(`You are about to uninstall ${appTitle} from your cluster.`);
});
it('displays a custom warning text depending on the application', () => {
expect(wrapper.text()).toContain(
`The associated load balancer and IP will be deleted and cannot be restored.`,
);
});
});

View File

@ -1,52 +0,0 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import UpdateApplicationConfirmationModal from '~/clusters/components/update_application_confirmation_modal.vue';
import { ELASTIC_STACK } from '~/clusters/constants';
describe('UpdateApplicationConfirmationModal', () => {
let wrapper;
const appTitle = 'Elastic stack';
const createComponent = (props = {}) => {
wrapper = shallowMount(UpdateApplicationConfirmationModal, {
propsData: { ...props },
});
};
afterEach(() => {
wrapper.destroy();
});
beforeEach(() => {
createComponent({ application: ELASTIC_STACK, applicationTitle: appTitle });
});
it(`renders a modal with a title "Update ${appTitle}"`, () => {
expect(wrapper.find(GlModal).attributes('title')).toEqual(`Update ${appTitle}`);
});
it(`renders a modal with an ok button labeled "Update ${appTitle}"`, () => {
expect(wrapper.find(GlModal).attributes('ok-title')).toEqual(`Update ${appTitle}`);
});
describe('when ok button is clicked', () => {
beforeEach(() => {
wrapper.find(GlModal).vm.$emit('ok');
});
it('emits confirm event', () =>
wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('confirm')).toBeTruthy();
}));
it('displays a warning text indicating the app will be updated', () => {
expect(wrapper.text()).toContain(`You are about to update ${appTitle} on your cluster.`);
});
it('displays a custom warning text depending on the application', () => {
expect(wrapper.text()).toContain(
`Your Elasticsearch cluster will be re-created during this upgrade. Your logs will be re-indexed, and you will lose historical logs from hosts terminated in the last 30 days.`,
);
});
});
});

View File

@ -1,206 +0,0 @@
import {
APPLICATION_STATUS,
UNINSTALL_EVENT,
UPDATE_EVENT,
INSTALL_EVENT,
} from '~/clusters/constants';
import transitionApplicationState from '~/clusters/services/application_state_machine';
const {
NO_STATUS,
SCHEDULED,
NOT_INSTALLABLE,
INSTALLABLE,
INSTALLING,
INSTALLED,
ERROR,
UPDATING,
UPDATED,
UPDATE_ERRORED,
UNINSTALLING,
UNINSTALL_ERRORED,
UNINSTALLED,
PRE_INSTALLED,
EXTERNALLY_INSTALLED,
} = APPLICATION_STATUS;
const NO_EFFECTS = 'no effects';
describe('applicationStateMachine', () => {
const noEffectsToEmptyObject = (effects) => (typeof effects === 'string' ? {} : effects);
describe(`current state is ${NO_STATUS}`, () => {
it.each`
expectedState | event | effects
${INSTALLING} | ${SCHEDULED} | ${NO_EFFECTS}
${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS}
${INSTALLING} | ${INSTALLING} | ${NO_EFFECTS}
${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
${UPDATING} | ${UPDATING} | ${NO_EFFECTS}
${INSTALLED} | ${UPDATED} | ${NO_EFFECTS}
${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
${UNINSTALLING} | ${UNINSTALLING} | ${NO_EFFECTS}
${INSTALLED} | ${UNINSTALL_ERRORED} | ${{ uninstallFailed: true }}
${UNINSTALLED} | ${UNINSTALLED} | ${NO_EFFECTS}
${PRE_INSTALLED} | ${PRE_INSTALLED} | ${NO_EFFECTS}
${EXTERNALLY_INSTALLED} | ${EXTERNALLY_INSTALLED} | ${NO_EFFECTS}
`(`transitions to $expectedState on $event event and applies $effects`, (data) => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: NO_STATUS,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...noEffectsToEmptyObject(effects),
});
});
});
describe(`current state is ${NOT_INSTALLABLE}`, () => {
it.each`
expectedState | event | effects
${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS}
`(`transitions to $expectedState on $event event and applies $effects`, (data) => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: NOT_INSTALLABLE,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...noEffectsToEmptyObject(effects),
});
});
});
describe(`current state is ${INSTALLABLE}`, () => {
it.each`
expectedState | event | effects
${INSTALLING} | ${INSTALL_EVENT} | ${{ installFailed: false }}
${INSTALLED} | ${INSTALLED} | ${{ installFailed: false }}
${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
${UNINSTALLED} | ${UNINSTALLED} | ${{ installFailed: false }}
`(`transitions to $expectedState on $event event and applies $effects`, (data) => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: INSTALLABLE,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...noEffectsToEmptyObject(effects),
});
});
});
describe(`current state is ${INSTALLING}`, () => {
it.each`
expectedState | event | effects
${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
`(`transitions to $expectedState on $event event and applies $effects`, (data) => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: INSTALLING,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...noEffectsToEmptyObject(effects),
});
});
});
describe(`current state is ${INSTALLED}`, () => {
it.each`
expectedState | event | effects
${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }}
${UNINSTALLING} | ${UNINSTALL_EVENT} | ${{ uninstallFailed: false, uninstallSuccessful: false }}
${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
${UNINSTALLED} | ${UNINSTALLED} | ${NO_EFFECTS}
${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
`(`transitions to $expectedState on $event event and applies $effects`, (data) => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: INSTALLED,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...noEffectsToEmptyObject(effects),
});
});
});
describe(`current state is ${UPDATING}`, () => {
it.each`
expectedState | event | effects
${INSTALLED} | ${UPDATED} | ${{ updateSuccessful: true }}
${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
`(`transitions to $expectedState on $event event and applies $effects`, (data) => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: UPDATING,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...effects,
});
});
});
describe(`current state is ${UNINSTALLING}`, () => {
it.each`
expectedState | event | effects
${INSTALLABLE} | ${INSTALLABLE} | ${{ uninstallSuccessful: true }}
${INSTALLED} | ${UNINSTALL_ERRORED} | ${{ uninstallFailed: true }}
`(`transitions to $expectedState on $event event and applies $effects`, (data) => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: UNINSTALLING,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...effects,
});
});
});
describe(`current state is ${UNINSTALLED}`, () => {
it.each`
expectedState | event | effects
${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
`(`transitions to $expectedState on $event event and applies $effects`, (data) => {
const { expectedState, event, effects } = data;
const currentAppState = {
status: UNINSTALLED,
};
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
...noEffectsToEmptyObject(effects),
});
});
});
describe('current state is undefined', () => {
it('returns the current state without having any effects', () => {
const currentAppState = {};
expect(transitionApplicationState(currentAppState, INSTALLABLE)).toEqual(currentAppState);
});
});
describe('with event is undefined', () => {
it('returns the current state without having any effects', () => {
const currentAppState = {
status: NO_STATUS,
};
expect(transitionApplicationState(currentAppState, undefined)).toEqual(currentAppState);
});
});
});

View File

@ -1,85 +0,0 @@
import { GlDropdownItem, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue';
describe('CrossplaneProviderStack component', () => {
let wrapper;
const defaultProps = {
stacks: [
{
name: 'Google Cloud Platform',
code: 'gcp',
},
{
name: 'Amazon Web Services',
code: 'aws',
},
],
};
function createComponent(props = {}) {
const propsData = {
...defaultProps,
...props,
};
wrapper = shallowMount(CrossplaneProviderStack, {
propsData,
});
}
beforeEach(() => {
const crossplane = {
title: 'crossplane',
stack: '',
};
createComponent({ crossplane });
});
const findDropdownElements = () => wrapper.findAll(GlDropdownItem);
const findFirstDropdownElement = () => findDropdownElements().at(0);
afterEach(() => {
wrapper.destroy();
});
it('renders all of the available stacks in the dropdown', () => {
const dropdownElements = findDropdownElements();
expect(dropdownElements.length).toBe(defaultProps.stacks.length);
defaultProps.stacks.forEach((stack, index) =>
expect(dropdownElements.at(index).text()).toEqual(stack.name),
);
});
it('displays the correct label for the first dropdown item if a stack is selected', () => {
const crossplane = {
title: 'crossplane',
stack: 'gcp',
};
createComponent({ crossplane });
expect(wrapper.vm.dropdownText).toBe('Google Cloud Platform');
});
it('emits the "set" event with the selected stack value', () => {
const crossplane = {
title: 'crossplane',
stack: 'gcp',
};
createComponent({ crossplane });
findFirstDropdownElement().vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted().set[0][0].code).toEqual('gcp');
});
});
it('renders the correct dropdown text when no stack is selected', () => {
expect(wrapper.vm.dropdownText).toBe('Select Stack');
});
it('renders an external link', () => {
expect(wrapper.find(GlIcon).props('name')).toBe('external-link');
});
});

View File

@ -1,170 +1,19 @@
import { APPLICATION_STATUS } from '~/clusters/constants';
const CLUSTERS_MOCK_DATA = {
GET: {
'/gitlab-org/gitlab-shell/clusters/1/status.json': {
data: {
status: 'errored',
status_reason: 'Failed to request to CloudPlatform.',
applications: [
{
name: 'helm',
status: APPLICATION_STATUS.INSTALLABLE,
status_reason: null,
can_uninstall: false,
},
{
name: 'ingress',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
external_ip: null,
external_hostname: null,
can_uninstall: false,
},
{
name: 'runner',
status: APPLICATION_STATUS.INSTALLING,
status_reason: null,
can_uninstall: false,
},
{
name: 'prometheus',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
can_uninstall: false,
},
{
name: 'jupyter',
status: APPLICATION_STATUS.INSTALLING,
status_reason: 'Cannot connect',
can_uninstall: false,
},
{
name: 'knative',
status: APPLICATION_STATUS.INSTALLING,
status_reason: 'Cannot connect',
can_uninstall: false,
},
{
name: 'cert_manager',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
email: 'test@example.com',
can_uninstall: false,
},
{
name: 'crossplane',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
can_uninstall: false,
},
{
name: 'elastic_stack',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
can_uninstall: false,
},
],
},
},
'/gitlab-org/gitlab-shell/clusters/2/status.json': {
data: {
status: 'errored',
status_reason: 'Failed to request to CloudPlatform.',
applications: [
{
name: 'helm',
status: APPLICATION_STATUS.INSTALLED,
status_reason: null,
},
{
name: 'ingress',
status: APPLICATION_STATUS.INSTALLED,
status_reason: 'Cannot connect',
external_ip: '1.1.1.1',
external_hostname: null,
},
{
name: 'runner',
status: APPLICATION_STATUS.INSTALLING,
status_reason: null,
},
{
name: 'prometheus',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
},
{
name: 'jupyter',
status: APPLICATION_STATUS.INSTALLABLE,
status_reason: 'Cannot connect',
},
{
name: 'knative',
status: APPLICATION_STATUS.INSTALLABLE,
status_reason: 'Cannot connect',
},
{
name: 'cert_manager',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
email: 'test@example.com',
},
{
name: 'crossplane',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
stack: 'gcp',
},
{
name: 'elastic_stack',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
},
],
},
},
},
POST: {
'/gitlab-org/gitlab-shell/clusters/1/applications/helm': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/ingress': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/crossplane': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/cert_manager': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/runner': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/knative': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/elastic_stack': {},
},
POST: {},
};
const DEFAULT_APPLICATION_STATE = {
id: 'some-app',
title: 'My App',
titleLink: 'https://about.gitlab.com/',
description: 'Some description about this interesting application!',
status: null,
statusReason: null,
requestReason: null,
};
const APPLICATIONS_MOCK_STATE = {
helm: { title: 'Helm Tiller', status: 'installable' },
ingress: {
title: 'Ingress',
status: 'installable',
},
crossplane: { title: 'Crossplane', status: 'installable', stack: '' },
cert_manager: { title: 'Cert-Manager', status: 'installable' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', status: 'installable', hostname: '' },
knative: { title: 'Knative ', status: 'installable', hostname: '' },
elastic_stack: { title: 'Elastic Stack', status: 'installable' },
cilium: {
title: 'GitLab Container Network Policies',
status: 'not_installable',
},
};
export { CLUSTERS_MOCK_DATA, DEFAULT_APPLICATION_STATE, APPLICATIONS_MOCK_STATE };
export { CLUSTERS_MOCK_DATA };

View File

@ -1,4 +1,3 @@
import { APPLICATION_INSTALLED_STATUSES, APPLICATION_STATUS, RUNNER } from '~/clusters/constants';
import ClustersStore from '~/clusters/stores/clusters_store';
import { CLUSTERS_MOCK_DATA } from '../services/mock_data';
@ -31,17 +30,6 @@ describe('Clusters Store', () => {
});
});
describe('updateAppProperty', () => {
it('should store new request reason', () => {
expect(store.state.applications.helm.requestReason).toEqual(null);
const newReason = 'We broke it.';
store.updateAppProperty('helm', 'requestReason', newReason);
expect(store.state.applications.helm.requestReason).toEqual(newReason);
});
});
describe('updateStateFromServer', () => {
it('should store new polling data from server', () => {
const mockResponseData =
@ -50,196 +38,16 @@ describe('Clusters Store', () => {
expect(store.state).toEqual({
helpPath: null,
helmHelpPath: null,
ingressHelpPath: null,
environmentsHelpPath: null,
clustersHelpPath: null,
deployBoardsHelpPath: null,
cloudRunHelpPath: null,
status: mockResponseData.status,
statusReason: mockResponseData.status_reason,
providerType: null,
preInstalledKnative: false,
rbac: false,
applications: {
helm: {
title: 'Legacy Helm Tiller server',
status: mockResponseData.applications[0].status,
statusReason: mockResponseData.applications[0].status_reason,
requestReason: null,
installable: true,
installed: false,
installFailed: false,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
ingress: {
title: 'Ingress',
status: APPLICATION_STATUS.INSTALLABLE,
statusReason: mockResponseData.applications[1].status_reason,
requestReason: null,
externalIp: null,
externalHostname: null,
installable: true,
installed: false,
installFailed: true,
uninstallable: false,
updateFailed: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
runner: {
title: 'GitLab Runner',
status: mockResponseData.applications[2].status,
statusReason: mockResponseData.applications[2].status_reason,
requestReason: null,
version: mockResponseData.applications[2].version,
updateAvailable: mockResponseData.applications[2].update_available,
chartRepo: 'https://gitlab.com/gitlab-org/charts/gitlab-runner',
installable: true,
installed: false,
installFailed: false,
updateFailed: false,
updateSuccessful: false,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
prometheus: {
title: 'Prometheus',
status: APPLICATION_STATUS.INSTALLABLE,
statusReason: mockResponseData.applications[3].status_reason,
requestReason: null,
installable: true,
installed: false,
installFailed: true,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
jupyter: {
title: 'JupyterHub',
status: mockResponseData.applications[4].status,
statusReason: mockResponseData.applications[4].status_reason,
requestReason: null,
hostname: '',
installable: true,
installed: false,
installFailed: false,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
knative: {
title: 'Knative',
status: mockResponseData.applications[5].status,
statusReason: mockResponseData.applications[5].status_reason,
requestReason: null,
hostname: null,
isEditingDomain: false,
externalIp: null,
externalHostname: null,
installable: true,
installed: false,
installFailed: false,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
updateSuccessful: false,
updateFailed: false,
validationError: null,
},
cert_manager: {
title: 'Cert-Manager',
status: APPLICATION_STATUS.INSTALLABLE,
installFailed: true,
statusReason: mockResponseData.applications[6].status_reason,
requestReason: null,
email: mockResponseData.applications[6].email,
installable: true,
installed: false,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
elastic_stack: {
title: 'Elastic Stack',
status: APPLICATION_STATUS.INSTALLABLE,
installFailed: true,
statusReason: mockResponseData.applications[7].status_reason,
requestReason: null,
installable: true,
installed: false,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
crossplane: {
title: 'Crossplane',
status: APPLICATION_STATUS.INSTALLABLE,
installFailed: true,
statusReason: mockResponseData.applications[8].status_reason,
requestReason: null,
installable: true,
installed: false,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
cilium: {
title: 'GitLab Container Network Policies',
status: null,
statusReason: null,
requestReason: null,
installable: false,
installed: false,
installFailed: false,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
},
environments: [],
fetchingEnvironments: false,
});
});
describe.each(APPLICATION_INSTALLED_STATUSES)(
'given the current app status is %s',
(status) => {
it('marks application as installed', () => {
const mockResponseData =
CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
const runnerAppIndex = 2;
mockResponseData.applications[runnerAppIndex].status = status;
store.updateStateFromServer(mockResponseData);
expect(store.state.applications[RUNNER].installed).toBe(true);
});
},
);
it('sets default hostname for jupyter when ingress has a ip address', () => {
const mockResponseData =
CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
store.updateStateFromServer(mockResponseData);
expect(store.state.applications.jupyter.hostname).toEqual(
`jupyter.${store.state.applications.ingress.externalIp}.nip.io`,
);
});
});
});

View File

@ -4,8 +4,6 @@ import Vuex from 'vuex';
import DiffContentComponent from '~/diffs/components/diff_content.vue';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
import DiffView from '~/diffs/components/diff_view.vue';
import InlineDiffView from '~/diffs/components/inline_diff_view.vue';
import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue';
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
import { diffViewerModes } from '~/ide/constants';
import NoteForm from '~/notes/components/note_form.vue';
@ -107,25 +105,10 @@ describe('DiffContent', () => {
});
const textDiffFile = { ...defaultProps.diffFile, viewer: { name: diffViewerModes.text } };
it('should render diff inline view if `isInlineView` is true', () => {
isInlineViewGetterMock.mockReturnValue(true);
createComponent({ props: { diffFile: textDiffFile } });
expect(wrapper.find(InlineDiffView).exists()).toBe(true);
});
it('should render parallel view if `isParallelView` getter is true', () => {
isParallelViewGetterMock.mockReturnValue(true);
createComponent({ props: { diffFile: textDiffFile } });
expect(wrapper.find(ParallelDiffView).exists()).toBe(true);
});
it('should render diff view if `unifiedDiffComponents` are true', () => {
isParallelViewGetterMock.mockReturnValue(true);
createComponent({
props: { diffFile: textDiffFile },
provide: { glFeatures: { unifiedDiffComponents: true } },
});
expect(wrapper.find(DiffView).exists()).toBe(true);

View File

@ -258,30 +258,3 @@ describe('mapParallel', () => {
expect(mapped.right).toMatchObject(rightExpectation);
});
});
describe('mapInline', () => {
it('should assign computed properties to the line object', () => {
const content = {
diffFile: {},
shouldRenderDraftRow: () => false,
};
const line = {
discussions: [{}],
discussionsExpanded: true,
hasForm: true,
};
const expectation = {
commentRowClasses: '',
hasDiscussions: true,
isContextLine: false,
isMatchLine: false,
isMetaLine: false,
renderDiscussion: true,
hasDraft: false,
hasCommentForm: true,
};
const mapped = utils.mapInline(content)(line);
expect(mapped).toMatchObject(expectation);
});
});

View File

@ -1,325 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
import { mapInline } from '~/diffs/components/diff_row_utils';
import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue';
import { createStore } from '~/mr_notes/stores';
import { findInteropAttributes } from '../find_interop_attributes';
import discussionsMockData from '../mock_data/diff_discussions';
import diffFileMockData from '../mock_data/diff_file';
const TEST_USER_ID = 'abc123';
const TEST_USER = { id: TEST_USER_ID };
describe('InlineDiffTableRow', () => {
let wrapper;
let store;
const mockDiffContent = {
diffFile: diffFileMockData,
shouldRenderDraftRow: jest.fn(),
hasParallelDraftLeft: jest.fn(),
hasParallelDraftRight: jest.fn(),
draftForLine: jest.fn(),
};
const applyMap = mapInline(mockDiffContent);
const thisLine = applyMap(diffFileMockData.highlighted_diff_lines[0]);
const createComponent = (props = {}, propsStore = store) => {
wrapper = shallowMount(InlineDiffTableRow, {
store: propsStore,
propsData: {
line: thisLine,
fileHash: diffFileMockData.file_hash,
filePath: diffFileMockData.file_path,
contextLinesPath: 'contextLinesPath',
isHighlighted: false,
...props,
},
});
};
beforeEach(() => {
store = createStore();
store.state.notes.userData = TEST_USER;
});
afterEach(() => {
wrapper.destroy();
});
it('does not add hll class to line content when line does not match highlighted row', () => {
createComponent();
expect(wrapper.find('.line_content').classes('hll')).toBe(false);
});
it('adds hll class to lineContent when line is the highlighted row', () => {
store.state.diffs.highlightedRow = thisLine.line_code;
createComponent({}, store);
expect(wrapper.find('.line_content').classes('hll')).toBe(true);
});
it('adds hll class to lineContent when line is part of a multiline comment', () => {
createComponent({ isCommented: true });
expect(wrapper.find('.line_content').classes('hll')).toBe(true);
});
describe('sets coverage title and class', () => {
it('for lines with coverage', () => {
const name = diffFileMockData.file_path;
const line = thisLine.new_line;
store.state.diffs.coverageFiles = { files: { [name]: { [line]: 5 } } };
createComponent({}, store);
const coverage = wrapper.find('.line-coverage');
expect(coverage.attributes('title')).toContain('Test coverage: 5 hits');
expect(coverage.classes('coverage')).toBe(true);
});
it('for lines without coverage', () => {
const name = diffFileMockData.file_path;
const line = thisLine.new_line;
store.state.diffs.coverageFiles = { files: { [name]: { [line]: 0 } } };
createComponent({}, store);
const coverage = wrapper.find('.line-coverage');
expect(coverage.attributes('title')).toContain('No test coverage');
expect(coverage.classes('no-coverage')).toBe(true);
});
it('for unknown lines', () => {
store.state.diffs.coverageFiles = {};
createComponent({}, store);
const coverage = wrapper.find('.line-coverage');
expect(coverage.attributes('title')).toBeUndefined();
expect(coverage.classes('coverage')).toBe(false);
expect(coverage.classes('no-coverage')).toBe(false);
});
});
describe('Table Cells', () => {
const findNewTd = () => wrapper.find({ ref: 'newTd' });
const findOldTd = () => wrapper.find({ ref: 'oldTd' });
describe('td', () => {
it('highlights when isHighlighted true', () => {
store.state.diffs.highlightedRow = thisLine.line_code;
createComponent({}, store);
expect(findNewTd().classes()).toContain('hll');
expect(findOldTd().classes()).toContain('hll');
});
it('does not highlight when isHighlighted false', () => {
createComponent();
expect(findNewTd().classes()).not.toContain('hll');
expect(findOldTd().classes()).not.toContain('hll');
});
});
describe('comment button', () => {
const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButton' });
it.each`
userData | expectation
${TEST_USER} | ${true}
${null} | ${false}
`('exists is $expectation - with userData ($userData)', ({ userData, expectation }) => {
store.state.notes.userData = userData;
createComponent({}, store);
expect(findNoteButton().exists()).toBe(expectation);
});
it.each`
isHover | line | expectation
${true} | ${{ ...thisLine, discussions: [] }} | ${true}
${false} | ${{ ...thisLine, discussions: [] }} | ${false}
${true} | ${{ ...thisLine, type: 'context', discussions: [] }} | ${false}
${true} | ${{ ...thisLine, type: 'old-nonewline', discussions: [] }} | ${false}
${true} | ${{ ...thisLine, discussions: [{}] }} | ${false}
`('visible is $expectation - line ($line)', ({ isHover, line, expectation }) => {
createComponent({ line: applyMap(line) });
wrapper.setData({ isHover });
return wrapper.vm.$nextTick().then(() => {
expect(findNoteButton().isVisible()).toBe(expectation);
});
});
it.each`
disabled | commentsDisabled
${'disabled'} | ${true}
${undefined} | ${false}
`(
'has attribute disabled=$disabled when the outer component has prop commentsDisabled=$commentsDisabled',
({ disabled, commentsDisabled }) => {
createComponent({
line: applyMap({ ...thisLine, commentsDisabled }),
});
wrapper.setData({ isHover: true });
return wrapper.vm.$nextTick().then(() => {
expect(findNoteButton().attributes('disabled')).toBe(disabled);
});
},
);
const symlinkishFileTooltip =
'Commenting on symbolic links that replace or are replaced by files is currently not supported.';
const realishFileTooltip =
'Commenting on files that replace or are replaced by symbolic links is currently not supported.';
const otherFileTooltip = 'Add a comment to this line';
const findTooltip = () => wrapper.find({ ref: 'addNoteTooltip' });
it.each`
tooltip | commentsDisabled
${symlinkishFileTooltip} | ${{ wasSymbolic: true }}
${symlinkishFileTooltip} | ${{ isSymbolic: true }}
${realishFileTooltip} | ${{ wasReal: true }}
${realishFileTooltip} | ${{ isReal: true }}
${otherFileTooltip} | ${false}
`(
'has the correct tooltip when commentsDisabled=$commentsDisabled',
({ tooltip, commentsDisabled }) => {
createComponent({
line: applyMap({ ...thisLine, commentsDisabled }),
});
wrapper.setData({ isHover: true });
return wrapper.vm.$nextTick().then(() => {
expect(findTooltip().attributes('title')).toBe(tooltip);
});
},
);
});
describe('line number', () => {
const findLineNumberOld = () => wrapper.find({ ref: 'lineNumberRefOld' });
const findLineNumberNew = () => wrapper.find({ ref: 'lineNumberRefNew' });
it('renders line numbers in correct cells', () => {
createComponent();
expect(findLineNumberOld().exists()).toBe(false);
expect(findLineNumberNew().exists()).toBe(true);
});
describe('with lineNumber prop', () => {
const TEST_LINE_CODE = 'LC_42';
const TEST_LINE_NUMBER = 1;
describe.each`
lineProps | findLineNumber | expectedHref | expectedClickArg
${{ line_code: TEST_LINE_CODE, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${`#${TEST_LINE_CODE}`} | ${TEST_LINE_CODE}
${{ line_code: undefined, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${'#'} | ${undefined}
${{ line_code: undefined, left: { line_code: TEST_LINE_CODE }, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${'#'} | ${TEST_LINE_CODE}
${{ line_code: undefined, right: { line_code: TEST_LINE_CODE }, new_line: TEST_LINE_NUMBER }} | ${findLineNumberNew} | ${'#'} | ${TEST_LINE_CODE}
`(
'with line ($lineProps)',
({ lineProps, findLineNumber, expectedHref, expectedClickArg }) => {
beforeEach(() => {
jest.spyOn(store, 'dispatch').mockImplementation();
createComponent({
line: applyMap({ ...thisLine, ...lineProps }),
});
});
it('renders', () => {
expect(findLineNumber().exists()).toBe(true);
expect(findLineNumber().attributes()).toEqual({
href: expectedHref,
'data-linenumber': TEST_LINE_NUMBER.toString(),
});
});
it('on click, dispatches setHighlightedRow', () => {
expect(store.dispatch).toHaveBeenCalledTimes(1);
findLineNumber().trigger('click');
expect(store.dispatch).toHaveBeenCalledWith(
'diffs/setHighlightedRow',
expectedClickArg,
);
expect(store.dispatch).toHaveBeenCalledTimes(2);
});
},
);
});
});
describe('diff-gutter-avatars', () => {
const TEST_LINE_CODE = 'LC_42';
const TEST_FILE_HASH = diffFileMockData.file_hash;
const findAvatars = () => wrapper.find(DiffGutterAvatars);
let line;
beforeEach(() => {
jest.spyOn(store, 'dispatch').mockImplementation();
line = {
line_code: TEST_LINE_CODE,
type: 'new',
old_line: null,
new_line: 1,
discussions: [{ ...discussionsMockData }],
discussionsExpanded: true,
text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
rich_text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
meta_data: null,
};
});
describe('with showCommentButton', () => {
it('renders if line has discussions', () => {
createComponent({ line: applyMap(line) });
expect(findAvatars().props()).toEqual({
discussions: line.discussions,
discussionsExpanded: line.discussionsExpanded,
});
});
it('does notrender if line has no discussions', () => {
line.discussions = [];
createComponent({ line: applyMap(line) });
expect(findAvatars().exists()).toEqual(false);
});
it('toggles line discussion', () => {
createComponent({ line: applyMap(line) });
expect(store.dispatch).toHaveBeenCalledTimes(1);
findAvatars().vm.$emit('toggleLineDiscussions');
expect(store.dispatch).toHaveBeenCalledWith('diffs/toggleLineDiscussions', {
lineCode: TEST_LINE_CODE,
fileHash: TEST_FILE_HASH,
expanded: !line.discussionsExpanded,
});
});
});
});
});
describe('interoperability', () => {
it.each`
desc | line | expectation
${'with type old'} | ${{ ...thisLine, type: 'old', old_line: 3, new_line: 5 }} | ${{ type: 'old', line: '3', oldLine: '3', newLine: '5' }}
${'with type new'} | ${{ ...thisLine, type: 'new', old_line: 3, new_line: 5 }} | ${{ type: 'new', line: '5', oldLine: '3', newLine: '5' }}
`('$desc, sets interop data attributes', ({ line, expectation }) => {
createComponent({ line });
expect(findInteropAttributes(wrapper)).toEqual(expectation);
});
});
});

View File

@ -1,57 +0,0 @@
import '~/behaviors/markdown/render_gfm';
import { getByText } from '@testing-library/dom';
import { mount } from '@vue/test-utils';
import { mapInline } from '~/diffs/components/diff_row_utils';
import InlineDiffView from '~/diffs/components/inline_diff_view.vue';
import { createStore } from '~/mr_notes/stores';
import discussionsMockData from '../mock_data/diff_discussions';
import diffFileMockData from '../mock_data/diff_file';
describe('InlineDiffView', () => {
let wrapper;
const getDiffFileMock = () => ({ ...diffFileMockData });
const getDiscussionsMockData = () => [{ ...discussionsMockData }];
const notesLength = getDiscussionsMockData()[0].notes.length;
const setup = (diffFile, diffLines) => {
const mockDiffContent = {
diffFile,
shouldRenderDraftRow: jest.fn(),
};
const store = createStore();
store.dispatch('diffs/setInlineDiffViewType');
wrapper = mount(InlineDiffView, {
store,
propsData: {
diffFile,
diffLines: diffLines.map(mapInline(mockDiffContent)),
},
});
};
describe('template', () => {
it('should have rendered diff lines', () => {
const diffFile = getDiffFileMock();
setup(diffFile, diffFile.highlighted_diff_lines);
expect(wrapper.findAll('tr.line_holder').length).toEqual(8);
expect(wrapper.findAll('tr.line_holder.new').length).toEqual(4);
expect(wrapper.findAll('tr.line_expansion.match').length).toEqual(1);
getByText(wrapper.element, /Bad dates/i);
});
it('should render discussions', () => {
const diffFile = getDiffFileMock();
diffFile.highlighted_diff_lines[1].discussions = getDiscussionsMockData();
diffFile.highlighted_diff_lines[1].discussionsExpanded = true;
setup(diffFile, diffFile.highlighted_diff_lines);
expect(wrapper.findAll('.notes_holder').length).toEqual(1);
expect(wrapper.findAll('.notes_holder .note').length).toEqual(notesLength + 1);
getByText(wrapper.element, 'comment 5');
wrapper.vm.$store.dispatch('setInitialNotes', []);
});
});
});

View File

@ -1,445 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
import { mapParallel } from '~/diffs/components/diff_row_utils';
import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue';
import { createStore } from '~/mr_notes/stores';
import { findInteropAttributes } from '../find_interop_attributes';
import discussionsMockData from '../mock_data/diff_discussions';
import diffFileMockData from '../mock_data/diff_file';
describe('ParallelDiffTableRow', () => {
const mockDiffContent = {
diffFile: diffFileMockData,
shouldRenderDraftRow: jest.fn(),
hasParallelDraftLeft: jest.fn(),
hasParallelDraftRight: jest.fn(),
draftForLine: jest.fn(),
};
const applyMap = mapParallel(mockDiffContent);
describe('when one side is empty', () => {
let wrapper;
let vm;
const thisLine = diffFileMockData.parallel_diff_lines[0];
const rightLine = diffFileMockData.parallel_diff_lines[0].right;
beforeEach(() => {
wrapper = shallowMount(ParallelDiffTableRow, {
store: createStore(),
propsData: {
line: applyMap(thisLine),
fileHash: diffFileMockData.file_hash,
filePath: diffFileMockData.file_path,
contextLinesPath: 'contextLinesPath',
isHighlighted: false,
},
});
vm = wrapper.vm;
});
it('does not highlight non empty line content when line does not match highlighted row', (done) => {
vm.$nextTick()
.then(() => {
expect(vm.$el.querySelector('.line_content.right-side').classList).not.toContain('hll');
})
.then(done)
.catch(done.fail);
});
it('highlights nonempty line content when line is the highlighted row', (done) => {
vm.$nextTick()
.then(() => {
vm.$store.state.diffs.highlightedRow = rightLine.line_code;
return vm.$nextTick();
})
.then(() => {
expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll');
})
.then(done)
.catch(done.fail);
});
it('highlights nonempty line content when line is part of a multiline comment', () => {
wrapper.setProps({ isCommented: true });
return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll');
});
});
});
describe('when both sides have content', () => {
let vm;
const thisLine = diffFileMockData.parallel_diff_lines[2];
const rightLine = diffFileMockData.parallel_diff_lines[2].right;
beforeEach(() => {
vm = createComponentWithStore(Vue.extend(ParallelDiffTableRow), createStore(), {
line: applyMap(thisLine),
fileHash: diffFileMockData.file_hash,
filePath: diffFileMockData.file_path,
contextLinesPath: 'contextLinesPath',
isHighlighted: false,
}).$mount();
});
it('does not highlight either line when line does not match highlighted row', (done) => {
vm.$nextTick()
.then(() => {
expect(vm.$el.querySelector('.line_content.right-side').classList).not.toContain('hll');
expect(vm.$el.querySelector('.line_content.left-side').classList).not.toContain('hll');
})
.then(done)
.catch(done.fail);
});
it('adds hll class to lineContent when line is the highlighted row', (done) => {
vm.$nextTick()
.then(() => {
vm.$store.state.diffs.highlightedRow = rightLine.line_code;
return vm.$nextTick();
})
.then(() => {
expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll');
expect(vm.$el.querySelector('.line_content.left-side').classList).toContain('hll');
})
.then(done)
.catch(done.fail);
});
describe('sets coverage title and class', () => {
it('for lines with coverage', (done) => {
vm.$nextTick()
.then(() => {
const name = diffFileMockData.file_path;
const line = rightLine.new_line;
vm.$store.state.diffs.coverageFiles = { files: { [name]: { [line]: 5 } } };
return vm.$nextTick();
})
.then(() => {
const coverage = vm.$el.querySelector('.line-coverage.right-side');
expect(coverage.title).toContain('Test coverage: 5 hits');
expect(coverage.classList).toContain('coverage');
})
.then(done)
.catch(done.fail);
});
it('for lines without coverage', (done) => {
vm.$nextTick()
.then(() => {
const name = diffFileMockData.file_path;
const line = rightLine.new_line;
vm.$store.state.diffs.coverageFiles = { files: { [name]: { [line]: 0 } } };
return vm.$nextTick();
})
.then(() => {
const coverage = vm.$el.querySelector('.line-coverage.right-side');
expect(coverage.title).toContain('No test coverage');
expect(coverage.classList).toContain('no-coverage');
})
.then(done)
.catch(done.fail);
});
it('for unknown lines', (done) => {
vm.$nextTick()
.then(() => {
vm.$store.state.diffs.coverageFiles = {};
return vm.$nextTick();
})
.then(() => {
const coverage = vm.$el.querySelector('.line-coverage.right-side');
expect(coverage.title).not.toContain('Coverage');
expect(coverage.classList).not.toContain('coverage');
expect(coverage.classList).not.toContain('no-coverage');
})
.then(done)
.catch(done.fail);
});
});
});
describe('Table Cells', () => {
let wrapper;
let store;
let thisLine;
const TEST_USER_ID = 'abc123';
const TEST_USER = { id: TEST_USER_ID };
const createComponent = (props = {}, propsStore = store, data = {}) => {
wrapper = shallowMount(ParallelDiffTableRow, {
store: propsStore,
propsData: {
line: thisLine,
fileHash: diffFileMockData.file_hash,
filePath: diffFileMockData.file_path,
contextLinesPath: 'contextLinesPath',
isHighlighted: false,
...props,
},
data() {
return data;
},
});
};
beforeEach(() => {
// eslint-disable-next-line prefer-destructuring
thisLine = diffFileMockData.parallel_diff_lines[2];
store = createStore();
store.state.notes.userData = TEST_USER;
});
afterEach(() => {
wrapper.destroy();
});
const findNewTd = () => wrapper.find({ ref: 'newTd' });
const findOldTd = () => wrapper.find({ ref: 'oldTd' });
describe('td', () => {
it('highlights when isHighlighted true', () => {
store.state.diffs.highlightedRow = thisLine.left.line_code;
createComponent({}, store);
expect(findNewTd().classes()).toContain('hll');
expect(findOldTd().classes()).toContain('hll');
});
it('does not highlight when isHighlighted false', () => {
createComponent();
expect(findNewTd().classes()).not.toContain('hll');
expect(findOldTd().classes()).not.toContain('hll');
});
});
describe('comment button', () => {
const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButtonLeft' });
it.each`
hover | line | userData | expectation
${true} | ${{}} | ${TEST_USER} | ${true}
${true} | ${{ line: { left: null } }} | ${TEST_USER} | ${false}
${true} | ${{}} | ${null} | ${false}
${false} | ${{}} | ${TEST_USER} | ${false}
`(
'exists is $expectation - with userData ($userData)',
async ({ hover, line, userData, expectation }) => {
store.state.notes.userData = userData;
createComponent(line, store);
if (hover) await wrapper.find('.line_holder').trigger('mouseover');
expect(findNoteButton().exists()).toBe(expectation);
},
);
it.each`
line | expectation
${{ ...thisLine, left: { discussions: [] } }} | ${true}
${{ ...thisLine, left: { type: 'context', discussions: [] } }} | ${false}
${{ ...thisLine, left: { type: 'old-nonewline', discussions: [] } }} | ${false}
${{ ...thisLine, left: { discussions: [{}] } }} | ${false}
`('visible is $expectation - line ($line)', async ({ line, expectation }) => {
createComponent({ line: applyMap(line) }, store, {
isLeftHover: true,
isCommentButtonRendered: true,
});
expect(findNoteButton().isVisible()).toBe(expectation);
});
it.each`
disabled | commentsDisabled
${'disabled'} | ${true}
${undefined} | ${false}
`(
'has attribute disabled=$disabled when the outer component has prop commentsDisabled=$commentsDisabled',
({ disabled, commentsDisabled }) => {
thisLine.left.commentsDisabled = commentsDisabled;
createComponent({ line: { ...thisLine } }, store, {
isLeftHover: true,
isCommentButtonRendered: true,
});
expect(findNoteButton().attributes('disabled')).toBe(disabled);
},
);
const symlinkishFileTooltip =
'Commenting on symbolic links that replace or are replaced by files is currently not supported.';
const realishFileTooltip =
'Commenting on files that replace or are replaced by symbolic links is currently not supported.';
const otherFileTooltip = 'Add a comment to this line';
const findTooltip = () => wrapper.find({ ref: 'addNoteTooltipLeft' });
it.each`
tooltip | commentsDisabled
${symlinkishFileTooltip} | ${{ wasSymbolic: true }}
${symlinkishFileTooltip} | ${{ isSymbolic: true }}
${realishFileTooltip} | ${{ wasReal: true }}
${realishFileTooltip} | ${{ isReal: true }}
${otherFileTooltip} | ${false}
`(
'has the correct tooltip when commentsDisabled=$commentsDisabled',
({ tooltip, commentsDisabled }) => {
thisLine.left.commentsDisabled = commentsDisabled;
createComponent({ line: { ...thisLine } }, store, {
isLeftHover: true,
isCommentButtonRendered: true,
});
expect(findTooltip().attributes('title')).toBe(tooltip);
},
);
});
describe('line number', () => {
const findLineNumberOld = () => wrapper.find({ ref: 'lineNumberRefOld' });
const findLineNumberNew = () => wrapper.find({ ref: 'lineNumberRefNew' });
it('renders line numbers in correct cells', () => {
createComponent();
expect(findLineNumberOld().exists()).toBe(true);
expect(findLineNumberNew().exists()).toBe(true);
});
describe('with lineNumber prop', () => {
const TEST_LINE_CODE = 'LC_42';
const TEST_LINE_NUMBER = 1;
describe.each`
lineProps | findLineNumber | expectedHref | expectedClickArg
${{ line_code: TEST_LINE_CODE, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${`#${TEST_LINE_CODE}`} | ${TEST_LINE_CODE}
${{ line_code: undefined, old_line: TEST_LINE_NUMBER }} | ${findLineNumberOld} | ${'#'} | ${undefined}
`(
'with line ($lineProps)',
({ lineProps, findLineNumber, expectedHref, expectedClickArg }) => {
beforeEach(() => {
jest.spyOn(store, 'dispatch').mockImplementation();
Object.assign(thisLine.left, lineProps);
Object.assign(thisLine.right, lineProps);
createComponent({
line: applyMap({ ...thisLine }),
});
});
it('renders', () => {
expect(findLineNumber().exists()).toBe(true);
expect(findLineNumber().attributes()).toEqual({
href: expectedHref,
'data-linenumber': TEST_LINE_NUMBER.toString(),
});
});
it('on click, dispatches setHighlightedRow', () => {
expect(store.dispatch).toHaveBeenCalledTimes(1);
findLineNumber().trigger('click');
expect(store.dispatch).toHaveBeenCalledWith(
'diffs/setHighlightedRow',
expectedClickArg,
);
expect(store.dispatch).toHaveBeenCalledTimes(2);
});
},
);
});
});
describe('diff-gutter-avatars', () => {
const TEST_LINE_CODE = 'LC_42';
const TEST_FILE_HASH = diffFileMockData.file_hash;
const findAvatars = () => wrapper.find(DiffGutterAvatars);
let line;
beforeEach(() => {
jest.spyOn(store, 'dispatch').mockImplementation();
line = applyMap({
left: {
line_code: TEST_LINE_CODE,
type: 'new',
old_line: null,
new_line: 1,
discussions: [{ ...discussionsMockData }],
discussionsExpanded: true,
text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
rich_text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
meta_data: null,
},
});
});
describe('with showCommentButton', () => {
it('renders if line has discussions', () => {
createComponent({ line });
expect(findAvatars().props()).toEqual({
discussions: line.left.discussions,
discussionsExpanded: line.left.discussionsExpanded,
});
});
it('does notrender if line has no discussions', () => {
line.left.discussions = [];
createComponent({ line: applyMap(line) });
expect(findAvatars().exists()).toEqual(false);
});
it('toggles line discussion', () => {
createComponent({ line });
expect(store.dispatch).toHaveBeenCalledTimes(1);
findAvatars().vm.$emit('toggleLineDiscussions');
expect(store.dispatch).toHaveBeenCalledWith('diffs/toggleLineDiscussions', {
lineCode: TEST_LINE_CODE,
fileHash: TEST_FILE_HASH,
expanded: !line.left.discussionsExpanded,
});
});
});
});
describe('interoperability', () => {
beforeEach(() => {
createComponent();
});
it('adds old side interoperability data attributes', () => {
expect(findInteropAttributes(wrapper, '.line_content.left-side')).toEqual({
type: 'old',
line: thisLine.left.old_line.toString(),
oldLine: thisLine.left.old_line.toString(),
});
});
it('adds new side interoperability data attributes', () => {
expect(findInteropAttributes(wrapper, '.line_content.right-side')).toEqual({
type: 'new',
line: thisLine.right.new_line.toString(),
newLine: thisLine.right.new_line.toString(),
});
});
});
});
});

View File

@ -1,37 +0,0 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import parallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue';
import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue';
import { createStore } from '~/mr_notes/stores';
import diffFileMockData from '../mock_data/diff_file';
let wrapper;
const localVue = createLocalVue();
localVue.use(Vuex);
function factory() {
const diffFile = { ...diffFileMockData };
const store = createStore();
wrapper = shallowMount(ParallelDiffView, {
localVue,
store,
propsData: {
diffFile,
diffLines: diffFile.parallel_diff_lines,
},
});
}
describe('ParallelDiffView', () => {
afterEach(() => {
wrapper.destroy();
});
it('renders diff lines', () => {
factory();
expect(wrapper.findAll(parallelDiffTableRow).length).toBe(8);
});
});

Some files were not shown because too many files have changed in this diff Show More