Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-06-22 18:08:47 +00:00
parent e829ca213b
commit 7105e0c53e
54 changed files with 733 additions and 207 deletions

View File

@ -174,7 +174,7 @@ export default function renderMermaid($els) {
if (!$els.length) return;
const visibleMermaids = $els.filter(function filter() {
return $(this).closest('details').length === 0;
return $(this).closest('details').length === 0 && $(this).is(':visible');
});
renderMermaids(visibleMermaids);

View File

@ -226,6 +226,8 @@ export default class MergeRequestTabs {
this.resetViewContainer();
this.destroyPipelinesView();
}
$('.detail-page-description').renderGFM();
} else if (action === this.currentAction) {
// ContentTop is used to handle anything at the top of the page before the main content
const mainContentContainer = document.querySelector('.content-wrapper');

View File

@ -17,7 +17,9 @@ const defaultTooltipFormat = defaultFormat;
const defaultTooltipPrecision = 3;
// Give enough space for y-axis with units and name.
const chartGridLeft = 75;
const chartGridLeft = 63; // larger gap than gitlab-ui's default to fit formatted numbers
const chartGridRight = 10; // half of the scroll-handle icon for data zoom
const yAxisNameGap = chartGridLeft - 12; // offset the axis label line-height
// Axis options
@ -62,7 +64,7 @@ export const getYAxisOptions = ({
precision = defaultYAxisPrecision,
} = {}) => {
return {
nameGap: 63, // larger gap than gitlab-ui's default to fit with formatted numbers
nameGap: yAxisNameGap,
scale: true,
boundaryGap: yAxisBoundaryGap,
@ -90,7 +92,10 @@ export const getTimeAxisOptions = ({ timezone = timezones.LOCAL } = {}) => ({
/**
* Grid with enough room to display chart.
*/
export const getChartGrid = ({ left = chartGridLeft } = {}) => ({ left });
export const getChartGrid = ({ left = chartGridLeft, right = chartGridRight } = {}) => ({
left,
right,
});
// Tooltip options

View File

@ -132,7 +132,8 @@ export default {
return this.graphData?.title || '';
},
graphDataHasResult() {
return this.graphData?.metrics?.[0]?.result?.length > 0;
const metrics = this.graphData?.metrics || [];
return metrics.some(({ result }) => result?.length > 0);
},
graphDataIsLoading() {
const metrics = this.graphData?.metrics || [];
@ -207,7 +208,17 @@ export default {
return MonitorTimeSeriesChart;
},
isContextualMenuShown() {
return Boolean(this.graphDataHasResult && !this.basicChartComponent);
if (!this.graphDataHasResult) {
return false;
}
// Only a few charts have a contextual menu, support
// for more chart types planned at:
// https://gitlab.com/groups/gitlab-org/-/epics/3573
return (
this.isPanelType(panelTypes.AREA_CHART) ||
this.isPanelType(panelTypes.LINE_CHART) ||
this.isPanelType(panelTypes.SINGLE_STAT)
);
},
editCustomMetricLink() {
if (this.graphData.metrics.length > 1) {
@ -223,7 +234,10 @@ export default {
return metrics.some(({ metricId }) => this.metricsSavedToDb.includes(metricId));
},
alertWidgetAvailable() {
const supportsAlerts =
this.isPanelType(panelTypes.AREA_CHART) || this.isPanelType(panelTypes.LINE_CHART);
return (
supportsAlerts &&
this.prometheusAlertsAvailable &&
this.alertsEndpoint &&
this.graphData &&
@ -284,7 +298,7 @@ export default {
</script>
<template>
<div v-gl-resize-observer="onResize" class="prometheus-graph">
<div class="d-flex align-items-center mr-3">
<div class="d-flex align-items-center">
<slot name="topLeft"></slot>
<h5
ref="graphTitle"
@ -375,7 +389,7 @@ export default {
{{ __('Alerts') }}
</gl-dropdown-item>
<template v-if="graphData.links.length">
<template v-if="graphData.links && graphData.links.length">
<gl-dropdown-divider />
<gl-dropdown-item
v-for="(link, index) in graphData.links"

View File

@ -0,0 +1,56 @@
<script>
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
export default {
name: 'DeleteButton',
components: {
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
title: {
type: String,
required: true,
},
tooltipTitle: {
type: String,
required: true,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
tooltipDisabled: {
type: Boolean,
default: false,
required: false,
},
},
computed: {
tooltipConfiguration() {
return {
disabled: this.tooltipDisabled,
title: this.tooltipTitle,
};
},
},
};
</script>
<template>
<div v-gl-tooltip="tooltipConfiguration">
<gl-button
v-gl-tooltip
:disabled="disabled"
:title="title"
:aria-label="title"
category="secondary"
variant="danger"
icon="remove"
@click="$emit('delete')"
/>
</div>
</template>

View File

@ -0,0 +1,47 @@
<script>
export default {
name: 'ListItem',
props: {
index: {
type: Number,
default: 0,
required: false,
},
disabled: {
type: Boolean,
default: false,
required: false,
},
},
computed: {
optionalClasses() {
return {
'gl-border-t-solid gl-border-t-1': this.index === 0,
'disabled-content': this.disabled,
};
},
},
};
</script>
<template>
<div
:class="[
'gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-2 gl-px-1 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-py-4',
optionalClasses,
]"
>
<div class="gl-display-flex gl-flex-direction-column">
<div class="gl-display-flex gl-align-items-center">
<slot name="left-primary"></slot>
</div>
<div class="gl-font-sm gl-text-gray-500">
<slot name="left-secondary"></slot>
</div>
</div>
<div>
<slot name="right"></slot>
</div>
</div>
</template>

View File

@ -37,7 +37,7 @@ export default {
v-for="(listItem, index) in images"
:key="index"
:item="listItem"
:show-top-border="index === 0"
:index="index"
@delete="$emit('delete', $event)"
/>

View File

@ -1,7 +1,9 @@
<script>
import { GlTooltipDirective, GlButton, GlIcon, GlSprintf } from '@gitlab/ui';
import { GlTooltipDirective, GlIcon, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '../list_item.vue';
import DeleteButton from '../delete_button.vue';
import {
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
@ -14,9 +16,10 @@ export default {
name: 'ImageListrow',
components: {
ClipboardButton,
GlButton,
DeleteButton,
GlSprintf,
GlIcon,
ListItem,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -26,9 +29,9 @@ export default {
type: Object,
required: true,
},
showTopBorder: {
type: Boolean,
default: false,
index: {
type: Number,
default: 0,
required: false,
},
},
@ -62,75 +65,56 @@ export default {
</script>
<template>
<div
<list-item
v-gl-tooltip="{
placement: 'left',
disabled: !item.deleting,
title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
}"
:index="index"
:disabled="item.deleting"
>
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-2 gl-px-1 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-py-4 "
:class="{
'gl-border-t-solid gl-border-t-1': showTopBorder,
'disabled-content': item.deleting,
}"
>
<div class="gl-display-flex gl-flex-direction-column">
<div class="gl-display-flex gl-align-items-center">
<router-link
class="gl-text-black-normal gl-font-weight-bold"
data-testid="detailsLink"
:to="{ name: 'details', params: { id: encodedItem } }"
>
{{ item.path }}
</router-link>
<clipboard-button
v-if="item.location"
:disabled="item.deleting"
:text="item.location"
:title="item.location"
css-class="btn-default btn-transparent btn-clipboard gl-text-gray-500"
/>
<gl-icon
v-if="item.failedDelete"
v-gl-tooltip
:title="$options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE"
name="warning"
class="text-warning"
/>
</div>
<div class="gl-font-sm gl-text-gray-500">
<span class="gl-display-flex gl-align-items-center" data-testid="tagsCount">
<gl-icon name="tag" class="gl-mr-2" />
<gl-sprintf :message="tagsCountText">
<template #count>
{{ item.tags_count }}
</template>
</gl-sprintf>
</span>
</div>
</div>
<div
v-gl-tooltip="{
disabled: item.destroy_path,
title: $options.i18n.LIST_DELETE_BUTTON_DISABLED,
}"
class="d-none d-sm-block"
data-testid="deleteButtonWrapper"
<template #left-primary>
<router-link
class="gl-text-black-normal gl-font-weight-bold"
data-testid="detailsLink"
:to="{ name: 'details', params: { id: encodedItem } }"
>
<gl-button
v-gl-tooltip
data-testid="deleteImageButton"
:disabled="disabledDelete"
:title="$options.i18n.REMOVE_REPOSITORY_LABEL"
:aria-label="$options.i18n.REMOVE_REPOSITORY_LABEL"
category="secondary"
variant="danger"
icon="remove"
@click="$emit('delete', item)"
/>
</div>
</div>
</div>
{{ item.path }}
</router-link>
<clipboard-button
v-if="item.location"
:disabled="item.deleting"
:text="item.location"
:title="item.location"
css-class="btn-default btn-transparent btn-clipboard gl-text-gray-500"
/>
<gl-icon
v-if="item.failedDelete"
v-gl-tooltip="{ title: $options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE }"
name="warning"
class="text-warning"
/>
</template>
<template #left-secondary>
<span class="gl-display-flex gl-align-items-center" data-testid="tagsCount">
<gl-icon name="tag" class="gl-mr-2" />
<gl-sprintf :message="tagsCountText">
<template #count>
{{ item.tags_count }}
</template>
</gl-sprintf>
</span>
</template>
<template #right>
<delete-button
class="gl-display-none d-sm-block"
:title="$options.i18n.REMOVE_REPOSITORY_LABEL"
:disabled="disabledDelete"
:tooltip-disabled="Boolean(item.destroy_path)"
:tooltip-title="$options.i18n.LIST_DELETE_BUTTON_DISABLED"
@delete="$emit('delete', item)"
/>
</template>
</list-item>
</template>

View File

@ -314,8 +314,8 @@ input[type=color].form-control {
.toggle-sidebar-button {
.collapse-text,
.icon-angle-double-left,
.icon-angle-double-right {
.icon-chevron-double-lg-left,
.icon-chevron-double-lg-right {
color: $gl-text-color-secondary;
}
}

View File

@ -98,11 +98,11 @@
width: $contextual-sidebar-collapsed-width - 1px;
.collapse-text,
.icon-angle-double-left {
.icon-chevron-double-lg-left {
display: none;
}
.icon-angle-double-right {
.icon-chevron-double-lg-right {
display: block;
margin: 0;
}
@ -381,7 +381,7 @@
margin-right: 8px;
}
.icon-angle-double-right {
.icon-chevron-double-lg-right {
display: none;
}

View File

@ -31,7 +31,7 @@
= issuable_meta(@issue, @project, "Issue")
%a.btn.btn-default.float-right.d-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
= sprite_icon('chevron-double-lg-left')
.detail-page-header-actions.js-issuable-actions.js-issuable-buttons{ data: { "action": "close-reopen" } }
.clearfix.issue-btn-group.dropdown

View File

@ -20,7 +20,7 @@
= issuable_meta(@merge_request, @project, "Merge request")
%a.btn.btn-default.float-right.d-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
= sprite_icon('chevron-double-lg-left')
.detail-page-header-actions.js-issuable-actions
.clearfix.issue-btn-group.dropdown

View File

@ -3,7 +3,7 @@
.wiki-page-header.top-area.has-sidebar-toggle.py-3.flex-column.flex-lg-row
%button.btn.btn-default.d-block.d-sm-block.d-md-none.float-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
= sprite_icon('chevron-double-lg-left')
.git-access-header.w-100.d-flex.flex-column.justify-content-center
%span

View File

@ -1,6 +1,6 @@
%a.toggle-sidebar-button.js-toggle-sidebar.qa-toggle-sidebar.rspec-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
= sprite_icon('angle-double-left', css_class: 'icon-angle-double-left')
= sprite_icon('angle-double-right', css_class: 'icon-angle-double-right')
= sprite_icon('chevron-double-lg-left', css_class: 'icon-chevron-double-lg-left')
= sprite_icon('chevron-double-lg-right', css_class: 'icon-chevron-double-lg-right')
%span.collapse-text= _("Collapse sidebar")
= button_tag class: 'close-nav-button', type: 'button' do

View File

@ -33,4 +33,4 @@
= render 'shared/milestones/delete_button'
%button.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ type: 'button' }
= icon('angle-double-left')
= sprite_icon('chevron-double-lg-left')

View File

@ -2,7 +2,7 @@
.sidebar-container
.block.wiki-sidebar-header.gl-mb-3.w-100
%a.gutter-toggle.float-right.d-block.d-sm-block.d-md-none.js-sidebar-wiki-toggle{ href: "#" }
= icon('angle-double-right')
= sprite_icon('chevron-double-lg-right', size: 16, css_class: 'gl-icon')
- git_access_url = wiki_path(@wiki, action: :git_access)
= link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '', data: { qa_selector: 'clone_repository_link' } do

View File

@ -7,7 +7,7 @@
.wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
= sprite_icon('chevron-double-lg-left')
.nav-text
%h2.wiki-page-title

View File

@ -2,7 +2,7 @@
.wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
= sprite_icon('chevron-double-lg-left')
.nav-text
%h2.wiki-page-title

View File

@ -6,7 +6,7 @@
.wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
= sprite_icon('chevron-double-lg-left')
.nav-text.flex-fill
%h2.wiki-page-title{ data: { qa_selector: 'wiki_page_title' } }= @page.human_title

View File

@ -0,0 +1,5 @@
---
title: Optimize rolling 28 days snippets counter
merge_request: 34918
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Add contextual menu to single stat panels
merge_request: 34497
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Replace double angle icons with double chevron
merge_request: 34736
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Do not mask key comments for DeployKeys
merge_request: 35014
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Copy snippet route under - scope
merge_request: 35020
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Add RSpecs for Gitlab::Emoji module
merge_request: 34980
author: Rajendra Kadam
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Add package scope validation to Node.js template
merge_request: 34778
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Fixed mermaid not rendering when switching diff tabs
merge_request: 35023
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix Gitaly duration timings for other CommitService RPCs
merge_request: 34933
author:
type: other

View File

@ -262,6 +262,14 @@ Rails.application.routes.draw do
draw :user
draw :project
# Serve snippet routes under /-/snippets.
# To ensure an old unscoped routing is used for the UI we need to
# add prefix 'as' to the scope routing and place it below original routing.
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/210024
scope '-', as: :scoped do
draw :snippets
end
root to: "root#index"
get '*unmatched_route', to: 'application#route_not_found'

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddIndexOnIdAndCreatedAtToSnippets < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :snippets, [:id, :created_at]
end
def down
remove_concurrent_index :snippets, [:id, :created_at]
end
end

View File

@ -10960,6 +10960,8 @@ CREATE INDEX index_snippets_on_description_trigram ON public.snippets USING gin
CREATE INDEX index_snippets_on_file_name_trigram ON public.snippets USING gin (file_name public.gin_trgm_ops);
CREATE INDEX index_snippets_on_id_and_created_at ON public.snippets USING btree (id, created_at);
CREATE INDEX index_snippets_on_id_and_type ON public.snippets USING btree (id, type);
CREATE INDEX index_snippets_on_project_id_and_visibility_level ON public.snippets USING btree (project_id, visibility_level);
@ -14086,6 +14088,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200617001637
20200617001848
20200617002030
20200618105638
20200618134223
20200618134723
\.

View File

@ -1,3 +1,10 @@
---
stage: Defend
group: Container Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Web Application Firewall - ModSecurity
A web application firewall (or WAF) filters, monitors, and blocks HTTP traffic to

View File

@ -1,3 +1,9 @@
---
stage: Defend
group: Container Security
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Getting started with the Web Application Firewall
This is a step-by-step guide that will help you use GitLab's [Web Application Firewall](index.md) after

View File

@ -1,3 +1,10 @@
---
type: reference, howto
stage: Secure
group: Composition Analysis
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Dependency Scanning Analyzers **(ULTIMATE)**
Dependency Scanning relies on underlying third party tools that are wrapped into

View File

@ -1,5 +1,8 @@
---
type: reference, howto
stage: Secure
group: Threat Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# GitLab Security Dashboard **(ULTIMATE)**

View File

@ -1,7 +1,7 @@
---
type: reference, howto
stage: Secure
group: Vulnerability Research
group: Threat Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---

View File

@ -1007,7 +1007,7 @@ falco:
You can customize Falco's Helm variables by defining the
`.gitlab/managed-apps/falco/values.yaml` file in your cluster
management project. Refer to the
[Falco chart](https://github.com/helm/charts/blob/master/stable/falco/)
[Falco chart](https://github.com/falcosecurity/charts/tree/master/falco)
for the available configuration options.
CAUTION: **Caution:**

View File

@ -25,8 +25,7 @@ module API
get "deploy_keys" do
authenticated_as_admin!
deploy_keys = DeployKey.all.preload_users
present paginate(deploy_keys), with: Entities::SSHKey
present paginate(DeployKey.all), with: Entities::DeployKey
end
params do
@ -43,7 +42,7 @@ module API
end
# rubocop: disable CodeReuse/ActiveRecord
get ":id/deploy_keys" do
keys = user_project.deploy_keys_projects.preload(deploy_key: [:user])
keys = user_project.deploy_keys_projects.preload(:deploy_key)
present paginate(keys), with: Entities::DeployKeysProject
end
@ -105,7 +104,7 @@ module API
# rubocop: enable CodeReuse/ActiveRecord
desc 'Update an existing deploy key for a project' do
success Entities::SSHKey
success Entities::DeployKey
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
@ -140,7 +139,7 @@ module API
desc 'Enable a deploy key for a project' do
detail 'This feature was added in GitLab 8.11'
success Entities::SSHKey
success Entities::DeployKey
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
@ -150,7 +149,7 @@ module API
current_user, declared_params).execute
if key
present key, with: Entities::SSHKey
present key, with: Entities::DeployKey
else
not_found!('Deploy Key')
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module API
module Entities
class DeployKey < Entities::SSHKey
expose :key
end
end
end

View File

@ -2,7 +2,8 @@
module API
module Entities
class DeployKeyWithUser < Entities::SSHKeyWithUser
class DeployKeyWithUser < Entities::DeployKey
expose :user, using: Entities::UserPublic
expose :deploy_keys_projects
end
end

View File

@ -3,7 +3,7 @@
module API
module Entities
class DeployKeysProject < Grape::Entity
expose :deploy_key, merge: true, using: Entities::SSHKey
expose :deploy_key, merge: true, using: Entities::DeployKey
expose :can_push
end
end

View File

@ -1,29 +1,59 @@
image: node:latest
default:
image: node:latest
# Validate that the repository contains a package.json and extract a few values from it.
before_script:
- |
if [[ ! -f package.json ]]; then
echo "No package.json found! A package.json file is required to publish a package to GitLab's NPM registry."
echo 'For more information, see https://docs.gitlab.com/ee/user/packages/npm_registry/#creating-a-project'
exit 1
fi
- NPM_PACKAGE_NAME=$(node -p "require('./package.json').name")
- NPM_PACKAGE_VERSION=$(node -p "require('./package.json').version")
# Validate that the package name is properly scoped to the project's root namespace.
# For more information, see https://docs.gitlab.com/ee/user/packages/npm_registry/#package-naming-convention
validate_package_scope:
stage: build
script:
- |
if [[ ! $NPM_PACKAGE_NAME =~ ^@$CI_PROJECT_ROOT_NAMESPACE/ ]]; then
echo "Invalid package scope! Packages must be scoped in the root namespace of the project, e.g. \"@${CI_PROJECT_ROOT_NAMESPACE}/${CI_PROJECT_NAME}\""
echo 'For more information, see https://docs.gitlab.com/ee/user/packages/npm_registry/#package-naming-convention'
exit 1
fi
# If no .npmrc if included in the repo, generate a temporary one to use during the publish step
# that is configured to publish to GitLab's NPM registry
create_npmrc:
stage: build
script:
- |
if [ ! -f .npmrc ]; then
if [[ ! -f .npmrc ]]; then
echo 'No .npmrc found! Creating one now. Please review the following link for more information: https://docs.gitlab.com/ee/user/packages/npm_registry/index.html#authenticating-with-a-ci-job-token'
echo "@${CI_PROJECT_NAMESPACE%%/*}:registry=\${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/" >> .npmrc
echo '//gitlab.com/api/v4/packages/npm/:_authToken=${CI_JOB_TOKEN}' >> .npmrc
echo '//gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}' >> .npmrc
{
echo "@${CI_PROJECT_ROOT_NAMESPACE}:registry=\${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/"
echo '//gitlab.com/api/v4/packages/npm/:_authToken=${CI_JOB_TOKEN}'
echo '//gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}'
} >> .npmrc
fi
artifacts:
paths:
- .npmrc
# Publish the package. If the version in package.json has not yet been published, it will be
# published to GitLab's NPM registry. If the version already exists, the publish command
# will fail and the existing package will not be updated.
publish_package:
stage: deploy
script:
- export NPM_PACKAGE_NAME=$(node -p "require('./package.json').name")
- export NPM_PACKAGE_VERSION=$(node -p "require('./package.json').version")
- |
{
npm publish &&
echo "Successfully published version $NPM_PACKAGE_VERSION of $NPM_PACKAGE_NAME to GitLab's NPM registry."
echo "Successfully published version ${NPM_PACKAGE_VERSION} of ${NPM_PACKAGE_NAME} to GitLab's NPM registry: ${CI_PROJECT_URL}/-/packages"
} || {
echo "No new version of $NPM_PACKAGE_NAME published. This is most likely because version $NPM_PACKAGE_VERSION already exists in GitLab's NPM registry."
echo "No new version of ${NPM_PACKAGE_NAME} published. This is most likely because version ${NPM_PACKAGE_VERSION} already exists in GitLab's NPM registry."
}

View File

@ -72,9 +72,9 @@ module Gitlab
def commit_deltas(commit)
request = Gitaly::CommitDeltaRequest.new(diff_from_parent_request_params(commit))
response = GitalyClient.call(@repository.storage, :diff_service, :commit_delta, request, timeout: GitalyClient.fast_timeout)
response.flat_map { |msg| msg.deltas }
GitalyClient.streaming_call(@repository.storage, :diff_service, :commit_delta, request, timeout: GitalyClient.fast_timeout) do |response|
response.flat_map { |msg| msg.deltas }
end
end
def tree_entry(ref, path, limit = nil)
@ -349,10 +349,10 @@ module Gitlab
end
end
response = GitalyClient.call(@repository.storage, :commit_service, :filter_shas_with_signatures, enum, timeout: GitalyClient.fast_timeout)
response.flat_map do |msg|
msg.shas.map { |sha| EncodingHelper.encode!(sha) }
GitalyClient.streaming_call(@repository.storage, :commit_service, :filter_shas_with_signatures, enum, timeout: GitalyClient.fast_timeout) do |response|
response.flat_map do |msg|
msg.shas.map { |sha| EncodingHelper.encode!(sha) }
end
end
end
@ -400,8 +400,9 @@ module Gitlab
request_params.merge!(Gitlab::Git::DiffCollection.limits(options).to_h)
request = Gitaly::CommitDiffRequest.new(request_params)
response = GitalyClient.call(@repository.storage, :diff_service, :commit_diff, request, timeout: GitalyClient.medium_timeout)
GitalyClient::DiffStitcher.new(response)
GitalyClient.streaming_call(@repository.storage, :diff_service, :commit_diff, request, timeout: GitalyClient.medium_timeout) do |response|
GitalyClient::DiffStitcher.new(response)
end
end
def diff_from_parent_request_params(commit, options = {})

View File

@ -233,23 +233,32 @@ describe('Dashboard Panel', () => {
expect(wrapper.find(MonitorTimeSeriesChart).isVueInstance()).toBe(true);
});
it.each`
data | component
${dataWithType(panelTypes.AREA_CHART)} | ${MonitorTimeSeriesChart}
${dataWithType(panelTypes.LINE_CHART)} | ${MonitorTimeSeriesChart}
${anomalyMockGraphData} | ${MonitorAnomalyChart}
${dataWithType(panelTypes.COLUMN)} | ${MonitorColumnChart}
${dataWithType(panelTypes.STACKED_COLUMN)} | ${MonitorStackedColumnChart}
${singleStatMetricsResult} | ${MonitorSingleStatChart}
${graphDataPrometheusQueryRangeMultiTrack} | ${MonitorHeatmapChart}
${barMockData} | ${MonitorBarChart}
`('wrapps a $data.type component binding attributes', ({ data, component }) => {
describe.each`
data | component | hasCtxMenu
${dataWithType(panelTypes.AREA_CHART)} | ${MonitorTimeSeriesChart} | ${true}
${dataWithType(panelTypes.LINE_CHART)} | ${MonitorTimeSeriesChart} | ${true}
${singleStatMetricsResult} | ${MonitorSingleStatChart} | ${true}
${anomalyMockGraphData} | ${MonitorAnomalyChart} | ${false}
${dataWithType(panelTypes.COLUMN)} | ${MonitorColumnChart} | ${false}
${dataWithType(panelTypes.STACKED_COLUMN)} | ${MonitorStackedColumnChart} | ${false}
${graphDataPrometheusQueryRangeMultiTrack} | ${MonitorHeatmapChart} | ${false}
${barMockData} | ${MonitorBarChart} | ${false}
`('when $data.type data is provided', ({ data, component, hasCtxMenu }) => {
const attrs = { attr1: 'attr1Value', attr2: 'attr2Value' };
createWrapper({ graphData: data }, { attrs });
expect(wrapper.find(component).exists()).toBe(true);
expect(wrapper.find(component).isVueInstance()).toBe(true);
expect(wrapper.find(component).attributes()).toMatchObject(attrs);
beforeEach(() => {
createWrapper({ graphData: data }, { attrs });
});
it(`renders the chart component and binds attributes`, () => {
expect(wrapper.find(component).exists()).toBe(true);
expect(wrapper.find(component).isVueInstance()).toBe(true);
expect(wrapper.find(component).attributes()).toMatchObject(attrs);
});
it(`contextual menu is ${hasCtxMenu ? '' : 'not '}shown`, () => {
expect(findCtxMenu().exists()).toBe(hasCtxMenu);
});
});
});
});

View File

@ -369,6 +369,7 @@ export const singleStatMetricsResult = {
{
metric: { job: 'prometheus' },
value: ['2019-06-26T21:03:20.881Z', 91],
values: [['2019-06-26T21:03:20.881Z', 91]],
},
],
},

View File

@ -0,0 +1,73 @@
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import component from '~/registry/explorer/components/delete_button.vue';
describe('delete_button', () => {
let wrapper;
const defaultProps = {
title: 'Foo title',
tooltipTitle: 'Bar tooltipTitle',
};
const findButton = () => wrapper.find(GlButton);
const mountComponent = props => {
wrapper = shallowMount(component, {
propsData: {
...defaultProps,
...props,
},
directives: {
GlTooltip: createMockDirective(),
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('tooltip', () => {
it('the title is controlled by tooltipTitle prop', () => {
mountComponent();
const tooltip = getBinding(wrapper.element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(tooltip.value.title).toBe(defaultProps.tooltipTitle);
});
it('is disabled when tooltipTitle is disabled', () => {
mountComponent({ tooltipDisabled: true });
const tooltip = getBinding(wrapper.element, 'gl-tooltip');
expect(tooltip.value.disabled).toBe(true);
});
describe('button', () => {
it('exists', () => {
mountComponent();
expect(findButton().exists()).toBe(true);
});
it('has the correct props/attributes bound', () => {
mountComponent({ disabled: true });
expect(findButton().attributes()).toMatchObject({
'aria-label': 'Foo title',
category: 'secondary',
icon: 'remove',
title: 'Foo title',
variant: 'danger',
disabled: 'true',
});
});
it('emits a delete event', () => {
mountComponent();
expect(wrapper.emitted('delete')).toEqual(undefined);
findButton().vm.$emit('click');
expect(wrapper.emitted('delete')).toEqual([[]]);
});
});
});
});

View File

@ -0,0 +1,68 @@
import { shallowMount } from '@vue/test-utils';
import component from '~/registry/explorer/components/list_item.vue';
describe('list item', () => {
let wrapper;
const findLeftPrimarySlot = () => wrapper.find('[data-testid="left-primary"]');
const findLeftSecondarySlot = () => wrapper.find('[data-testid="left-secondary"]');
const findRightSlot = () => wrapper.find('[data-testid="right"]');
const mountComponent = propsData => {
wrapper = shallowMount(component, {
propsData,
slots: {
'left-primary': '<div data-testid="left-primary" />',
'left-secondary': '<div data-testid="left-secondary" />',
right: '<div data-testid="right" />',
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('has a left primary slot', () => {
mountComponent();
expect(findLeftPrimarySlot().exists()).toBe(true);
});
it('has a left secondary slot', () => {
mountComponent();
expect(findLeftSecondarySlot().exists()).toBe(true);
});
it('has a right slot', () => {
mountComponent();
expect(findRightSlot().exists()).toBe(true);
});
describe('disabled prop', () => {
it('when true applies disabled-content class', () => {
mountComponent({ disabled: true });
expect(wrapper.classes('disabled-content')).toBe(true);
});
it('when false does not apply disabled-content class', () => {
mountComponent({ disabled: false });
expect(wrapper.classes('disabled-content')).toBe(false);
});
});
describe('index prop', () => {
it('when index is 0 displays a top border', () => {
mountComponent({ index: 0 });
expect(wrapper.classes()).toEqual(
expect.arrayContaining(['gl-border-t-solid', 'gl-border-t-1']),
);
});
it('when index is not 0 hides top border', () => {
mountComponent({ index: 1 });
expect(wrapper.classes('gl-border-t-solid')).toBe(false);
expect(wrapper.classes('gl-border-t-1')).toBe(false);
});
});
});

View File

@ -1,11 +1,14 @@
import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlSprintf } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import Component from '~/registry/explorer/components/list_page/image_list_row.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Component from '~/registry/explorer/components/list_page/image_list_row.vue';
import ListItem from '~/registry/explorer/components/list_item.vue';
import DeleteButton from '~/registry/explorer/components/delete_button.vue';
import {
ROW_SCHEDULED_FOR_DELETION,
LIST_DELETE_BUTTON_DISABLED,
REMOVE_REPOSITORY_LABEL,
} from '~/registry/explorer/constants';
import { RouterLink } from '../../stubs';
import { imagesListResponse } from '../../mock_data';
@ -13,10 +16,10 @@ import { imagesListResponse } from '../../mock_data';
describe('Image List Row', () => {
let wrapper;
const item = imagesListResponse.data[0];
const findDeleteBtn = () => wrapper.find('[data-testid="deleteImageButton"]');
const findDetailsLink = () => wrapper.find('[data-testid="detailsLink"]');
const findTagsCount = () => wrapper.find('[data-testid="tagsCount"]');
const findDeleteButtonWrapper = () => wrapper.find('[data-testid="deleteButtonWrapper"]');
const findDeleteBtn = () => wrapper.find(DeleteButton);
const findClipboardButton = () => wrapper.find(ClipboardButton);
const mountComponent = props => {
@ -24,6 +27,7 @@ describe('Image List Row', () => {
stubs: {
RouterLink,
GlSprintf,
ListItem,
},
propsData: {
item,
@ -72,29 +76,24 @@ describe('Image List Row', () => {
});
});
describe('delete button wrapper', () => {
it('has a tooltip', () => {
mountComponent();
const tooltip = getBinding(findDeleteButtonWrapper().element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(tooltip.value.title).toBe(LIST_DELETE_BUTTON_DISABLED);
});
it('tooltip is enabled when destroy_path is falsy', () => {
mountComponent({ item: { ...item, destroy_path: null } });
const tooltip = getBinding(findDeleteButtonWrapper().element, 'gl-tooltip');
expect(tooltip.value.disabled).toBeFalsy();
});
});
describe('delete button', () => {
it('exists', () => {
mountComponent();
expect(findDeleteBtn().exists()).toBe(true);
});
it('has the correct props', () => {
mountComponent();
expect(findDeleteBtn().attributes()).toMatchObject({
title: REMOVE_REPOSITORY_LABEL,
tooltipdisabled: `${Boolean(item.destroy_path)}`,
tooltiptitle: LIST_DELETE_BUTTON_DISABLED,
});
});
it('emits a delete event', () => {
mountComponent();
findDeleteBtn().vm.$emit('click');
findDeleteBtn().vm.$emit('delete');
expect(wrapper.emitted('delete')).toEqual([[item]]);
});

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'spec_helper'
describe API::Entities::DeployKey do
describe '#as_json' do
subject { entity.as_json }
let(:deploy_key) { create(:deploy_key, public: true) }
let(:entity) { described_class.new(deploy_key) }
it 'includes basic fields', :aggregate_failures do
is_expected.to include(
id: deploy_key.id,
title: deploy_key.title,
created_at: deploy_key.created_at,
expires_at: deploy_key.expires_at,
key: deploy_key.key
)
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
require 'spec_helper'
describe API::Entities::DeployKeysProject do
describe '#as_json' do
subject { entity.as_json }
let(:deploy_keys_project) { create(:deploy_keys_project, :write_access) }
let(:entity) { described_class.new(deploy_keys_project) }
it 'includes basic fields', :aggregate_failures do
deploy_key = deploy_keys_project.deploy_key
is_expected.to include(
id: deploy_key.id,
title: deploy_key.title,
created_at: deploy_key.created_at,
expires_at: deploy_key.expires_at,
key: deploy_key.key,
can_push: deploy_keys_project.can_push
)
end
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'spec_helper'
describe API::Entities::SSHKey do
describe '#as_json' do
subject { entity.as_json }
let(:key) { create(:key, user: create(:user)) }
let(:entity) { described_class.new(key) }
it 'includes basic fields', :aggregate_failures do
is_expected.to include(
id: key.id,
title: key.title,
created_at: key.created_at,
expires_at: key.expires_at,
key: key.publishable_key
)
end
end
end

View File

@ -0,0 +1,111 @@
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Emoji do
let_it_be(:emojis) { Gemojione.index.instance_variable_get(:@emoji_by_name) }
let_it_be(:emojis_by_moji) { Gemojione.index.instance_variable_get(:@emoji_by_moji) }
let_it_be(:emoji_unicode_versions_by_name) { Gitlab::Json.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json'))) }
let_it_be(:emojis_aliases) { Gitlab::Json.parse(File.read(Rails.root.join('fixtures', 'emojis', 'aliases.json'))) }
describe '.emojis' do
it 'returns emojis' do
current_emojis = described_class.emojis
expect(current_emojis).to eq(emojis)
end
end
describe '.emojis_by_moji' do
it 'return emojis by moji' do
current_emojis_by_moji = described_class.emojis_by_moji
expect(current_emojis_by_moji).to eq(emojis_by_moji)
end
end
describe '.emojis_unicodes' do
it 'returns emoji unicodes' do
emoji_keys = described_class.emojis_unicodes
expect(emoji_keys).to eq(emojis_by_moji.keys)
end
end
describe '.emojis_names' do
it 'returns emoji names' do
emoji_names = described_class.emojis_names
expect(emoji_names).to eq(emojis.keys)
end
end
describe '.emojis_aliases' do
it 'returns emoji aliases' do
emoji_aliases = described_class.emojis_aliases
expect(emoji_aliases).to eq(emojis_aliases)
end
end
describe '.emoji_filename' do
it 'returns emoji filename' do
# "100" => {"unicode"=>"1F4AF"...}
emoji_filename = described_class.emoji_filename('100')
expect(emoji_filename).to eq(emojis['100']['unicode'])
end
end
describe '.emoji_unicode_filename' do
it 'returns emoji unicode filename' do
emoji_unicode_filename = described_class.emoji_unicode_filename('💯')
expect(emoji_unicode_filename).to eq(emojis_by_moji['💯']['unicode'])
end
end
describe '.emoji_unicode_version' do
it 'returns emoji unicode version by name' do
emoji_unicode_version = described_class.emoji_unicode_version('100')
expect(emoji_unicode_version).to eq(emoji_unicode_versions_by_name['100'])
end
end
describe '.normalize_emoji_name' do
it 'returns same name if not found in aliases' do
emoji_name = described_class.normalize_emoji_name('random')
expect(emoji_name).to eq('random')
end
it 'returns name if name found in aliases' do
emoji_name = described_class.normalize_emoji_name('small_airplane')
expect(emoji_name).to eq(emojis_aliases['small_airplane'])
end
end
describe '.emoji_image_tag' do
it 'returns emoji image tag' do
emoji_image = described_class.emoji_image_tag('emoji_one', 'src_url')
expect(emoji_image).to eq( "<img class='emoji' title=':emoji_one:' alt=':emoji_one:' src='src_url' height='20' width='20' align='absmiddle' />")
end
end
describe '.gl_emoji_tag' do
it 'returns gl emoji tag if emoji is found' do
gl_tag = described_class.gl_emoji_tag('small_airplane')
expect(gl_tag).to eq('<gl-emoji title="small airplane" data-name="airplane_small" data-unicode-version="7.0">🛩</gl-emoji>')
end
it 'returns nil if emoji name is not found' do
gl_tag = described_class.gl_emoji_tag('random')
expect(gl_tag).to be_nil
end
end
end

View File

@ -8,7 +8,7 @@ describe API::DeployKeys do
let(:admin) { create(:admin) }
let(:project) { create(:project, creator_id: user.id) }
let(:project2) { create(:project, creator_id: user.id) }
let(:deploy_key) { create(:deploy_key, public: true, user: user) }
let(:deploy_key) { create(:deploy_key, public: true) }
let!(:deploy_keys_project) do
create(:deploy_keys_project, project: project, deploy_key: deploy_key)
@ -40,32 +40,6 @@ describe API::DeployKeys do
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id)
end
it 'returns all deploy keys with comments replaced with'\
'a simple identifier of username + hostname' do
get api('/deploy_keys', admin)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
keys = json_response.map { |key_detail| key_detail['key'] }
expect(keys).to all(include("#{user.name} (#{Gitlab.config.gitlab.host}"))
end
context 'N+1 queries' do
before do
get api('/deploy_keys', admin)
end
it 'avoids N+1 queries', :request_store do
control_count = ActiveRecord::QueryRecorder.new { get api('/deploy_keys', admin) }.count
create_list(:deploy_key, 2, public: true, user: create(:user))
expect { get api('/deploy_keys', admin) }.not_to exceed_query_limit(control_count)
end
end
end
end
@ -82,25 +56,6 @@ describe API::DeployKeys do
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(deploy_key.title)
end
context 'N+1 queries' do
before do
get api("/projects/#{project.id}/deploy_keys", admin)
end
it 'avoids N+1 queries', :request_store do
control_count = ActiveRecord::QueryRecorder.new do
get api("/projects/#{project.id}/deploy_keys", admin)
end.count
deploy_key = create(:deploy_key, user: create(:user))
create(:deploy_keys_project, project: project, deploy_key: deploy_key)
expect do
get api("/projects/#{project.id}/deploy_keys", admin)
end.not_to exceed_query_limit(control_count)
end
end
end
describe 'GET /projects/:id/deploy_keys/:key_id' do
@ -111,13 +66,6 @@ describe API::DeployKeys do
expect(json_response['title']).to eq(deploy_key.title)
end
it 'exposes key comment as a simple identifier of username + hostname' do
get api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['key']).to include("#{deploy_key.user_name} (#{Gitlab.config.gitlab.host})")
end
it 'returns 404 Not Found with invalid ID' do
get api("/projects/#{project.id}/deploy_keys/404", admin)

View File

@ -99,6 +99,10 @@ describe SnippetsController, "routing" do
it "to #destroy" do
expect(delete("/snippets/1")).to route_to('snippets#destroy', id: '1')
end
it 'to #show from scope routing' do
expect(get("/-/snippets/1")).to route_to('snippets#show', id: '1')
end
end
# help GET /help(.:format) help#index