Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c8cc2fe990
commit
dd6e32bf47
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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_'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
.cluster-applications-table#js-cluster-applications
|
||||
|
|
@ -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')
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||

|
||||
|
||||
## 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 |
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
## 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 |
|
|
@ -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:
|
||||
|
||||

|
||||

|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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**.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 cluster’s 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 organization’s 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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
@ -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}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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', []);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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(),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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
Loading…
Reference in New Issue