Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e829ca213b
commit
7105e0c53e
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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)"
|
||||
/>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Optimize rolling 28 days snippets counter
|
||||
merge_request: 34918
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add contextual menu to single stat panels
|
||||
merge_request: 34497
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replace double angle icons with double chevron
|
||||
merge_request: 34736
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Do not mask key comments for DeployKeys
|
||||
merge_request: 35014
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Copy snippet route under - scope
|
||||
merge_request: 35020
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add RSpecs for Gitlab::Emoji module
|
||||
merge_request: 34980
|
||||
author: Rajendra Kadam
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add package scope validation to Node.js template
|
||||
merge_request: 34778
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fixed mermaid not rendering when switching diff tabs
|
||||
merge_request: 35023
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix Gitaly duration timings for other CommitService RPCs
|
||||
merge_request: 34933
|
||||
author:
|
||||
type: other
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
\.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)**
|
||||
|
|
|
@ -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
|
||||
---
|
||||
|
||||
|
|
|
@ -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:**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class DeployKey < Entities::SSHKey
|
||||
expose :key
|
||||
end
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
|
|
|
@ -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 = {})
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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]],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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([[]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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]]);
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue