Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ec884edd46
commit
c6e6762bbf
33 changed files with 629 additions and 240 deletions
|
@ -1 +1 @@
|
|||
13.1.0-rc1
|
||||
d08b7024a0a2882dc55d8a480d891fc0ded5bb9b
|
||||
|
|
|
@ -1 +1 @@
|
|||
8.32.1
|
||||
8.33.0
|
||||
|
|
|
@ -281,14 +281,16 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
|
|||
class="rounded-top"
|
||||
title-link="https://docs.helm.sh/"
|
||||
>
|
||||
<div slot="description">
|
||||
{{
|
||||
s__(`ClusterIntegration|Helm streamlines installing
|
||||
and managing Kubernetes applications.
|
||||
Tiller runs inside of your Kubernetes Cluster,
|
||||
and manages releases of your charts.`)
|
||||
}}
|
||||
</div>
|
||||
<template #description>
|
||||
<div>
|
||||
{{
|
||||
s__(`ClusterIntegration|Helm streamlines installing
|
||||
and managing Kubernetes applications.
|
||||
Tiller runs inside of your Kubernetes Cluster,
|
||||
and manages releases of your charts.`)
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
</application-row>
|
||||
<div v-show="!helmInstalled" class="cluster-application-warning">
|
||||
<div class="svg-container" v-html="$options.helmInstallIllustration"></div>
|
||||
|
@ -318,76 +320,80 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
|
|||
:updateable="false"
|
||||
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
|
||||
>
|
||||
<div slot="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 #description>
|
||||
<div>
|
||||
<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>
|
||||
|
||||
<ingress-modsecurity-settings
|
||||
:ingress="ingress"
|
||||
:ingress-mod-security-help-path="ingressModSecurityHelpPath"
|
||||
/>
|
||||
<ingress-modsecurity-settings
|
||||
:ingress="ingress"
|
||||
:ingress-mod-security-help-path="ingressModSecurityHelpPath"
|
||||
/>
|
||||
|
||||
<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"
|
||||
<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>
|
||||
</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
|
||||
<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.`)
|
||||
}}
|
||||
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
|
||||
{{ __('More information') }}
|
||||
</a>
|
||||
</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.`)
|
||||
}}
|
||||
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
|
||||
{{ __('More information') }}
|
||||
</a>
|
||||
</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.`)
|
||||
}}
|
||||
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
|
||||
{{ __('More information') }}
|
||||
</a>
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="bs-callout bs-callout-info">
|
||||
<strong v-html="ingressDescription"></strong>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="bs-callout bs-callout-info">
|
||||
<strong v-html="ingressDescription"></strong>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</application-row>
|
||||
<application-row
|
||||
id="cert_manager"
|
||||
|
@ -406,8 +412,8 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
|
|||
:disabled="!helmInstalled"
|
||||
title-link="https://cert-manager.readthedocs.io/en/latest/#"
|
||||
>
|
||||
<template>
|
||||
<div slot="description">
|
||||
<template #description>
|
||||
<div>
|
||||
<p v-html="certManagerDescription"></p>
|
||||
<div class="form-group">
|
||||
<label for="cert-manager-issuer-email">
|
||||
|
@ -455,7 +461,9 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
|
|||
:disabled="!helmInstalled"
|
||||
title-link="https://prometheus.io/docs/introduction/overview/"
|
||||
>
|
||||
<div slot="description" v-html="prometheusDescription"></div>
|
||||
<template #description>
|
||||
<div v-html="prometheusDescription"></div>
|
||||
</template>
|
||||
</application-row>
|
||||
<application-row
|
||||
id="runner"
|
||||
|
@ -478,14 +486,16 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
|
|||
:disabled="!helmInstalled"
|
||||
title-link="https://docs.gitlab.com/runner/"
|
||||
>
|
||||
<div slot="description">
|
||||
{{
|
||||
s__(`ClusterIntegration|GitLab Runner connects to the
|
||||
repository and executes CI/CD jobs,
|
||||
pushing results back and deploying
|
||||
applications to production.`)
|
||||
}}
|
||||
</div>
|
||||
<template #description>
|
||||
<div>
|
||||
{{
|
||||
s__(`ClusterIntegration|GitLab Runner connects to the
|
||||
repository and executes CI/CD jobs,
|
||||
pushing results back and deploying
|
||||
applications to production.`)
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
</application-row>
|
||||
<application-row
|
||||
id="crossplane"
|
||||
|
@ -504,8 +514,8 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
|
|||
:disabled="!helmInstalled"
|
||||
title-link="https://crossplane.io"
|
||||
>
|
||||
<template>
|
||||
<div slot="description">
|
||||
<template #description>
|
||||
<div>
|
||||
<p v-html="crossplaneDescription"></p>
|
||||
<div class="form-group">
|
||||
<CrossplaneProviderStack :crossplane="crossplane" @set="setCrossplaneProviderStack" />
|
||||
|
@ -531,50 +541,54 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
|
|||
:disabled="!helmInstalled"
|
||||
title-link="https://jupyterhub.readthedocs.io/en/stable/"
|
||||
>
|
||||
<div slot="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.`)
|
||||
}}
|
||||
</p>
|
||||
<template #description>
|
||||
<div>
|
||||
<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.`)
|
||||
}}
|
||||
</p>
|
||||
|
||||
<template v-if="ingressExternalEndpoint">
|
||||
<div class="form-group">
|
||||
<label for="jupyter-hostname">{{ s__('ClusterIntegration|Jupyter Hostname') }}</label>
|
||||
<template v-if="ingressExternalEndpoint">
|
||||
<div class="form-group">
|
||||
<label for="jupyter-hostname">{{
|
||||
s__('ClusterIntegration|Jupyter Hostname')
|
||||
}}</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="jupyter-hostname"
|
||||
v-model="applications.jupyter.hostname"
|
||||
:readonly="jupyterInstalled"
|
||||
type="text"
|
||||
class="form-control js-hostname"
|
||||
/>
|
||||
<span class="input-group-btn">
|
||||
<clipboard-button
|
||||
:text="jupyterHostname"
|
||||
:title="s__('ClusterIntegration|Copy Jupyter Hostname')"
|
||||
class="js-clipboard-btn"
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="jupyter-hostname"
|
||||
v-model="applications.jupyter.hostname"
|
||||
:readonly="jupyterInstalled"
|
||||
type="text"
|
||||
class="form-control js-hostname"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<span class="input-group-btn">
|
||||
<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.`)
|
||||
}}
|
||||
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
|
||||
{{ __('More information') }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</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.`)
|
||||
}}
|
||||
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
|
||||
{{ __('More information') }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</application-row>
|
||||
<application-row
|
||||
id="knative"
|
||||
|
@ -599,33 +613,36 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
|
|||
v-bind="applications.knative"
|
||||
title-link="https://github.com/knative/docs"
|
||||
>
|
||||
<div slot="description">
|
||||
<p v-if="!rbac" class="rbac-notice bs-callout bs-callout-info">
|
||||
{{
|
||||
s__(`ClusterIntegration|You must have an RBAC-enabled cluster
|
||||
to install Knative.`)
|
||||
}}
|
||||
<a :href="helpPath" target="_blank" rel="noopener noreferrer">
|
||||
{{ __('More information') }}
|
||||
</a>
|
||||
</p>
|
||||
<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>
|
||||
<template #description>
|
||||
<div>
|
||||
<p v-if="!rbac" class="rbac-notice bs-callout bs-callout-info gl-mb-0">
|
||||
{{
|
||||
s__(`ClusterIntegration|You must have an RBAC-enabled cluster
|
||||
to install Knative.`)
|
||||
}}
|
||||
<a :href="helpPath" target="_blank" rel="noopener noreferrer">
|
||||
{{ __('More information') }}
|
||||
</a>
|
||||
</p>
|
||||
<br />
|
||||
<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 || (helmInstalled && rbac)) && !preInstalledKnative"
|
||||
:knative="knative"
|
||||
:ingress-dns-help-path="ingressDnsHelpPath"
|
||||
@save="saveKnativeDomain"
|
||||
@set="setKnativeDomain"
|
||||
/>
|
||||
</div>
|
||||
<knative-domain-editor
|
||||
v-if="(knative.installed || (helmInstalled && rbac)) && !preInstalledKnative"
|
||||
:knative="knative"
|
||||
:ingress-dns-help-path="ingressDnsHelpPath"
|
||||
@save="saveKnativeDomain"
|
||||
@set="setKnativeDomain"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</application-row>
|
||||
<application-row
|
||||
id="elastic_stack"
|
||||
|
@ -648,15 +665,17 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
|
|||
:disabled="!helmInstalled"
|
||||
title-link="https://gitlab.com/gitlab-org/charts/elastic-stack"
|
||||
>
|
||||
<div slot="description">
|
||||
<p>
|
||||
{{
|
||||
s__(
|
||||
`ClusterIntegration|The elastic stack collects logs from all pods in your cluster`,
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<template #description>
|
||||
<div>
|
||||
<p>
|
||||
{{
|
||||
s__(
|
||||
`ClusterIntegration|The elastic stack collects logs from all pods in your cluster`,
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</application-row>
|
||||
|
||||
<application-row
|
||||
|
@ -683,25 +702,27 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
|
|||
:updateable="false"
|
||||
title-link="https://github.com/helm/charts/tree/master/stable/fluentd"
|
||||
>
|
||||
<div slot="description">
|
||||
<p>
|
||||
{{
|
||||
s__(
|
||||
`ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed.`,
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<template #description>
|
||||
<div>
|
||||
<p>
|
||||
{{
|
||||
s__(
|
||||
`ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed.`,
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
|
||||
<fluentd-output-settings
|
||||
:port="applications.fluentd.port"
|
||||
:protocol="applications.fluentd.protocol"
|
||||
:host="applications.fluentd.host"
|
||||
:waf-log-enabled="applications.fluentd.wafLogEnabled"
|
||||
:cilium-log-enabled="applications.fluentd.ciliumLogEnabled"
|
||||
:status="applications.fluentd.status"
|
||||
:update-failed="applications.fluentd.updateFailed"
|
||||
/>
|
||||
</div>
|
||||
<fluentd-output-settings
|
||||
:port="applications.fluentd.port"
|
||||
:protocol="applications.fluentd.protocol"
|
||||
:host="applications.fluentd.host"
|
||||
:waf-log-enabled="applications.fluentd.wafLogEnabled"
|
||||
:cilium-log-enabled="applications.fluentd.ciliumLogEnabled"
|
||||
:status="applications.fluentd.status"
|
||||
:update-failed="applications.fluentd.updateFailed"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</application-row>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -12,3 +12,5 @@ export const ACTIVE_DISCUSSION_SOURCE_TYPES = {
|
|||
pin: 'pin',
|
||||
discussion: 'discussion',
|
||||
};
|
||||
|
||||
export const DESIGN_DETAIL_LAYOUT_CLASSLIST = ['design-detail-layout', 'overflow-hidden', 'm-0'];
|
||||
|
|
|
@ -2,6 +2,9 @@ import $ from 'jquery';
|
|||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import routes from './routes';
|
||||
import { DESIGN_ROUTE_NAME } from './constants';
|
||||
import { getPageLayoutElement } from '~/design_management/utils/design_management_utils';
|
||||
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../constants';
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
|
@ -11,10 +14,20 @@ export default function createRouter(base) {
|
|||
mode: 'history',
|
||||
routes,
|
||||
});
|
||||
const pageEl = getPageLayoutElement();
|
||||
|
||||
router.beforeEach(({ meta: { el } }, from, next) => {
|
||||
router.beforeEach(({ meta: { el }, name }, _, next) => {
|
||||
$(`#${el}`).tab('show');
|
||||
|
||||
// apply a fullscreen layout style in Design View (a.k.a design detail)
|
||||
if (pageEl) {
|
||||
if (name === DESIGN_ROUTE_NAME) {
|
||||
pageEl.classList.add(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
|
||||
} else {
|
||||
pageEl.classList.remove(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
|
|
|
@ -123,3 +123,5 @@ const normalizeAuthor = author => ({
|
|||
});
|
||||
|
||||
export const extractParticipants = users => users.edges.map(({ node }) => normalizeAuthor(node));
|
||||
|
||||
export const getPageLayoutElement = () => document.querySelector('.layout-page');
|
||||
|
|
|
@ -8,12 +8,25 @@ import {
|
|||
GlIcon,
|
||||
GlTooltipDirective,
|
||||
GlFormInput,
|
||||
GlFormSelect,
|
||||
} from '@gitlab/ui';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { DEFAULT_ASSET_LINK_TYPE, ASSET_LINK_TYPE } from '../constants';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'AssetLinksForm',
|
||||
components: { GlSprintf, GlLink, GlFormGroup, GlButton, GlIcon, GlFormInput },
|
||||
components: {
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
GlFormGroup,
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlFormInput,
|
||||
GlFormSelect,
|
||||
},
|
||||
directives: { GlTooltip: GlTooltipDirective },
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
computed: {
|
||||
...mapState('detail', ['release', 'releaseAssetsDocsPath']),
|
||||
...mapGetters('detail', ['validationErrors']),
|
||||
|
@ -26,6 +39,7 @@ export default {
|
|||
'addEmptyAssetLink',
|
||||
'updateAssetLinkUrl',
|
||||
'updateAssetLinkName',
|
||||
'updateAssetLinkType',
|
||||
'removeAssetLink',
|
||||
]),
|
||||
onAddAnotherClicked() {
|
||||
|
@ -35,12 +49,6 @@ export default {
|
|||
this.removeAssetLink(linkId);
|
||||
this.ensureAtLeastOneLink();
|
||||
},
|
||||
onUrlInput(linkIdToUpdate, newUrl) {
|
||||
this.updateAssetLinkUrl({ linkIdToUpdate, newUrl });
|
||||
},
|
||||
onLinkTitleInput(linkIdToUpdate, newName) {
|
||||
this.updateAssetLinkName({ linkIdToUpdate, newName });
|
||||
},
|
||||
hasDuplicateUrl(link) {
|
||||
return Boolean(this.getLinkErrors(link).isDuplicate);
|
||||
},
|
||||
|
@ -73,6 +81,13 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
typeOptions: [
|
||||
{ value: ASSET_LINK_TYPE.IMAGE, text: s__('ReleaseAssetLinkType|Image') },
|
||||
{ value: ASSET_LINK_TYPE.PACKAGE, text: s__('ReleaseAssetLinkType|Package') },
|
||||
{ value: ASSET_LINK_TYPE.RUNBOOK, text: s__('ReleaseAssetLinkType|Runbook') },
|
||||
{ value: ASSET_LINK_TYPE.OTHER, text: s__('ReleaseAssetLinkType|Other') },
|
||||
],
|
||||
defaultTypeOptionValue: DEFAULT_ASSET_LINK_TYPE,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -123,7 +138,7 @@ export default {
|
|||
type="text"
|
||||
class="form-control"
|
||||
:state="isUrlValid(link)"
|
||||
@change="onUrlInput(link.id, $event)"
|
||||
@change="updateAssetLinkUrl({ linkIdToUpdate: link.id, newUrl: $event })"
|
||||
/>
|
||||
<template #invalid-feedback>
|
||||
<span v-if="hasEmptyUrl(link)" class="invalid-feedback d-inline">
|
||||
|
@ -160,7 +175,7 @@ export default {
|
|||
type="text"
|
||||
class="form-control"
|
||||
:state="isNameValid(link)"
|
||||
@change="onLinkTitleInput(link.id, $event)"
|
||||
@change="updateAssetLinkName({ linkIdToUpdate: link.id, newName: $event })"
|
||||
/>
|
||||
<template #invalid-feedback>
|
||||
<span v-if="hasEmptyName(link)" class="invalid-feedback d-inline">
|
||||
|
@ -169,6 +184,22 @@ export default {
|
|||
</template>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group
|
||||
v-if="glFeatures.releaseAssetLinkType"
|
||||
class="link-type-field col-auto"
|
||||
:label="__('Type')"
|
||||
:label-for="`asset-type-${index}`"
|
||||
>
|
||||
<gl-form-select
|
||||
:id="`asset-type-${index}`"
|
||||
ref="typeSelect"
|
||||
:value="link.linkType || $options.defaultTypeOptionValue"
|
||||
class="form-control pr-4"
|
||||
:options="$options.typeOptions"
|
||||
@change="updateAssetLinkType({ linkIdToUpdate: link.id, newType: $event })"
|
||||
/>
|
||||
</gl-form-group>
|
||||
|
||||
<div class="mb-5 mb-sm-3 mt-sm-4 col col-sm-auto">
|
||||
<gl-button
|
||||
v-gl-tooltip
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
export const MAX_MILESTONES_TO_DISPLAY = 5;
|
||||
|
||||
export const BACK_URL_PARAM = 'back_url';
|
||||
|
||||
export const ASSET_LINK_TYPE = Object.freeze({
|
||||
OTHER: 'other',
|
||||
IMAGE: 'image',
|
||||
PACKAGE: 'package',
|
||||
RUNBOOK: 'runbook',
|
||||
});
|
||||
|
||||
export const DEFAULT_ASSET_LINK_TYPE = ASSET_LINK_TYPE.OTHER;
|
||||
|
|
|
@ -3,7 +3,10 @@ import api from '~/api';
|
|||
import createFlash from '~/flash';
|
||||
import { s__ } from '~/locale';
|
||||
import { redirectTo } from '~/lib/utils/url_utility';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import {
|
||||
convertObjectPropsToCamelCase,
|
||||
convertObjectPropsToSnakeCase,
|
||||
} from '~/lib/utils/common_utils';
|
||||
|
||||
export const requestRelease = ({ commit }) => commit(types.REQUEST_RELEASE);
|
||||
export const receiveReleaseSuccess = ({ commit }, data) =>
|
||||
|
@ -91,7 +94,11 @@ export const updateRelease = ({ dispatch, state, getters }) => {
|
|||
// Create a new link for each link in the form
|
||||
return Promise.all(
|
||||
getters.releaseLinksToCreate.map(l =>
|
||||
api.createReleaseLink(state.projectId, release.tagName, l),
|
||||
api.createReleaseLink(
|
||||
state.projectId,
|
||||
release.tagName,
|
||||
convertObjectPropsToSnakeCase(l, { deep: true }),
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
|
@ -118,6 +125,10 @@ export const updateAssetLinkName = ({ commit }, { linkIdToUpdate, newName }) =>
|
|||
commit(types.UPDATE_ASSET_LINK_NAME, { linkIdToUpdate, newName });
|
||||
};
|
||||
|
||||
export const updateAssetLinkType = ({ commit }, { linkIdToUpdate, newType }) => {
|
||||
commit(types.UPDATE_ASSET_LINK_TYPE, { linkIdToUpdate, newType });
|
||||
};
|
||||
|
||||
export const removeAssetLink = ({ commit }, linkIdToRemove) => {
|
||||
commit(types.REMOVE_ASSET_LINK, linkIdToRemove);
|
||||
};
|
||||
|
|
|
@ -13,4 +13,5 @@ export const RECEIVE_UPDATE_RELEASE_ERROR = 'RECEIVE_UPDATE_RELEASE_ERROR';
|
|||
export const ADD_EMPTY_ASSET_LINK = 'ADD_EMPTY_ASSET_LINK';
|
||||
export const UPDATE_ASSET_LINK_URL = 'UPDATE_ASSET_LINK_URL';
|
||||
export const UPDATE_ASSET_LINK_NAME = 'UPDATE_ASSET_LINK_NAME';
|
||||
export const UPDATE_ASSET_LINK_TYPE = 'UPDATE_ASSET_LINK_TYPE';
|
||||
export const REMOVE_ASSET_LINK = 'REMOVE_ASSET_LINK';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as types from './mutation_types';
|
||||
import { uniqueId, cloneDeep } from 'lodash';
|
||||
import { DEFAULT_ASSET_LINK_TYPE } from '../../../constants';
|
||||
|
||||
const findReleaseLink = (release, id) => {
|
||||
return release.assets.links.find(l => l.id === id);
|
||||
|
@ -49,6 +50,7 @@ export default {
|
|||
id: uniqueId('new-link-'),
|
||||
url: '',
|
||||
name: '',
|
||||
linkType: DEFAULT_ASSET_LINK_TYPE,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -62,6 +64,11 @@ export default {
|
|||
linkToUpdate.name = newName;
|
||||
},
|
||||
|
||||
[types.UPDATE_ASSET_LINK_TYPE](state, { linkIdToUpdate, newType }) {
|
||||
const linkToUpdate = findReleaseLink(state.release, linkIdToUpdate);
|
||||
linkToUpdate.linkType = newType;
|
||||
},
|
||||
|
||||
[types.REMOVE_ASSET_LINK](state, linkIdToRemove) {
|
||||
state.release.assets.links = state.release.assets.links.filter(l => l.id !== linkIdToRemove);
|
||||
},
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
.layout-page.design-detail-layout {
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.design-detail {
|
||||
background-color: rgba($black, 0.9);
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ class Projects::ReleasesController < Projects::ApplicationController
|
|||
push_frontend_feature_flag(:release_evidence_collection, project, default_enabled: true)
|
||||
push_frontend_feature_flag(:release_show_page, project, default_enabled: true)
|
||||
push_frontend_feature_flag(:release_asset_link_editing, project, default_enabled: true)
|
||||
push_frontend_feature_flag(:release_asset_link_type, project, default_enabled: false)
|
||||
end
|
||||
before_action :authorize_update_release!, only: %i[edit update]
|
||||
|
||||
|
|
5
changelogs/unreleased/218025-xff-is-a-400-error.yml
Normal file
5
changelogs/unreleased/218025-xff-is-a-400-error.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Convert IP spoofing errors into client errors
|
||||
merge_request: 33280
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove ability to scroll Issue while in Design View
|
||||
merge_request: 29881
|
||||
author:
|
||||
type: fixed
|
|
@ -25,6 +25,7 @@ module Gitlab
|
|||
require_dependency Rails.root.join('lib/gitlab/middleware/read_only')
|
||||
require_dependency Rails.root.join('lib/gitlab/middleware/basic_health_check')
|
||||
require_dependency Rails.root.join('lib/gitlab/middleware/same_site_cookies')
|
||||
require_dependency Rails.root.join('lib/gitlab/middleware/handle_ip_spoof_attack_error')
|
||||
require_dependency Rails.root.join('lib/gitlab/runtime')
|
||||
|
||||
# Settings in config/environments/* take precedence over those specified here.
|
||||
|
@ -235,6 +236,8 @@ module Gitlab
|
|||
|
||||
config.middleware.insert_before ActionDispatch::Cookies, ::Gitlab::Middleware::SameSiteCookies
|
||||
|
||||
config.middleware.insert_before ActionDispatch::RemoteIp, ::Gitlab::Middleware::HandleIpSpoofAttackError
|
||||
|
||||
# Allow access to GitLab API from other domains
|
||||
config.middleware.insert_before Warden::Manager, Rack::Cors do
|
||||
headers_to_expose = %w[Link X-Total X-Total-Pages X-Per-Page X-Page X-Next-Page X-Prev-Page X-Gitlab-Blob-Id X-Gitlab-Commit-Id X-Gitlab-Content-Sha256 X-Gitlab-Encoding X-Gitlab-File-Name X-Gitlab-File-Path X-Gitlab-Last-Commit-Id X-Gitlab-Ref X-Gitlab-Size]
|
||||
|
|
|
@ -533,7 +533,9 @@ If you wish to use a different configuration file, use the `-c` flag:
|
|||
markdownlint -c <config-file-name> 'doc/**/*.md'
|
||||
```
|
||||
|
||||
markdownlint can also be run from within text editors using [plugins/extensions](https://github.com/DavidAnson/markdownlint#related),
|
||||
##### Run markdownlint in an editor
|
||||
|
||||
markdownlint can also be run as a linter within text editors using [plugins/extensions](https://github.com/DavidAnson/markdownlint#related),
|
||||
such as:
|
||||
|
||||
- [Sublime Text](https://packagecontrol.io/packages/SublimeLinter-contrib-markdownlint)
|
||||
|
@ -581,12 +583,17 @@ and from GitLab's root directory (where `.vale.ini` is located), run:
|
|||
vale --glob='*.{md}' doc
|
||||
```
|
||||
|
||||
You can also
|
||||
[configure the text editor of your choice](https://errata-ai.github.io/vale/#local-use-by-a-single-writer)
|
||||
to display the results.
|
||||
|
||||
Vale's error-level test results [are displayed](#testing) in CI pipelines.
|
||||
|
||||
##### Run Vale in an editor
|
||||
|
||||
You can run Vale as a linter within your text editor of choice, with:
|
||||
|
||||
- The Sublime Text [`SublimeLinter-contrib-vale` plugin](https://packagecontrol.io/packages/SublimeLinter-contrib-vale)
|
||||
- The Visual Studio Code [`testthedocs.vale` extension](https://marketplace.visualstudio.com/items?itemName=testthedocs.vale)
|
||||
|
||||
We don't use [Vale Server](https://errata-ai.github.io/vale/#using-vale-with-a-text-editor-or-another-third-party-application).
|
||||
|
||||
##### Disable a Vale test
|
||||
|
||||
You can disable a specific Vale linting rule or all Vale linting rules for any portion of a document:
|
||||
|
|
|
@ -612,6 +612,7 @@ The following is example content of the Usage Ping payload.
|
|||
"gitaly": {
|
||||
"version": "12.10.0-rc1-93-g40980d40",
|
||||
"servers": 56,
|
||||
"clusters": 14,
|
||||
"filesystems": [
|
||||
"EXT_2_3_4"
|
||||
]
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
module Gitaly
|
||||
class Server
|
||||
SHA_VERSION_REGEX = /\A\d+\.\d+\.\d+-\d+-g([a-f0-9]{8})\z/.freeze
|
||||
DEFAULT_REPLICATION_FACTOR = 1
|
||||
|
||||
class << self
|
||||
def all
|
||||
|
@ -16,6 +17,10 @@ module Gitaly
|
|||
def filesystems
|
||||
all.map(&:filesystem_type).compact.uniq
|
||||
end
|
||||
|
||||
def gitaly_clusters
|
||||
all.count { |g| g.replication_factor > DEFAULT_REPLICATION_FACTOR }
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :storage
|
||||
|
@ -73,6 +78,10 @@ module Gitaly
|
|||
"Error getting the address: #{e.message}"
|
||||
end
|
||||
|
||||
def replication_factor
|
||||
storage_status&.replication_factor
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def storage_status
|
||||
|
|
33
lib/gitlab/middleware/handle_ip_spoof_attack_error.rb
Normal file
33
lib/gitlab/middleware/handle_ip_spoof_attack_error.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Middleware
|
||||
# ActionDispatch::RemoteIp tries to set the `request.ip` for controllers by
|
||||
# looking at the request IP and headers. It needs to see through any reverse
|
||||
# proxies to get the right answer, but there are some security issues with
|
||||
# that.
|
||||
#
|
||||
# Proxies can specify `Client-Ip` or `X-Forwarded-For`, and the security of
|
||||
# that is determined at the edge. If both headers are present, it's likely
|
||||
# that the edge is securing one, but ignoring the other. Rails blocks this,
|
||||
# which is correct, because we don't know which header is the safe one - but
|
||||
# we want the block to be a 400, rather than 500, error.
|
||||
#
|
||||
# This middleware needs to go before ActionDispatch::RemoteIp in the chain.
|
||||
class HandleIpSpoofAttackError
|
||||
attr_reader :app
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
app.call(env)
|
||||
rescue ActionDispatch::RemoteIp::IpSpoofAttackError => err
|
||||
Gitlab::ErrorTracking.track_exception(err)
|
||||
|
||||
[400, { 'Content-Type' => 'text/plain' }, ['Bad Request']]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -217,6 +217,7 @@ module Gitlab
|
|||
gitaly: {
|
||||
version: alt_usage_data { Gitaly::Server.all.first.server_version },
|
||||
servers: alt_usage_data { Gitaly::Server.count },
|
||||
clusters: alt_usage_data { Gitaly::Server.gitaly_clusters },
|
||||
filesystems: alt_usage_data(fallback: ["-1"]) { Gitaly::Server.filesystems }
|
||||
},
|
||||
gitlab_pages: {
|
||||
|
|
|
@ -17957,6 +17957,18 @@ msgstr ""
|
|||
msgid "Release title"
|
||||
msgstr ""
|
||||
|
||||
msgid "ReleaseAssetLinkType|Image"
|
||||
msgstr ""
|
||||
|
||||
msgid "ReleaseAssetLinkType|Other"
|
||||
msgstr ""
|
||||
|
||||
msgid "ReleaseAssetLinkType|Package"
|
||||
msgstr ""
|
||||
|
||||
msgid "ReleaseAssetLinkType|Runbook"
|
||||
msgstr ""
|
||||
|
||||
msgid "Releases"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -13,6 +13,14 @@ import FluentdOutputSettings from '~/clusters/components/fluentd_output_settings
|
|||
describe('Applications', () => {
|
||||
let vm;
|
||||
let Applications;
|
||||
const ApplicationRowStub = {
|
||||
name: 'application-row-stub',
|
||||
template: `
|
||||
<div>
|
||||
<slot name="description"></slot>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Applications = Vue.extend(applications);
|
||||
|
@ -202,7 +210,12 @@ describe('Applications', () => {
|
|||
|
||||
let wrapper;
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(Applications, { propsData });
|
||||
wrapper = shallowMount(Applications, {
|
||||
propsData,
|
||||
stubs: {
|
||||
ApplicationRow: ApplicationRowStub,
|
||||
},
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
@ -436,7 +449,10 @@ describe('Applications', () => {
|
|||
let knativeDomainEditor;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(Applications, { propsData });
|
||||
wrapper = shallowMount(Applications, {
|
||||
propsData,
|
||||
stubs: { ApplicationRow: ApplicationRowStub },
|
||||
});
|
||||
jest.spyOn(eventHub, '$emit');
|
||||
|
||||
knativeDomainEditor = wrapper.find(KnativeDomainEditor);
|
||||
|
@ -504,7 +520,10 @@ describe('Applications', () => {
|
|||
|
||||
let wrapper;
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(Applications, { propsData });
|
||||
wrapper = shallowMount(Applications, {
|
||||
propsData,
|
||||
stubs: { ApplicationRow: ApplicationRowStub },
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
@ -563,7 +582,10 @@ describe('Applications', () => {
|
|||
|
||||
let wrapper;
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(Applications, { propsData });
|
||||
wrapper = shallowMount(Applications, {
|
||||
propsData,
|
||||
stubs: { ApplicationRow: ApplicationRowStub },
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import VueRouter from 'vue-router';
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import { ApolloMutation } from 'vue-apollo';
|
||||
import createFlash from '~/flash';
|
||||
|
@ -17,6 +18,9 @@ import {
|
|||
DESIGN_VERSION_NOT_EXIST_ERROR,
|
||||
} from '~/design_management/utils/error_messages';
|
||||
import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants';
|
||||
import createRouter from '~/design_management/router';
|
||||
import * as utils from '~/design_management/utils/design_management_utils';
|
||||
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants';
|
||||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('mousetrap', () => ({
|
||||
|
@ -24,8 +28,13 @@ jest.mock('mousetrap', () => ({
|
|||
unbind: jest.fn(),
|
||||
}));
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueRouter);
|
||||
|
||||
describe('Design management design index page', () => {
|
||||
let wrapper;
|
||||
let router;
|
||||
|
||||
const newComment = 'new comment';
|
||||
const annotationCoordinates = {
|
||||
x: 10,
|
||||
|
@ -62,14 +71,13 @@ describe('Design management design index page', () => {
|
|||
};
|
||||
|
||||
const mutate = jest.fn().mockResolvedValue();
|
||||
const routerPush = jest.fn();
|
||||
|
||||
const findDiscussions = () => wrapper.findAll(DesignDiscussion);
|
||||
const findDiscussionForm = () => wrapper.find(DesignReplyForm);
|
||||
const findParticipants = () => wrapper.find(Participants);
|
||||
const findDiscussionsWrapper = () => wrapper.find('.image-notes');
|
||||
|
||||
function createComponent(loading = false, data = {}, { routeQuery = {} } = {}) {
|
||||
function createComponent(loading = false, data = {}) {
|
||||
const $apollo = {
|
||||
queries: {
|
||||
design: {
|
||||
|
@ -79,17 +87,11 @@ describe('Design management design index page', () => {
|
|||
mutate,
|
||||
};
|
||||
|
||||
const $router = {
|
||||
push: routerPush,
|
||||
};
|
||||
|
||||
const $route = {
|
||||
query: routeQuery,
|
||||
};
|
||||
router = createRouter();
|
||||
|
||||
wrapper = shallowMount(DesignIndex, {
|
||||
propsData: { id: '1' },
|
||||
mocks: { $apollo, $router, $route },
|
||||
mocks: { $apollo },
|
||||
stubs: {
|
||||
ApolloMutation,
|
||||
DesignDiscussion,
|
||||
|
@ -104,6 +106,8 @@ describe('Design management design index page', () => {
|
|||
...data,
|
||||
};
|
||||
},
|
||||
localVue,
|
||||
router,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -111,6 +115,23 @@ describe('Design management design index page', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('when navigating', () => {
|
||||
it('applies fullscreen layout', () => {
|
||||
const mockEl = {
|
||||
classList: {
|
||||
add: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
},
|
||||
};
|
||||
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockEl);
|
||||
createComponent(true);
|
||||
|
||||
wrapper.vm.$router.push('/designs/test');
|
||||
expect(mockEl.classList.add).toHaveBeenCalledTimes(1);
|
||||
expect(mockEl.classList.add).toHaveBeenCalledWith(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets loading state', () => {
|
||||
createComponent(true);
|
||||
|
||||
|
@ -269,31 +290,35 @@ describe('Design management design index page', () => {
|
|||
describe('with no designs', () => {
|
||||
it('redirects to /designs', () => {
|
||||
createComponent(true);
|
||||
router.push = jest.fn();
|
||||
|
||||
wrapper.vm.onDesignQueryResult({ data: mockResponseNoDesigns, loading: false });
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(createFlash).toHaveBeenCalledTimes(1);
|
||||
expect(createFlash).toHaveBeenCalledWith(DESIGN_NOT_FOUND_ERROR);
|
||||
expect(routerPush).toHaveBeenCalledTimes(1);
|
||||
expect(routerPush).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME });
|
||||
expect(router.push).toHaveBeenCalledTimes(1);
|
||||
expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when no design exists for given version', () => {
|
||||
it('redirects to /designs', () => {
|
||||
// attempt to query for a version of the design that doesn't exist
|
||||
createComponent(true, {}, { routeQuery: { version: '999' } });
|
||||
createComponent(true);
|
||||
wrapper.setData({
|
||||
allVersions: mockAllVersions,
|
||||
});
|
||||
|
||||
// attempt to query for a version of the design that doesn't exist
|
||||
router.push({ query: { version: '999' } });
|
||||
router.push = jest.fn();
|
||||
|
||||
wrapper.vm.onDesignQueryResult({ data: mockResponseWithDesigns, loading: false });
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(createFlash).toHaveBeenCalledTimes(1);
|
||||
expect(createFlash).toHaveBeenCalledWith(DESIGN_VERSION_NOT_EXIST_ERROR);
|
||||
expect(routerPush).toHaveBeenCalledTimes(1);
|
||||
expect(routerPush).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME });
|
||||
expect(router.push).toHaveBeenCalledTimes(1);
|
||||
expect(router.push).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,6 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
|
|||
import { ApolloMutation } from 'vue-apollo';
|
||||
import VueRouter from 'vue-router';
|
||||
import { GlEmptyState } from '@gitlab/ui';
|
||||
|
||||
import Index from '~/design_management/pages/index.vue';
|
||||
import uploadDesignQuery from '~/design_management/graphql/mutations/uploadDesign.mutation.graphql';
|
||||
import DesignDestroyer from '~/design_management/components/design_destroyer.vue';
|
||||
|
@ -14,20 +13,21 @@ import {
|
|||
EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE,
|
||||
} from '~/design_management/utils/error_messages';
|
||||
import createFlash from '~/flash';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueRouter);
|
||||
const router = new VueRouter({
|
||||
routes: [
|
||||
{
|
||||
name: DESIGNS_ROUTE_NAME,
|
||||
path: '/designs',
|
||||
component: Index,
|
||||
},
|
||||
],
|
||||
});
|
||||
import createRouter from '~/design_management/router';
|
||||
import * as utils from '~/design_management/utils/design_management_utils';
|
||||
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants';
|
||||
|
||||
jest.mock('~/flash.js');
|
||||
const mockPageEl = {
|
||||
classList: {
|
||||
remove: jest.fn(),
|
||||
},
|
||||
};
|
||||
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageEl);
|
||||
|
||||
const localVue = createLocalVue();
|
||||
const router = createRouter();
|
||||
localVue.use(VueRouter);
|
||||
|
||||
const mockDesigns = [
|
||||
{
|
||||
|
@ -530,4 +530,14 @@ describe('Design management index page', () => {
|
|||
expect(wrapper.vm.onUploadDesign).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when navigating', () => {
|
||||
it('ensures fullscreen layout is not applied', () => {
|
||||
createComponent(true);
|
||||
|
||||
wrapper.vm.$router.push('/designs');
|
||||
expect(mockPageEl.classList.remove).toHaveBeenCalledTimes(1);
|
||||
expect(mockPageEl.classList.remove).toHaveBeenCalledWith(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@ import { mount, createLocalVue } from '@vue/test-utils';
|
|||
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
|
||||
import { release as originalRelease } from '../mock_data';
|
||||
import * as commonUtils from '~/lib/utils/common_utils';
|
||||
import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
@ -24,6 +25,7 @@ describe('Release edit component', () => {
|
|||
addEmptyAssetLink: jest.fn(),
|
||||
updateAssetLinkUrl: jest.fn(),
|
||||
updateAssetLinkName: jest.fn(),
|
||||
updateAssetLinkType: jest.fn(),
|
||||
removeAssetLink: jest.fn().mockImplementation((_context, linkId) => {
|
||||
state.release.assets.links = state.release.assets.links.filter(l => l.id !== linkId);
|
||||
}),
|
||||
|
@ -51,6 +53,11 @@ describe('Release edit component', () => {
|
|||
wrapper = mount(AssetLinksForm, {
|
||||
localVue,
|
||||
store,
|
||||
provide: {
|
||||
glFeatures: {
|
||||
releaseAssetLinkType: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -103,7 +110,7 @@ describe('Release edit component', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('calls the "updateAssetLinName" store method when text is entered into the "Link title" input field', () => {
|
||||
it('calls the "updateAssetLinkName" store method when text is entered into the "Link title" input field', () => {
|
||||
const linkIdToUpdate = release.assets.links[0].id;
|
||||
const newName = 'updated name';
|
||||
|
||||
|
@ -121,6 +128,31 @@ describe('Release edit component', () => {
|
|||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('calls the "updateAssetLinkType" store method when an option is selected from the "Type" dropdown', () => {
|
||||
const linkIdToUpdate = release.assets.links[0].id;
|
||||
const newType = ASSET_LINK_TYPE.RUNBOOK;
|
||||
|
||||
expect(actions.updateAssetLinkType).not.toHaveBeenCalled();
|
||||
|
||||
wrapper.find({ ref: 'typeSelect' }).vm.$emit('change', newType);
|
||||
|
||||
expect(actions.updateAssetLinkType).toHaveBeenCalledTimes(1);
|
||||
expect(actions.updateAssetLinkType).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
{
|
||||
linkIdToUpdate,
|
||||
newType,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('selects the default asset type if no type was provided by the backend', () => {
|
||||
const selected = wrapper.find({ ref: 'typeSelect' }).element.value;
|
||||
|
||||
expect(selected).toBe(DEFAULT_ASSET_LINK_TYPE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validation', () => {
|
||||
|
|
|
@ -10,6 +10,7 @@ import createFlash from '~/flash';
|
|||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import { redirectTo } from '~/lib/utils/url_utility';
|
||||
import api from '~/api';
|
||||
import { ASSET_LINK_TYPE } from '~/releases/constants';
|
||||
|
||||
jest.mock('~/flash', () => jest.fn());
|
||||
|
||||
|
@ -130,6 +131,54 @@ describe('Release detail actions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('updateAssetLinkUrl', () => {
|
||||
it(`commits ${types.UPDATE_ASSET_LINK_URL} with the updated link URL`, () => {
|
||||
const params = {
|
||||
linkIdToUpdate: 2,
|
||||
newUrl: 'https://example.com/updated',
|
||||
};
|
||||
|
||||
return testAction(actions.updateAssetLinkUrl, params, state, [
|
||||
{ type: types.UPDATE_ASSET_LINK_URL, payload: params },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateAssetLinkName', () => {
|
||||
it(`commits ${types.UPDATE_ASSET_LINK_NAME} with the updated link name`, () => {
|
||||
const params = {
|
||||
linkIdToUpdate: 2,
|
||||
newName: 'Updated link name',
|
||||
};
|
||||
|
||||
return testAction(actions.updateAssetLinkName, params, state, [
|
||||
{ type: types.UPDATE_ASSET_LINK_NAME, payload: params },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateAssetLinkType', () => {
|
||||
it(`commits ${types.UPDATE_ASSET_LINK_TYPE} with the updated link type`, () => {
|
||||
const params = {
|
||||
linkIdToUpdate: 2,
|
||||
newType: ASSET_LINK_TYPE.RUNBOOK,
|
||||
};
|
||||
|
||||
return testAction(actions.updateAssetLinkType, params, state, [
|
||||
{ type: types.UPDATE_ASSET_LINK_TYPE, payload: params },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeAssetLink', () => {
|
||||
it(`commits ${types.REMOVE_ASSET_LINK} with the ID of the asset link to remove`, () => {
|
||||
const idToRemove = 2;
|
||||
return testAction(actions.removeAssetLink, idToRemove, state, [
|
||||
{ type: types.REMOVE_ASSET_LINK, payload: idToRemove },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateReleaseMilestones', () => {
|
||||
it(`commits ${types.UPDATE_RELEASE_MILESTONES} with the updated release milestones`, () => {
|
||||
const newReleaseMilestones = ['v0.0', 'v0.1'];
|
||||
|
|
|
@ -3,6 +3,7 @@ import mutations from '~/releases/stores/modules/detail/mutations';
|
|||
import * as types from '~/releases/stores/modules/detail/mutation_types';
|
||||
import { release as originalRelease } from '../../../mock_data';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
|
||||
|
||||
describe('Release detail mutations', () => {
|
||||
let state;
|
||||
|
@ -24,7 +25,7 @@ describe('Release detail mutations', () => {
|
|||
it('set state.isFetchingRelease to true', () => {
|
||||
mutations[types.REQUEST_RELEASE](state);
|
||||
|
||||
expect(state.isFetchingRelease).toEqual(true);
|
||||
expect(state.isFetchingRelease).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -32,9 +33,9 @@ describe('Release detail mutations', () => {
|
|||
it('handles a successful response from the server', () => {
|
||||
mutations[types.RECEIVE_RELEASE_SUCCESS](state, release);
|
||||
|
||||
expect(state.fetchError).toEqual(undefined);
|
||||
expect(state.fetchError).toBeUndefined();
|
||||
|
||||
expect(state.isFetchingRelease).toEqual(false);
|
||||
expect(state.isFetchingRelease).toBe(false);
|
||||
|
||||
expect(state.release).toEqual(release);
|
||||
|
||||
|
@ -47,7 +48,7 @@ describe('Release detail mutations', () => {
|
|||
const error = { message: 'An error occurred!' };
|
||||
mutations[types.RECEIVE_RELEASE_ERROR](state, error);
|
||||
|
||||
expect(state.isFetchingRelease).toEqual(false);
|
||||
expect(state.isFetchingRelease).toBe(false);
|
||||
|
||||
expect(state.release).toBeUndefined();
|
||||
|
||||
|
@ -61,7 +62,7 @@ describe('Release detail mutations', () => {
|
|||
const newTitle = 'The new release title';
|
||||
mutations[types.UPDATE_RELEASE_TITLE](state, newTitle);
|
||||
|
||||
expect(state.release.name).toEqual(newTitle);
|
||||
expect(state.release.name).toBe(newTitle);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -71,7 +72,7 @@ describe('Release detail mutations', () => {
|
|||
const newNotes = 'The new release notes';
|
||||
mutations[types.UPDATE_RELEASE_NOTES](state, newNotes);
|
||||
|
||||
expect(state.release.description).toEqual(newNotes);
|
||||
expect(state.release.description).toBe(newNotes);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -79,7 +80,7 @@ describe('Release detail mutations', () => {
|
|||
it('set state.isUpdatingRelease to true', () => {
|
||||
mutations[types.REQUEST_UPDATE_RELEASE](state);
|
||||
|
||||
expect(state.isUpdatingRelease).toEqual(true);
|
||||
expect(state.isUpdatingRelease).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -87,9 +88,9 @@ describe('Release detail mutations', () => {
|
|||
it('handles a successful response from the server', () => {
|
||||
mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state, release);
|
||||
|
||||
expect(state.updateError).toEqual(undefined);
|
||||
expect(state.updateError).toBeUndefined();
|
||||
|
||||
expect(state.isUpdatingRelease).toEqual(false);
|
||||
expect(state.isUpdatingRelease).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -98,7 +99,7 @@ describe('Release detail mutations', () => {
|
|||
const error = { message: 'An error occurred!' };
|
||||
mutations[types.RECEIVE_UPDATE_RELEASE_ERROR](state, error);
|
||||
|
||||
expect(state.isUpdatingRelease).toEqual(false);
|
||||
expect(state.isUpdatingRelease).toBe(false);
|
||||
|
||||
expect(state.updateError).toEqual(error);
|
||||
});
|
||||
|
@ -118,6 +119,7 @@ describe('Release detail mutations', () => {
|
|||
id: expect.stringMatching(/^new-link-/),
|
||||
url: '',
|
||||
name: '',
|
||||
linkType: DEFAULT_ASSET_LINK_TYPE,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -134,7 +136,7 @@ describe('Release detail mutations', () => {
|
|||
newUrl,
|
||||
});
|
||||
|
||||
expect(state.release.assets.links[0].url).toEqual(newUrl);
|
||||
expect(state.release.assets.links[0].url).toBe(newUrl);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -149,7 +151,22 @@ describe('Release detail mutations', () => {
|
|||
newName,
|
||||
});
|
||||
|
||||
expect(state.release.assets.links[0].name).toEqual(newName);
|
||||
expect(state.release.assets.links[0].name).toBe(newName);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`${types.UPDATE_ASSET_LINK_TYPE}`, () => {
|
||||
it('updates an asset link with a new type', () => {
|
||||
state.release = release;
|
||||
|
||||
const newType = ASSET_LINK_TYPE.RUNBOOK;
|
||||
|
||||
mutations[types.UPDATE_ASSET_LINK_TYPE](state, {
|
||||
linkIdToUpdate: state.release.assets.links[0].id,
|
||||
newType,
|
||||
});
|
||||
|
||||
expect(state.release.assets.links[0].linkType).toBe(newType);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ describe Gitaly::Server do
|
|||
it { is_expected.to respond_to(:git_binary_version) }
|
||||
it { is_expected.to respond_to(:up_to_date?) }
|
||||
it { is_expected.to respond_to(:address) }
|
||||
it { is_expected.to respond_to(:replication_factor) }
|
||||
|
||||
describe 'readable?' do
|
||||
context 'when the storage is readable' do
|
||||
|
@ -134,4 +135,22 @@ describe Gitaly::Server do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'replication_factor' do
|
||||
context 'when examining for a given server' do
|
||||
let(:storage_status) { double('storage_status', storage_name: 'default') }
|
||||
|
||||
before do
|
||||
response = double('response', storage_statuses: [storage_status])
|
||||
allow_next_instance_of(Gitlab::GitalyClient::ServerService) do |instance|
|
||||
allow(instance).to receive(:info).and_return(response)
|
||||
end
|
||||
end
|
||||
|
||||
it do
|
||||
allow(storage_status).to receive(:replication_factor).and_return(2)
|
||||
expect(server.replication_factor).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Middleware::HandleIpSpoofAttackError do
|
||||
let(:spoof_error) { ActionDispatch::RemoteIp::IpSpoofAttackError.new('sensitive') }
|
||||
let(:standard_error) { StandardError.new('error') }
|
||||
let(:app) { -> (env) { env.is_a?(Exception) ? raise(env) : env } }
|
||||
|
||||
subject(:middleware) { described_class.new(app) }
|
||||
|
||||
it 'passes through the response from a valid upstream' do
|
||||
expect(middleware.call(:response)).to eq(:response)
|
||||
end
|
||||
|
||||
it 'translates an ActionDispatch::IpSpoofAttackError to a 400 response' do
|
||||
expect(middleware.call(spoof_error))
|
||||
.to eq([400, { 'Content-Type' => 'text/plain' }, ['Bad Request']])
|
||||
end
|
||||
|
||||
it 'passes through the exception raised by an invalid upstream' do
|
||||
expect { middleware.call(standard_error) }.to raise_error(standard_error)
|
||||
end
|
||||
end
|
|
@ -255,6 +255,7 @@ describe Gitlab::UsageData, :aggregate_failures do
|
|||
expect(subject[:database][:version]).to eq(Gitlab::Database.version)
|
||||
expect(subject[:gitaly][:version]).to be_present
|
||||
expect(subject[:gitaly][:servers]).to be >= 1
|
||||
expect(subject[:gitaly][:clusters]).to be >= 0
|
||||
expect(subject[:gitaly][:filesystems]).to be_an(Array)
|
||||
expect(subject[:gitaly][:filesystems].first).to be_a(String)
|
||||
end
|
||||
|
|
12
spec/requests/user_spoofs_ip_spec.rb
Normal file
12
spec/requests/user_spoofs_ip_spec.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'User spoofs their IP' do
|
||||
it 'raises a 400 error' do
|
||||
get '/nonexistent', headers: { 'Client-Ip' => '1.2.3.4', 'X-Forwarded-For' => '5.6.7.8' }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(response.body).to eq('Bad Request')
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue