Commit e3bdfa1a authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent c1a50b81
...@@ -259,7 +259,7 @@ export default class Clusters { ...@@ -259,7 +259,7 @@ export default class Clusters {
eventHub.$on('installApplication', this.installApplication); eventHub.$on('installApplication', this.installApplication);
eventHub.$on('updateApplication', data => this.updateApplication(data)); eventHub.$on('updateApplication', data => this.updateApplication(data));
eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data)); eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data));
eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data)); eventHub.$on('setKnativeDomain', data => this.setKnativeDomain(data));
eventHub.$on('uninstallApplication', data => this.uninstallApplication(data)); eventHub.$on('uninstallApplication', data => this.uninstallApplication(data));
eventHub.$on('setCrossplaneProviderStack', data => this.setCrossplaneProviderStack(data)); eventHub.$on('setCrossplaneProviderStack', data => this.setCrossplaneProviderStack(data));
eventHub.$on('setIngressModSecurityEnabled', data => this.setIngressModSecurityEnabled(data)); eventHub.$on('setIngressModSecurityEnabled', data => this.setIngressModSecurityEnabled(data));
...@@ -275,7 +275,7 @@ export default class Clusters { ...@@ -275,7 +275,7 @@ export default class Clusters {
eventHub.$off('installApplication', this.installApplication); eventHub.$off('installApplication', this.installApplication);
eventHub.$off('updateApplication', this.updateApplication); eventHub.$off('updateApplication', this.updateApplication);
eventHub.$off('saveKnativeDomain'); eventHub.$off('saveKnativeDomain');
eventHub.$off('setKnativeHostname'); eventHub.$off('setKnativeDomain');
eventHub.$off('setCrossplaneProviderStack'); eventHub.$off('setCrossplaneProviderStack');
eventHub.$off('uninstallApplication'); eventHub.$off('uninstallApplication');
eventHub.$off('setIngressModSecurityEnabled'); eventHub.$off('setIngressModSecurityEnabled');
...@@ -521,10 +521,10 @@ export default class Clusters { ...@@ -521,10 +521,10 @@ export default class Clusters {
}); });
} }
setKnativeHostname(data) { setKnativeDomain({ id: appId, domain, domainId }) {
const appId = data.id; this.store.updateAppProperty(appId, 'isEditingDomain', true);
this.store.updateAppProperty(appId, 'isEditingHostName', true); this.store.updateAppProperty(appId, 'hostname', domain);
this.store.updateAppProperty(appId, 'hostname', data.hostname); this.store.updateAppProperty(appId, 'pagesDomain', domainId ? { id: domainId, domain } : null);
} }
setCrossplaneProviderStack(data) { setCrossplaneProviderStack(data) {
......
...@@ -240,16 +240,20 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity ...@@ -240,16 +240,20 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
this.helmInstallIllustration = helmInstallIllustration; this.helmInstallIllustration = helmInstallIllustration;
}, },
methods: { methods: {
saveKnativeDomain(hostname) { saveKnativeDomain() {
eventHub.$emit('saveKnativeDomain', { eventHub.$emit('saveKnativeDomain', {
id: 'knative', id: 'knative',
params: { hostname }, params: {
hostname: this.applications.knative.hostname,
pages_domain_id: this.applications.knative.pagesDomain?.id,
},
}); });
}, },
setKnativeHostname(hostname) { setKnativeDomain({ domainId, domain }) {
eventHub.$emit('setKnativeHostname', { eventHub.$emit('setKnativeDomain', {
id: 'knative', id: 'knative',
hostname, domainId,
domain,
}); });
}, },
setCrossplaneProviderStack(stack) { setCrossplaneProviderStack(stack) {
...@@ -591,7 +595,10 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity ...@@ -591,7 +595,10 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
:request-reason="applications.knative.requestReason" :request-reason="applications.knative.requestReason"
:installed="applications.knative.installed" :installed="applications.knative.installed"
:install-failed="applications.knative.installFailed" :install-failed="applications.knative.installFailed"
:install-application-request-params="{ hostname: applications.knative.hostname }" :install-application-request-params="{
hostname: applications.knative.hostname,
pages_domain_id: applications.knative.pagesDomain && applications.knative.pagesDomain.id,
}"
:installed-via="installedVia" :installed-via="installedVia"
:uninstallable="applications.knative.uninstallable" :uninstallable="applications.knative.uninstallable"
:uninstall-successful="applications.knative.uninstallSuccessful" :uninstall-successful="applications.knative.uninstallSuccessful"
...@@ -628,7 +635,7 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity ...@@ -628,7 +635,7 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
:knative="knative" :knative="knative"
:ingress-dns-help-path="ingressDnsHelpPath" :ingress-dns-help-path="ingressDnsHelpPath"
@save="saveKnativeDomain" @save="saveKnativeDomain"
@set="setKnativeHostname" @set="setKnativeDomain"
/> />
</div> </div>
</application-row> </application-row>
......
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import {
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
GlSprintf,
} from '@gitlab/ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue'; import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
...@@ -13,6 +20,11 @@ export default { ...@@ -13,6 +20,11 @@ export default {
LoadingButton, LoadingButton,
ClipboardButton, ClipboardButton,
GlLoadingIcon, GlLoadingIcon,
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByType,
GlSprintf,
}, },
props: { props: {
knative: { knative: {
...@@ -25,6 +37,11 @@ export default { ...@@ -25,6 +37,11 @@ export default {
required: false, required: false,
}, },
}, },
data() {
return {
searchQuery: '',
};
},
computed: { computed: {
saveButtonDisabled() { saveButtonDisabled() {
return [UNINSTALLING, UPDATING].includes(this.knative.status); return [UNINSTALLING, UPDATING].includes(this.knative.status);
...@@ -49,9 +66,22 @@ export default { ...@@ -49,9 +66,22 @@ export default {
return this.knative.hostname; return this.knative.hostname;
}, },
set(hostname) { set(hostname) {
this.$emit('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;
},
}, },
watch: { watch: {
knativeUpdateSuccessful(updateSuccessful) { knativeUpdateSuccessful(updateSuccessful) {
...@@ -60,6 +90,14 @@ export default { ...@@ -60,6 +90,14 @@ export default {
} }
}, },
}, },
methods: {
selectDomain({ id, domain }) {
this.$emit('set', { domain, domainId: id });
},
selectCustomDomain(domain) {
this.$emit('set', { domain, domainId: null });
},
},
}; };
</script> </script>
...@@ -72,22 +110,55 @@ export default { ...@@ -72,22 +110,55 @@ export default {
{{ s__('ClusterIntegration|Something went wrong while updating Knative domain name.') }} {{ s__('ClusterIntegration|Something went wrong while updating Knative domain name.') }}
</div> </div>
<template> <div
<div :class="{ 'col-md-6': knativeInstalled, 'col-12': !knativeInstalled }"
:class="{ 'col-md-6': knativeInstalled, 'col-12': !knativeInstalled }" class="form-group col-sm-12 mb-0"
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"
> >
<label for="knative-domainname"> <gl-search-box-by-type
<strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong> v-model.trim="searchQuery"
</label> :placeholder="s__('ClusterIntegration|Search domains')"
<input class="m-2"
id="knative-domainname"
v-model="knativeHostname"
type="text"
class="form-control js-knative-domainname"
/> />
</div> <gl-dropdown-item
</template> 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"
/>
</div>
<template v-if="knativeInstalled"> <template v-if="knativeInstalled">
<div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0"> <div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0">
<label for="knative-endpoint"> <label for="knative-endpoint">
...@@ -144,7 +215,7 @@ export default { ...@@ -144,7 +215,7 @@ export default {
:loading="saving" :loading="saving"
:disabled="saveButtonDisabled" :disabled="saveButtonDisabled"
:label="saveButtonLabel" :label="saveButtonLabel"
@click="$emit('save', knativeHostname)" @click="$emit('save')"
/> />
</template> </template>
</div> </div>
......
...@@ -93,7 +93,7 @@ export default class ClusterStore { ...@@ -93,7 +93,7 @@ export default class ClusterStore {
...applicationInitialState, ...applicationInitialState,
title: s__('ClusterIntegration|Knative'), title: s__('ClusterIntegration|Knative'),
hostname: null, hostname: null,
isEditingHostName: false, isEditingDomain: false,
externalIp: null, externalIp: null,
externalHostname: null, externalHostname: null,
updateSuccessful: false, updateSuccessful: false,
...@@ -234,7 +234,12 @@ export default class ClusterStore { ...@@ -234,7 +234,12 @@ export default class ClusterStore {
'jupyter', 'jupyter',
); );
} else if (appId === KNATIVE) { } else if (appId === KNATIVE) {
if (!this.state.applications.knative.isEditingHostName) { 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 = this.state.applications.knative.hostname =
serverAppEntry.hostname || this.state.applications.knative.hostname; serverAppEntry.hostname || this.state.applications.knative.hostname;
} }
......
...@@ -318,3 +318,11 @@ export const setUrlParams = (params, url = window.location.href, clearParams = f ...@@ -318,3 +318,11 @@ export const setUrlParams = (params, url = window.location.href, clearParams = f
export function urlIsDifferent(url, compare = String(window.location)) { export function urlIsDifferent(url, compare = String(window.location)) {
return url !== compare; return url !== compare;
} }
export function getHTTPProtocol(url) {
if (!url) {
return window.location.protocol.slice(0, -1);
}
const protocol = url.split(':');
return protocol.length > 1 ? protocol[0] : undefined;
}
<script>
import {
GlNewDropdown,
GlNewDropdownHeader,
GlFormInputGroup,
GlNewButton,
GlIcon,
GlTooltipDirective,
} from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { getHTTPProtocol } from '~/lib/utils/url_utility';
export default {
components: {
GlNewDropdown,
GlNewDropdownHeader,
GlFormInputGroup,
GlNewButton,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
sshLink: {
type: String,
required: false,
default: '',
},
httpLink: {
type: String,
required: false,
default: '',
},
},
computed: {
httpLabel() {
const protocol = this.httpLink ? getHTTPProtocol(this.httpLink)?.toUpperCase() : '';
return sprintf(__('Clone with %{protocol}'), { protocol });
},
},
labels: {
defaultLabel: __('Clone'),
ssh: __('Clone with SSH'),
},
copyURLTooltip: __('Copy URL'),
};
</script>
<template>
<gl-new-dropdown :text="$options.labels.defaultLabel" category="primary" variant="info">
<div class="pb-2 mx-1">
<template v-if="sshLink">
<gl-new-dropdown-header>{{ $options.labels.ssh }}</gl-new-dropdown-header>
<div class="mx-3">
<gl-form-input-group :value="sshLink" readonly select-on-click>
<template #append>
<gl-new-button
v-gl-tooltip.hover
:title="$options.copyURLTooltip"
:data-clipboard-text="sshLink"
>
<gl-icon name="copy-to-clipboard" :title="$options.copyURLTooltip" />
</gl-new-button>
</template>
</gl-form-input-group>
</div>
</template>
<template v-if="httpLink">
<gl-new-dropdown-header>{{ httpLabel }}</gl-new-dropdown-header>
<div class="mx-3">
<gl-form-input-group :value="httpLink" readonly select-on-click>
<template #append>
<gl-new-button
v-gl-tooltip.hover
:title="$options.copyURLTooltip"
:data-clipboard-text="httpLink"
>
<gl-icon name="copy-to-clipboard" :title="$options.copyURLTooltip" />
</gl-new-button>
</template>
</gl-form-input-group>
</div>
</template>
</div>
</gl-new-dropdown>
</template>
...@@ -5,6 +5,12 @@ class Admin::IntegrationsController < Admin::ApplicationController ...@@ -5,6 +5,12 @@ class Admin::IntegrationsController < Admin::ApplicationController
private private
def find_or_initialize_integration(name)
if name.in?(Service.available_services_names)
"#{name}_service".camelize.constantize.find_or_initialize_by(instance: true) # rubocop:disable CodeReuse/ActiveRecord
end
end
def integrations_enabled? def integrations_enabled?
Feature.enabled?(:instance_level_integrations) Feature.enabled?(:instance_level_integrations)
end end
......
...@@ -47,7 +47,7 @@ class Clusters::ApplicationsController < Clusters::BaseController ...@@ -47,7 +47,7 @@ class Clusters::ApplicationsController < Clusters::BaseController
end end
def cluster_application_params def cluster_application_params
params.permit(:application, :hostname, :email, :stack, :modsecurity_enabled, :modsecurity_mode) params.permit(:application, :hostname, :pages_domain_id, :email, :stack, :modsecurity_enabled, :modsecurity_mode)
end end
def cluster_application_destroy_params def cluster_application_destroy_params
......
...@@ -37,11 +37,7 @@ module IntegrationsActions ...@@ -37,11 +37,7 @@ module IntegrationsActions
end end
def test def test
if integration.can_test? render json: {}, status: :ok
render json: service_test_response, status: :ok
else
render json: {}, status: :not_found
end
end end
private private
...@@ -50,17 +46,11 @@ module IntegrationsActions ...@@ -50,17 +46,11 @@ module IntegrationsActions
false false
end end
# TODO: Use actual integrations on the group / instance level
# To be completed in https://gitlab.com/groups/gitlab-org/-/epics/2430
def project
Project.first
end
def integration def integration
# Using instance variable `@service` still required as it's used in ServiceParams # Using instance variable `@service` still required as it's used in ServiceParams
# and app/views/shared/_service_settings.html.haml. Should be removed once # and app/views/shared/_service_settings.html.haml. Should be removed once
# those 2 are refactored to use `@integration`. # those 2 are refactored to use `@integration`.
@integration = @service ||= project.find_or_initialize_service(params[:id]) # rubocop:disable Gitlab/ModuleWithInstanceVariables @integration = @service ||= find_or_initialize_integration(params[:id]) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end end
def success_message def success_message
...@@ -74,21 +64,4 @@ module IntegrationsActions ...@@ -74,21 +64,4 @@ module IntegrationsActions
.as_json(only: integration.json_fields) .as_json(only: integration.json_fields)
.merge(errors: integration.errors.as_json) .merge(errors: integration.errors.as_json)
end end
def service_test_response
unless integration.update(service_params[:service])
return { error: true, message: _('Validations failed.'), service_response: integration.errors.full_messages.join(','), test_failed: false }
end
data = integration.test_data(project, current_user)
outcome = integration.test(data)
unless outcome[:success]
return { error: true, message: _('Test failed.'), service_response: outcome[:result].to_s, test_failed: true }
end
{}
rescue Gitlab::HTTP::BlockedUrlError => e
{ error: true, message: _('Test failed.'), service_response: e.message, test_failed: true }
end
end end
...@@ -9,6 +9,12 @@ module Groups ...@@ -9,6 +9,12 @@ module Groups
private private
# TODO: Make this compatible with group-level integration
# https://gitlab.com/groups/gitlab-org/-/epics/2543
def find_or_initialize_integration(name)
Project.first.find_or_initialize_service(name)
end
def integrations_enabled? def integrations_enabled?
Feature.enabled?(:group_level_integrations, group) Feature.enabled?(:group_level_integrations, group)
end end
......
...@@ -201,8 +201,7 @@ class JiraService < IssueTrackerService ...@@ -201,8 +201,7 @@ class JiraService < IssueTrackerService
end end
# Jira does not need test data. # Jira does not need test data.
# We are requesting the project that belongs to the project key. def test_data(_, _)
def test_data(user = nil, project = nil)
nil nil
end end
...@@ -221,7 +220,6 @@ class JiraService < IssueTrackerService ...@@ -221,7 +220,6 @@ class JiraService < IssueTrackerService
def test_settings def test_settings
return unless client_url.present? return unless client_url.present?
# Test settings by getting the project
jira_request { client.ServerInfo.all.attrs } jira_request { client.ServerInfo.all.attrs }
end end
......
...@@ -53,7 +53,7 @@ class PipelinesEmailService < Service ...@@ -53,7 +53,7 @@ class PipelinesEmailService < Service
end end
def can_test? def can_test?
project.ci_pipelines.any? project&.ci_pipelines&.any?
end end
def test_data(project, user) def test_data(project, user)
......
...@@ -184,8 +184,10 @@ class Service < ApplicationRecord ...@@ -184,8 +184,10 @@ class Service < ApplicationRecord
{ success: result.present?, result: result } { success: result.present?, result: result }
end end
# Disable test for instance-level services.
# https://gitlab.com/gitlab-org/gitlab/-/issues/213138
def can_test? def can_test?
true !instance?
end end
# Provide convenient accessor methods # Provide convenient accessor methods
......
---
title: Consume remaining LinkLFsObjectsProjects jobs
merge_request: 27558
author:
type: other
---
title: Remove unnecessary index index_ci_builds_on_name_for_security_reports_values
merge_request: 28224
author:
type: performance
# frozen_string_literal: true
class RemoveIndexCiBuildsOnNameForSecurityReportsValues < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
INDEX_NAME = 'index_ci_builds_on_name_for_security_reports_values'
def up
remove_concurrent_index_by_name :ci_builds, INDEX_NAME
end
def down
add_concurrent_index :ci_builds,
:name,
name: INDEX_NAME,
where: "((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('sast'::character varying)::text, ('license_scanning'::character varying)::text]))"
end
end
# frozen_string_literal: true
class ConsumeRemainingLinkLfsObjectsProjectsJobs < ActiveRecord::Migration[6.0]
DOWNTIME = false
disable_ddl_transaction!
def up
Gitlab::BackgroundMigration.steal('LinkLfsObjectsProjects')
end
def down
# no-op as there is no need to do anything if this gets rolled back
end
end
...@@ -8672,8 +8672,6 @@ CREATE INDEX index_ci_builds_on_commit_id_and_type_and_ref ON public.ci_builds U ...@@ -8672,8 +8672,6 @@ CREATE INDEX index_ci_builds_on_commit_id_and_type_and_ref ON public.ci_builds U
CREATE INDEX index_ci_builds_on_name_and_security_type_eq_ci_build ON public.ci_builds USING btree (name, id) WHERE (((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('sast'::character varying)::text, ('license_scanning'::character varying)::text])) AND ((type)::text = 'Ci::Build'::text)); CREATE INDEX index_ci_builds_on_name_and_security_type_eq_ci_build ON public.ci_builds USING btree (name, id) WHERE (((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('sast'::character varying)::text, ('license_scanning'::character varying)::text])) AND ((type)::text = 'Ci::Build'::text));
CREATE INDEX index_ci_builds_on_name_for_security_reports_values ON public.ci_builds USING btree (name) WHERE ((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('sast'::character varying)::text, ('license_scanning'::character varying)::text]));
CREATE INDEX index_ci_builds_on_project_id_and_id ON public.ci_builds USING btree (project_id, id); CREATE INDEX index_ci_builds_on_project_id_and_id ON public.ci_builds USING btree (project_id, id);
CREATE INDEX index_ci_builds_on_project_id_and_name_and_ref ON public.ci_builds USING btree (project_id, name, ref) WHERE (((type)::text = 'Ci::Build'::text) AND ((status)::text = 'success'::text) AND ((retried = false) OR (retried IS NULL))); CREATE INDEX index_ci_builds_on_project_id_and_name_and_ref ON public.ci_builds USING btree (project_id, name, ref) WHERE (((type)::text = 'Ci::Build'::text) AND ((status)::text = 'success'::text) AND ((retried = false) OR (retried IS NULL)));
...@@ -12902,6 +12900,7 @@ COPY "schema_migrations" (version) FROM STDIN; ...@@ -12902,6 +12900,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200318164448 20200318164448
20200318165448 20200318165448
20200318175008 20200318175008
20200319071702
20200319123041 20200319123041
20200319203901 20200319203901
20200320112455 20200320112455
...@@ -12927,6 +12926,7 @@ COPY "schema_migrations" (version) FROM STDIN; ...@@ -12927,6 +12926,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200326145443 20200326145443
20200330074719 20200330074719
20200330121000 20200330121000
20200330123739
20200330132913 20200330132913
20200331220930 20200331220930
\. \.
......
...@@ -4034,6 +4034,9 @@ msgstr "" ...@@ -4034,6 +4034,9 @@ msgstr ""
msgid "Clone with %{http_label}" msgid "Clone with %{http_label}"
msgstr "" msgstr ""
msgid "Clone with %{protocol}"
msgstr ""
msgid "Clone with KRB5" msgid "Clone with KRB5"
msgstr "" msgstr ""
...@@ -4691,6 +4694,9 @@ msgstr "" ...@@ -4691,6 +4694,9 @@ msgstr ""
msgid "ClusterIntegration|Search VPCs" msgid "ClusterIntegration|Search VPCs"
msgstr "" msgstr ""
msgid "ClusterIntegration|Search domains"
msgstr ""
msgid "ClusterIntegration|Search instance types" msgid "ClusterIntegration|Search instance types"
msgstr "" msgstr ""
...@@ -4748,6 +4754,9 @@ msgstr "" ...@@ -4748,6 +4754,9 @@ msgstr ""
msgid "ClusterIntegration|Select a zone to choose a network" msgid "ClusterIntegration|Select a zone to choose a network"
msgstr "" msgstr ""
msgid "ClusterIntegration|Select existing domain or use new"
msgstr ""
msgid "ClusterIntegration|Select machine type" msgid "ClusterIntegration|Select machine type"
msgstr "" msgstr ""
...@@ -4868,6 +4877,9 @@ msgstr "" ...@@ -4868,6 +4877,9 @@ msgstr ""
msgid "ClusterIntegration|Update failed. Please check the logs and try again." msgid "ClusterIntegration|Update failed. Please check the logs and try again."
msgstr "" msgstr ""
msgid "ClusterIntegration|Use %{query}"
msgstr ""
msgid "ClusterIntegration|Uses the Cloud Run, Istio, and HTTP Load Balancing addons for this cluster." msgid "ClusterIntegration|Uses the Cloud Run, Istio, and HTTP Load Balancing addons for this cluster."
msgstr "" msgstr ""
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
require 'spec_helper' require 'spec_helper'
describe Admin::IntegrationsController do describe Admin::IntegrationsController do
let_it_be(:project) { create(:project) }
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
before do before do
...@@ -34,7 +33,7 @@ describe Admin::IntegrationsController do ...@@ -34,7 +33,7 @@ describe Admin::IntegrationsController do
end end
describe '#update' do describe '#update' do
let(:integration) { create(:jira_service, project: project) } let(:integration) { create(:jira_service, :instance) }
before do before do
put :update, params: { id: integration.class.to_param, service: { url: url } } put :update, params: { id: integration.class.to_param, service: { url: url } }
...@@ -52,34 +51,9 @@ describe Admin::IntegrationsController do ...@@ -52,34 +51,9 @@ describe Admin::IntegrationsController do
context 'invalid params' do context 'invalid params' do
let(:url) { 'https://jira.localhost' } let(:url) { 'https://jira.localhost' }
it 'does not update the integration' do it 'updates the integration' do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:found)
expect(response).to render_template(:edit) expect(integration.reload.url).to eq(url)
expect(integration.reload.url).not_to eq(url)
end
end
end
describe '#test' do
context 'testable' do
let(:integration) { create(:jira_service, project: project) }
it 'returns ok' do
allow_any_instance_of(integration.class).to receive(:test) { { success: true } }
put :test, params: { id: integration.class.to_param }
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'not testable' do
let(:integration) { create(:alerts_service, project: project) }
it 'returns not found' do
put :test, params: { id: integration.class.to_param }
expect(response).to have_gitlab_http_status(:not_found)
end end
end end
end end
......
...@@ -67,40 +67,11 @@ describe Groups::Settings::IntegrationsController do ...@@ -67,40 +67,11 @@ describe Groups::Settings::IntegrationsController do
end end
context 'invalid params' do context 'invalid params' do
let(:url) { 'ftp://jira.localhost' } let(:url) { 'https://jira.localhost' }
it 'does not update the integration' do it 'does not update the integration' do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:found)
expect(response).to render_template(:edit) expect(integration.reload.url).to eq(url)
expect(integration.reload.url).not_to eq(url)
end
end
end
describe '#test' do
context 'testable' do
let(:integration) { create(:jira_service, project: project) }
before do
group.add_owner(user)
end
it 'returns ok' do
allow_any_instance_of(integration.class).to receive(:test) { { success: true } }
put :test, params: { group_id: group, id: integration.class.to_param }
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'not testable' do
let(:integration) { create(:alerts_service, project: project) }
it 'returns not found' do
put :test, params: { group_id: group, id: integration.class.to_param }
expect(response).to have_gitlab_http_status(:not_found)
end end
end end
end end
......
// `lodash/debounce` has a non-trivial implementation which can lead to
// [flaky spec errors][1]. This mock simply makes `debounce` calls synchronous.
//
// In the future we could enhance this by injecting some test values in
// the function passed to it. See [this issue][2] for more information.
//
// [1]: https://gitlab.com/gitlab-org/gitlab/-/issues/212532
// [2]: https://gitlab.com/gitlab-org/gitlab/-/issues/213378
// Further reference: https://github.com/facebook/jest/issues/3465
export default fn => fn;
...@@ -400,6 +400,10 @@ describe('Applications', () => { ...@@ -400,6 +400,10 @@ describe('Applications', () => {
}); });
describe('Knative application', () => { describe('Knative application', () => {
const availableDomain = {
id: 4,
domain: 'newhostname.com',
};
const propsData = { const propsData = {
applications: { applications: {
...APPLICATIONS_MOCK_STATE, ...APPLICATIONS_MOCK_STATE,
...@@ -409,10 +413,11 @@ describe('Applications', () => { ...@@ -409,10 +413,11 @@ describe('Applications', () => {
status: 'installed', status: 'installed',
externalIp: '1.1.1.1', externalIp: '1.1.1.1',
installed: true, installed: true,
availableDomains: [availableDomain],
pagesDomain: null,
}, },
}, },
}; };
const newHostname = 'newhostname.com';
let wrapper; let wrapper;
let knativeDomainEditor; let knativeDomainEditor;
...@@ -428,20 +433,44 @@ describe('Applications', () => { ...@@ -428,20 +433,44 @@ describe('Applications', () => {
}); });
it('emits saveKnativeDomain event when knative domain editor emits save event', () => { it('emits saveKnativeDomain event when knative domain editor emits save event', () => {
knativeDomainEditor.vm.$emit('save', newHostname); 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', { expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', {
id: 'knative', id: 'knative',
params: { hostname: newHostname }, params: {
hostname: newHostName,
pages_domain_id: undefined,
},
}); });
}); });
it('emits setKnativeHostname event when knative domain editor emits change event', () => { it('emits setKnativeHostname event when knative domain editor emits change event', () => {
wrapper.find(KnativeDomainEditor).vm.$emit('set', newHostname); wrapper.find(KnativeDomainEditor).vm.$emit('set', {
domain: availableDomain.domain,
domainId: availableDomain.id,
});
expect(eventHub.$emit).toHaveBeenCalledWith('setKnativeHostname', { expect(eventHub.$emit).toHaveBeenCalledWith('setKnativeDomain', {
id: 'knative', id: 'knative',
hostname: newHostname, domain: availableDomain.domain,
domainId: availableDomain.id,
}); });
}); });
}); });
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlDropdownItem } from '@gitlab/ui';
import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue'; import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { APPLICATION_STATUS } from '~/clusters/constants'; import { APPLICATION_STATUS } from '~/clusters/constants';
...@@ -80,7 +81,7 @@ describe('KnativeDomainEditor', () => { ...@@ -80,7 +81,7 @@ describe('KnativeDomainEditor', () => {
it('triggers save event and pass current knative hostname', () => { it('triggers save event and pass current knative hostname', () => {
wrapper.find(LoadingButton).vm.$emit('click'); wrapper.find(LoadingButton).vm.$emit('click');
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('save')[0]).toEqual([knative.hostname]); expect(wrapper.emitted('save').length).toEqual(1);
}); });
}); });
}); });
...@@ -104,14 +105,43 @@ describe('KnativeDomainEditor', () => { ...@@ -104,14 +105,43 @@ describe('KnativeDomainEditor', () => {
describe('when knative domain name input changes', () => { describe('when knative domain name input changes', () => {
it('emits "set" event with updated domain name', () => { it('emits "set" event with updated domain name', () => {
createComponent({ knative }); 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'; const newHostname = 'newhostname.com';
createComponent({ knative });
jest.spyOn(wrapper.vm, 'selectCustomDomain');
wrapper.setData({ knativeHostname: newHostname }); wrapper.setData({ knativeHostname: newHostname });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('set')[0]).toEqual([newHostname]); expect(wrapper.vm.selectCustomDomain).toHaveBeenCalledWith(newHostname);
expect(wrapper.emitted('set')[0]).toEqual([
{
domain: newHostname,
domainId: null,
},
]);
}); });
}); });
}); });
......
...@@ -140,7 +140,7 @@ describe('Clusters Store', () => { ...@@ -140,7 +140,7 @@ describe('Clusters Store', () => {
statusReason: mockResponseData.applications[5].status_reason, statusReason: mockResponseData.applications[5].status_reason,
requestReason: null, requestReason: null,
hostname: null, hostname: null,
isEditingHostName: false, isEditingDomain: false,
externalIp: null, externalIp: null,
externalHostname: null, externalHostname: null,
installed: false, installed: false,
......
...@@ -9,8 +9,6 @@ import { branches } from '../../mock_data'; ...@@ -9,8 +9,6 @@ import { branches } from '../../mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
jest.mock('lodash/debounce', () => jest.fn);
describe('IDE branches search list', () => { describe('IDE branches search list', () => {
let wrapper; let wrapper;
const fetchBranchesMock = jest.fn(); const fetchBranchesMock = jest.fn();
......
...@@ -134,9 +134,7 @@ describe('IDE merge requests list', () => { ...@@ -134,9 +134,7 @@ describe('IDE merge requests list', () => {
createComponent(defaultStateWithMergeRequests); createComponent(defaultStateWithMergeRequests);
const input = findTokenedInput(); const input = findTokenedInput();
input.vm.$emit('input', 'something'); input.vm.$emit('input', 'something');
fetchMergeRequestsMock.mockClear();
jest.runAllTimers();
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(fetchMergeRequestsMock).toHaveBeenCalledWith( expect(fetchMergeRequestsMock).toHaveBeenCalledWith(
expect.any(Object), expect.any(Object),
......
...@@ -485,4 +485,30 @@ describe('URL utility', () => { ...@@ -485,4 +485,30 @@ describe('URL utility', () => {
); );
}); });
}); });
describe('getHTTPProtocol', () => {
const httpProtocol = 'http:';
const httpsProtocol = 'https:';
it.each([[httpProtocol], [httpsProtocol]])(
'when no url passed, returns correct protocol for %i from window location',
protocol => {
setWindowLocation({
protocol,
});
expect(urlUtils.getHTTPProtocol()).toBe(protocol.slice(0, -1));
},
);
it.each`
url | expectation
${'not-a-url'} | ${undefined}
${'wss://example.com'} | ${'wss'}
${'https://foo.bar'} | ${'https'}
${'http://foo.bar'} | ${'http'}
${'http://foo.bar:8080'} | ${'http'}
`('returns correct protocol for $url', ({ url, expectation }) => {
expect(urlUtils.getHTTPProtocol(url)).toBe(expectation);
});
});
}); });
...@@ -20,7 +20,7 @@ afterEach(() => ...@@ -20,7 +20,7 @@ afterEach(() =>
// give Promises a bit more time so they fail the right test // give Promises a bit more time so they fail the right test
new Promise(setImmediate).then(() => { new Promise(setImmediate).then(() => {
// wait for pending setTimeout()s // wait for pending setTimeout()s
jest.runAllTimers(); jest.runOnlyPendingTimers();
}), }),
); );
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<gl-new-dropdown-stub
category="primary"
headertext=""
size="medium"
text="Clone"
variant="info"
>
<div
class="pb-2 mx-1"
>
<gl-new-dropdown-header-stub>
Clone with SSH
</gl-new-dropdown-header-stub>
<div
class="mx-3"
>
<div
readonly="readonly"
>
<b-input-group-stub
tag="div"
>
<b-input-group-prepend-stub
tag="div"
>
<!---->
</b-input-group-prepend-stub>
<b-form-input-stub
class="gl-form-input"
debounce="0"
readonly="true"
type="text"
value="ssh://foo.bar"
/>
<b-input-group-append-stub
tag="div"
>
<gl-new-button-stub
category="tertiary"
data-clipboard-text="ssh://foo.bar"
icon=""
size="medium"
title="Copy URL"
variant="default"
>
<gl-icon-stub
name="copy-to-clipboard"
size="16"
title="Copy URL"
/>
</gl-new-button-stub>
</b-input-group-append-stub>
</b-input-group-stub>
</div>
</div>
<gl-new-dropdown-header-stub>
Clone with HTTP
</gl-new-dropdown-header-stub>
<div
class="mx-3"
>
<div
readonly="readonly"
>
<b-input-group-stub
tag="div"
>
<b-input-group-prepend-stub
tag="div"
>
<!---->
</b-input-group-prepend-stub>
<b-form-input-stub
class="gl-form-input"
debounce="0"
readonly="true"
type="text"
value="http://foo.bar"
/>
<b-input-group-append-stub
tag="div"
>
<gl-new-button-stub
category="tertiary"
data-clipboard-text="http://foo.bar"
icon=""
size="medium"
title="Copy URL"
variant="default"
>
<gl-icon-stub
name="copy-to-clipboard"
size="16"
title="Copy URL"
/>
</gl-new-button-stub>
</b-input-group-append-stub>
</b-input-group-stub>
</div>
</div>
</div>
</gl-new-dropdown-stub>
`;
import CloneDropdown from '~/vue_shared/components/clone_dropdown.vue';
import { shallowMount } from '@vue/test-utils';
import { GlFormInputGroup, GlNewDropdownHeader } from '@gitlab/ui';
describe('Clone Dropdown Button', () => {
let wrapper;
const sshLink = 'ssh://foo.bar';
const httpLink = 'http://foo.bar';
const httpsLink = 'https://foo.bar';
const defaultPropsData = {
sshLink,
httpLink,
};
const createComponent = (propsData = defaultPropsData) => {
wrapper = shallowMount(CloneDropdown, {
propsData,
stubs: {
'gl-form-input-group': GlFormInputGroup,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('rendering', () => {
it('matches the snapshot', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it.each`
name | index | value
${'SSH'} | ${0} | ${sshLink}
${'HTTP'} | ${1} | ${httpLink}
`('renders correct link and a copy-button for $name', ({ index, value }) => {
createComponent();
const group = wrapper.findAll(GlFormInputGroup).at(index);
expect(group.props('value')).toBe(value);
expect(group.contains(GlFormInputGroup)).toBe(true);
});
it.each`
name | value
${'sshLink'} | ${sshLink}
${'httpLink'} | ${httpLink}
`('does not fail if only $name is set', ({ name, value }) => {
createComponent({ [name]: value });
expect(wrapper.find(GlFormInputGroup).props('value')).toBe(value);
expect(wrapper.findAll(GlNewDropdownHeader).length).toBe(1);
});
});
describe('functionality', () => {
it.each`
name | value
${'sshLink'} | ${null}
${'httpLink'} | ${null}
`('allows null values for the props', ({ name, value }) => {
createComponent({ ...defaultPropsData, [name]: value });
expect(wrapper.findAll(GlNewDropdownHeader).length).toBe(1);
});
it('correctly calculates httpLabel for HTTPS protocol', () => {
createComponent({ httpLink: httpsLink });
expect(wrapper.find(GlNewDropdownHeader).text()).toContain('HTTPS');
});
});
});
...@@ -104,21 +104,29 @@ describe Service do ...@@ -104,21 +104,29 @@ describe Service do
describe "Test Button" do describe "Test Button" do
describe '#can_test?' do describe '#can_test?' do
subject { service.can_test? }
let(:service) { create(:service, project: project) } let(:service) { create(:service, project: project) }
context 'when repository is not empty' do context 'when repository is not empty' do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
it 'returns true' do it { is_expected.to be true }
expect(service.can_test?).to be true
end
end end
context 'when repository is empty' do context 'when repository is empty' do
let(:project) { create(:project) } let(:project) { create(:project) }
it 'returns true' do it { is_expected.to be true }
expect(service.can_test?).to be true end
context 'when instance-level service' do
Service.available_services_types.each do |service_type|
let(:service) do
service_type.constantize.new(instance: true)
end
it { is_expected.to be_falsey }
end end
end end
end end
......
...@@ -27,14 +27,7 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto ...@@ -27,14 +27,7 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto
end end
context 'user does not have push right to repository' do context 'user does not have push right to repository' do
it 'returns an appropriate message and status code', :aggregate_failures do it_behaves_like 'misconfigured dashboard service response with stepable', :forbidden, 'You are not allowed to push into this branch. Create another branch or open a merge request.'
result = service_call
expect(result.keys).to contain_exactly(:message, :http_status, :status, :last_step)
expect(result[:status]).to eq(:error)
expect(result[:http_status]).to eq(:forbidden)
expect(result[:message]).to eq("You are not allowed to push into this branch. Create another branch or open a merge request.")
end
end end
context 'with rights to push to the repository' do context 'with rights to push to the repository' do
...@@ -46,27 +39,13 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto ...@@ -46,27 +39,13 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto
context 'with a yml extension' do context 'with a yml extension' do
let(:file_name) { 'config/prometheus/../database.yml' } let(:file_name) { 'config/prometheus/../database.yml' }
it 'returns an appropriate message and status code', :aggregate_failures do it_behaves_like 'misconfigured dashboard service response with stepable', :bad_request, "A file with this name doesn't exist"
result = service_call
expect(result.keys).to contain_exactly(:message, :http_status, :status, :last_step)
expect(result[:status]).to eq(:error)
expect(result[:http_status]).to eq(:bad_request)
expect(result[:message]).to eq("A file with this name doesn't exist")
end
end end
context 'without a yml extension' do context 'without a yml extension' do
let(:file_name) { '../../..../etc/passwd' } let(:file_name) { '../../..../etc/passwd' }
it 'returns an appropriate message and status code', :aggregate_failures do it_behaves_like 'misconfigured dashboard service response with stepable', :bad_request, 'The file name should have a .yml extension'
result = service_call
expect(result.keys).to contain_exactly(:message, :http_status, :status, :last_step)
expect(result[:status]).to eq(:error)
expect(result[:http_status]).to eq(:bad_request)
expect(result[:message]).to eq("The file name should have a .yml extension")
end
end end
end end
...@@ -81,14 +60,7 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto ...@@ -81,14 +60,7 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto
project.repository.add_branch(user, branch, 'master') project.repository.add_branch(user, branch, 'master')
end end
it 'returns an appropriate message and status code', :aggregate_failures do it_behaves_like 'misconfigured dashboard service response with stepable', :bad_request, 'There was an error updating the dashboard, branch named: existing_branch already exists.'
result = service_call
expect(result.keys).to contain_exactly(:message, :http_status, :status, :last_step)
expect(result[:status]).to eq(:error)
expect(result[:http_status]).to eq(:bad_request)
expect(result[:message]).to eq("There was an error updating the dashboard, branch named: existing_branch already exists.")
end
end end
context 'Files::UpdateService success' do context 'Files::UpdateService success' do
......
...@@ -786,10 +786,10 @@ ...@@ -786,10 +786,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.117.0.tgz#05239ddcf529c62ca29e1ec1a25a7e24efb98207" resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.117.0.tgz#05239ddcf529c62ca29e1ec1a25a7e24efb98207"
integrity sha512-dGy/VWuRAFCTZX3Yqu1+RnAHTSUWafteIk/RMfUCN9B/EMbYzjhYsNy0NLVoZ23Rj/KGv1bUGHvyQCoPP6VzpA== integrity sha512-dGy/VWuRAFCTZX3Yqu1+RnAHTSUWafteIk/RMfUCN9B/EMbYzjhYsNy0NLVoZ23Rj/KGv1bUGHvyQCoPP6VzpA==
"@gitlab/ui@11.0.0": "@gitlab/ui@11.0.1":
version "11.0.0" version "11.0.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-11.0.0.tgz#2dc6c4e92201911b80c1c24ff4ea6c5247810451" resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-11.0.1.tgz#7d9fdb823590c72c232b7dee06b86c3e8766ba28"
integrity sha512-Cu50RQMbNGxEfMIxr3iv6i09hNs3deRS01CnkxNEdxmmokyKr86a2TItHwrAKyacjkb8IEfkn0Q/yoBMTpfPAw== integrity sha512-JlZULrpmm2jELsVHfcMpE0uiam+hA+5tL4+xZxiHoG+i9UlTQCAteMHOgJVT7pQYvjPAoSnw9XzTATEEcHVcOw==
dependencies: dependencies:
"@babel/standalone" "^7.0.0" "@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0" "@gitlab/vue-toasted" "^1.3.0"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment