Commit 05b5c609 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 1078b7bf
<script>
import ViewerSwitcher from './blob_header_viewer_switcher.vue';
import DefaultActions from './blob_header_default_actions.vue';
import BlobFilepath from './blob_header_filepath.vue';
import eventHub from '../event_hub';
import { RICH_BLOB_VIEWER, SIMPLE_BLOB_VIEWER } from './constants';
export default {
components: {
ViewerSwitcher,
DefaultActions,
BlobFilepath,
},
props: {
blob: {
type: Object,
required: true,
},
hideDefaultActions: {
type: Boolean,
required: false,
default: false,
},
hideViewerSwitcher: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
activeViewer: this.blob.richViewer ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
};
},
computed: {
showViewerSwitcher() {
return !this.hideViewerSwitcher && Boolean(this.blob.simpleViewer && this.blob.richViewer);
},
showDefaultActions() {
return !this.hideDefaultActions;
},
},
created() {
if (this.showViewerSwitcher) {
eventHub.$on('switch-viewer', this.setActiveViewer);
}
},
beforeDestroy() {
if (this.showViewerSwitcher) {
eventHub.$off('switch-viewer', this.setActiveViewer);
}
},
methods: {
setActiveViewer(viewer) {
this.activeViewer = viewer;
},
},
};
</script>
<template>
<div class="js-file-title file-title-flex-parent">
<blob-filepath :blob="blob">
<template #filepathPrepend>
<slot name="prepend"></slot>
</template>
</blob-filepath>
<div class="file-actions d-none d-sm-block">
<viewer-switcher v-if="showViewerSwitcher" :blob="blob" :active-viewer="activeViewer" />
<slot name="actions"></slot>
<default-actions v-if="showDefaultActions" :blob="blob" :active-viewer="activeViewer" />
</div>
</div>
</template>
<script> <script>
import { GlButton, GlButtonGroup, GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { GlButton, GlButtonGroup, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { BTN_COPY_CONTENTS_TITLE, BTN_DOWNLOAD_TITLE, BTN_RAW_TITLE } from './constants'; import {
BTN_COPY_CONTENTS_TITLE,
BTN_DOWNLOAD_TITLE,
BTN_RAW_TITLE,
RICH_BLOB_VIEWER,
SIMPLE_BLOB_VIEWER,
} from './constants';
import eventHub from '../event_hub';
export default { export default {
components: { components: {
...@@ -16,6 +23,11 @@ export default { ...@@ -16,6 +23,11 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
activeViewer: {
type: String,
default: SIMPLE_BLOB_VIEWER,
required: false,
},
}, },
computed: { computed: {
rawUrl() { rawUrl() {
...@@ -24,10 +36,13 @@ export default { ...@@ -24,10 +36,13 @@ export default {
downloadUrl() { downloadUrl() {
return `${this.blob.rawPath}?inline=false`; return `${this.blob.rawPath}?inline=false`;
}, },
copyDisabled() {
return this.activeViewer === RICH_BLOB_VIEWER;
},
}, },
methods: { methods: {
requestCopyContents() { requestCopyContents() {
this.$emit('copy'); eventHub.$emit('copy');
}, },
}, },
BTN_COPY_CONTENTS_TITLE, BTN_COPY_CONTENTS_TITLE,
...@@ -41,6 +56,7 @@ export default { ...@@ -41,6 +56,7 @@ export default {
v-gl-tooltip.hover v-gl-tooltip.hover
:aria-label="$options.BTN_COPY_CONTENTS_TITLE" :aria-label="$options.BTN_COPY_CONTENTS_TITLE"
:title="$options.BTN_COPY_CONTENTS_TITLE" :title="$options.BTN_COPY_CONTENTS_TITLE"
:disabled="copyDisabled"
@click="requestCopyContents" @click="requestCopyContents"
> >
<gl-icon name="copy-to-clipboard" :size="14" /> <gl-icon name="copy-to-clipboard" :size="14" />
......
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
SIMPLE_BLOB_VIEWER, SIMPLE_BLOB_VIEWER,
SIMPLE_BLOB_VIEWER_TITLE, SIMPLE_BLOB_VIEWER_TITLE,
} from './constants'; } from './constants';
import eventHub from '../event_hub';
export default { export default {
components: { components: {
...@@ -21,25 +22,24 @@ export default { ...@@ -21,25 +22,24 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
}, activeViewer: {
data() { type: String,
return { default: SIMPLE_BLOB_VIEWER,
viewer: this.blob.richViewer ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER, required: false,
}; },
}, },
computed: { computed: {
isSimpleViewer() { isSimpleViewer() {
return this.viewer === SIMPLE_BLOB_VIEWER; return this.activeViewer === SIMPLE_BLOB_VIEWER;
}, },
isRichViewer() { isRichViewer() {
return this.viewer === RICH_BLOB_VIEWER; return this.activeViewer === RICH_BLOB_VIEWER;
}, },
}, },
methods: { methods: {
switchToViewer(viewer) { switchToViewer(viewer) {
if (viewer !== this.viewer) { if (viewer !== this.activeViewer) {
this.viewer = viewer; eventHub.$emit('switch-viewer', viewer);
this.$emit('switch-viewer', viewer);
} }
}, },
}, },
......
import Vue from 'vue';
export default new Vue();
...@@ -7,11 +7,13 @@ import { __ } from '~/locale'; ...@@ -7,11 +7,13 @@ import { __ } from '~/locale';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import { getDatesInRange } from '~/lib/utils/datetime_utility'; import { getDatesInRange } from '~/lib/utils/datetime_utility';
import { xAxisLabelFormatter, dateFormatter } from '../utils'; import { xAxisLabelFormatter, dateFormatter } from '../utils';
import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
export default { export default {
components: { components: {
GlAreaChart, GlAreaChart,
GlLoadingIcon, GlLoadingIcon,
ResizableChartContainer,
}, },
props: { props: {
endpoint: { endpoint: {
...@@ -201,25 +203,35 @@ export default { ...@@ -201,25 +203,35 @@ export default {
<div v-else-if="showChart" class="contributors-charts"> <div v-else-if="showChart" class="contributors-charts">
<h4>{{ __('Commits to') }} {{ branch }}</h4> <h4>{{ __('Commits to') }} {{ branch }}</h4>
<span>{{ __('Excluding merge commits. Limited to 6,000 commits.') }}</span> <span>{{ __('Excluding merge commits. Limited to 6,000 commits.') }}</span>
<div> <resizable-chart-container>
<gl-area-chart <gl-area-chart
slot-scope="{ width }"
:width="width"
:data="masterChartData" :data="masterChartData"
:option="masterChartOptions" :option="masterChartOptions"
:height="masterChartHeight" :height="masterChartHeight"
@created="onMasterChartCreated" @created="onMasterChartCreated"
/> />
</div> </resizable-chart-container>
<div class="row"> <div class="row">
<div v-for="contributor in individualChartsData" :key="contributor.name" class="col-6"> <div
v-for="(contributor, index) in individualChartsData"
:key="index"
class="col-lg-6 col-12"
>
<h4>{{ contributor.name }}</h4> <h4>{{ contributor.name }}</h4>
<p>{{ n__('%d commit', '%d commits', contributor.commits) }} ({{ contributor.email }})</p> <p>{{ n__('%d commit', '%d commits', contributor.commits) }} ({{ contributor.email }})</p>
<gl-area-chart <resizable-chart-container>
:data="contributor.dates" <gl-area-chart
:option="individualChartOptions" slot-scope="{ width }"
:height="individualChartHeight" :width="width"
@created="onIndividualChartCreated" :data="contributor.dates"
/> :option="individualChartOptions"
:height="individualChartHeight"
@created="onIndividualChartCreated"
/>
</resizable-chart-container>
</div> </div>
</div> </div>
</div> </div>
......
fragment BlobViewer on SnippetBlobViewer {
collapsed
loadingPartialName
renderError
tooLarge
}
import $ from 'jquery';
import Chart from 'chart.js';
import { lineChartOptions } from '~/lib/utils/chart_utils';
import initProjectPipelinesChartsApp from '~/projects/pipelines/charts/index'; import initProjectPipelinesChartsApp from '~/projects/pipelines/charts/index';
const SUCCESS_LINE_COLOR = '#1aaa55';
const TOTAL_LINE_COLOR = '#707070';
const buildChart = (chartScope, shouldAdjustFontSize) => {
const data = {
labels: chartScope.labels,
datasets: [
{
backgroundColor: SUCCESS_LINE_COLOR,
borderColor: SUCCESS_LINE_COLOR,
pointBackgroundColor: SUCCESS_LINE_COLOR,
pointBorderColor: '#fff',
data: chartScope.successValues,
fill: 'origin',
},
{
backgroundColor: TOTAL_LINE_COLOR,
borderColor: TOTAL_LINE_COLOR,
pointBackgroundColor: TOTAL_LINE_COLOR,
pointBorderColor: '#EEE',
data: chartScope.totalValues,
fill: '-1',
},
],
};
const ctx = $(`#${chartScope.scope}Chart`)
.get(0)
.getContext('2d');
return new Chart(ctx, {
type: 'line',
data,
options: lineChartOptions({
width: ctx.canvas.width,
numberOfPoints: chartScope.totalValues.length,
shouldAdjustFontSize,
}),
});
};
document.addEventListener('DOMContentLoaded', () => {
const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML);
// Scale fonts if window width lower than 768px (iPad portrait)
const shouldAdjustFontSize = window.innerWidth < 768;
chartsData.forEach(scope => buildChart(scope, shouldAdjustFontSize));
});
document.addEventListener('DOMContentLoaded', initProjectPipelinesChartsApp); document.addEventListener('DOMContentLoaded', initProjectPipelinesChartsApp);
<script> <script>
import dateFormat from 'dateformat';
import { __, sprintf } from '~/locale';
import { GlColumnChart } from '@gitlab/ui/dist/charts'; import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import StatisticsList from './statistics_list.vue'; import StatisticsList from './statistics_list.vue';
import PipelinesAreaChart from './pipelines_area_chart.vue';
import { import {
CHART_CONTAINER_HEIGHT, CHART_CONTAINER_HEIGHT,
INNER_CHART_HEIGHT, INNER_CHART_HEIGHT,
X_AXIS_LABEL_ROTATION, X_AXIS_LABEL_ROTATION,
X_AXIS_TITLE_OFFSET, X_AXIS_TITLE_OFFSET,
CHART_DATE_FORMAT,
ONE_WEEK_AGO_DAYS,
ONE_MONTH_AGO_DAYS,
} from '../constants'; } from '../constants';
export default { export default {
components: { components: {
StatisticsList, StatisticsList,
GlColumnChart, GlColumnChart,
PipelinesAreaChart,
}, },
props: { props: {
counts: { counts: {
...@@ -22,6 +30,18 @@ export default { ...@@ -22,6 +30,18 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
lastWeekChartData: {
type: Object,
required: true,
},
lastMonthChartData: {
type: Object,
required: true,
},
lastYearChartData: {
type: Object,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -30,10 +50,38 @@ export default { ...@@ -30,10 +50,38 @@ export default {
}, },
}; };
}, },
computed: {
areaCharts() {
const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles;
return [
this.buildAreaChartData(lastWeek, this.lastWeekChartData),
this.buildAreaChartData(lastMonth, this.lastMonthChartData),
this.buildAreaChartData(lastYear, this.lastYearChartData),
];
},
},
methods: { methods: {
mergeLabelsAndValues(labels, values) { mergeLabelsAndValues(labels, values) {
return labels.map((label, index) => [label, values[index]]); return labels.map((label, index) => [label, values[index]]);
}, },
buildAreaChartData(title, data) {
const { labels, totals, success } = data;
return {
title,
data: [
{
name: 'all',
data: this.mergeLabelsAndValues(labels, totals),
},
{
name: 'success',
data: this.mergeLabelsAndValues(labels, success),
},
],
};
},
}, },
chartContainerHeight: CHART_CONTAINER_HEIGHT, chartContainerHeight: CHART_CONTAINER_HEIGHT,
timesChartOptions: { timesChartOptions: {
...@@ -45,6 +93,22 @@ export default { ...@@ -45,6 +93,22 @@ export default {
nameGap: X_AXIS_TITLE_OFFSET, nameGap: X_AXIS_TITLE_OFFSET,
}, },
}, },
get chartTitles() {
const today = dateFormat(new Date(), CHART_DATE_FORMAT);
const pastDate = timeScale =>
dateFormat(getDateInPast(new Date(), timeScale), CHART_DATE_FORMAT);
return {
lastWeek: sprintf(__('Pipelines for last week (%{oneWeekAgo} - %{today})'), {
oneWeekAgo: pastDate(ONE_WEEK_AGO_DAYS),
today,
}),
lastMonth: sprintf(__('Pipelines for last month (%{oneMonthAgo} - %{today})'), {
oneMonthAgo: pastDate(ONE_MONTH_AGO_DAYS),
today,
}),
lastYear: __('Pipelines for last year'),
};
},
}; };
</script> </script>
<template> <template>
...@@ -68,5 +132,14 @@ export default { ...@@ -68,5 +132,14 @@ export default {
/> />
</div> </div>
</div> </div>
<hr />
<h4 class="my-4">{{ __('Pipelines charts') }}</h4>
<pipelines-area-chart
v-for="(chart, index) in areaCharts"
:key="index"
:chart-data="chart.data"
>
{{ chart.title }}
</pipelines-area-chart>
</div> </div>
</template> </template>
<script>
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { s__ } from '~/locale';
import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
import { CHART_CONTAINER_HEIGHT } from '../constants';
export default {
components: {
GlAreaChart,
ResizableChartContainer,
},
props: {
chartData: {
type: Array,
required: true,
},
},
areaChartOptions: {
xAxis: {
name: s__('Pipeline|Date'),
type: 'category',
},
yAxis: {
name: s__('Pipeline|Pipelines'),
},
},
chartContainerHeight: CHART_CONTAINER_HEIGHT,
};
</script>
<template>
<div class="prepend-top-default">
<p>
<slot></slot>
</p>
<resizable-chart-container>
<gl-area-chart
slot-scope="{ width }"
:width="width"
:height="$options.chartContainerHeight"
:data="chartData"
:include-legend-avg-max="false"
:option="$options.areaChartOptions"
/>
</resizable-chart-container>
</div>
</template>
...@@ -5,3 +5,9 @@ export const INNER_CHART_HEIGHT = 200; ...@@ -5,3 +5,9 @@ export const INNER_CHART_HEIGHT = 200;
export const X_AXIS_LABEL_ROTATION = 45; export const X_AXIS_LABEL_ROTATION = 45;
export const X_AXIS_TITLE_OFFSET = 60; export const X_AXIS_TITLE_OFFSET = 60;
export const ONE_WEEK_AGO_DAYS = 7;
export const ONE_MONTH_AGO_DAYS = 31;
export const CHART_DATE_FORMAT = 'dd mmm';
...@@ -10,8 +10,23 @@ export default () => { ...@@ -10,8 +10,23 @@ export default () => {
successRatio, successRatio,
timesChartLabels, timesChartLabels,
timesChartValues, timesChartValues,
lastWeekChartLabels,
lastWeekChartTotals,
lastWeekChartSuccess,
lastMonthChartLabels,
lastMonthChartTotals,
lastMonthChartSuccess,
lastYearChartLabels,
lastYearChartTotals,
lastYearChartSuccess,
} = el.dataset; } = el.dataset;
const parseAreaChartData = (labels, totals, success) => ({
labels: JSON.parse(labels),
totals: JSON.parse(totals),
success: JSON.parse(success),
});
return new Vue({ return new Vue({
el, el,
name: 'ProjectPipelinesChartsApp', name: 'ProjectPipelinesChartsApp',
...@@ -31,6 +46,21 @@ export default () => { ...@@ -31,6 +46,21 @@ export default () => {
labels: JSON.parse(timesChartLabels), labels: JSON.parse(timesChartLabels),
values: JSON.parse(timesChartValues), values: JSON.parse(timesChartValues),
}, },
lastWeekChartData: parseAreaChartData(
lastWeekChartLabels,
lastWeekChartTotals,
lastWeekChartSuccess,
),
lastMonthChartData: parseAreaChartData(
lastMonthChartLabels,
lastMonthChartTotals,
lastMonthChartSuccess,
),
lastYearChartData: parseAreaChartData(
lastYearChartLabels,
lastYearChartTotals,
lastYearChartSuccess,
),
}, },
}), }),
}); });
......
...@@ -108,7 +108,12 @@ export default { ...@@ -108,7 +108,12 @@ export default {
class="avatar-cell" class="avatar-cell"
/> />
<span v-else class="avatar-cell user-avatar-link"> <span v-else class="avatar-cell user-avatar-link">
<img :src="$options.defaultAvatarUrl" width="40" height="40" class="avatar s40" /> <img
:src="commit.authorGravatar || $options.defaultAvatarUrl"
width="40"
height="40"
class="avatar s40"
/>
</span> </span>
<div class="commit-detail flex-list"> <div class="commit-detail flex-list">
<div class="commit-content qa-commit-content"> <div class="commit-content qa-commit-content">
......
<script> <script>
import { escapeRegExp } from 'lodash';
import { GlBadge, GlLink, GlSkeletonLoading, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui'; import { GlBadge, GlLink, GlSkeletonLoading, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
...@@ -105,7 +106,7 @@ export default { ...@@ -105,7 +106,7 @@ export default {
return this.isFolder ? 'router-link' : 'a'; return this.isFolder ? 'router-link' : 'a';
}, },
fullPath() { fullPath() {
return this.path.replace(new RegExp(`^${this.currentPath}/`), ''); return this.path.replace(new RegExp(`^${escapeRegExp(this.currentPath)}/`), '');
}, },
shortSha() { shortSha() {
return this.sha.slice(0, 8); return this.sha.slice(0, 8);
......
...@@ -48,7 +48,7 @@ const defaultClient = createDefaultClient( ...@@ -48,7 +48,7 @@ const defaultClient = createDefaultClient(
case 'TreeEntry': case 'TreeEntry':
case 'Submodule': case 'Submodule':
case 'Blob': case 'Blob':
return `${obj.flatPath}-${obj.id}`; return `${escape(obj.flatPath)}-${obj.id}`;
default: default:
// If the type doesn't match any of the above we fallback // If the type doesn't match any of the above we fallback
// to using the default Apollo ID // to using the default Apollo ID
......
...@@ -10,6 +10,7 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) { ...@@ -10,6 +10,7 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
webUrl webUrl
authoredDate authoredDate
authorName authorName
authorGravatar
author { author {
name name
avatarUrl avatarUrl
......
<script> <script>
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue'; import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import { SNIPPET_VISIBILITY_PUBLIC } from '../constants'; import { SNIPPET_VISIBILITY_PUBLIC } from '../constants';
import BlobHeader from '~/blob/components/blob_header.vue';
import GetSnippetBlobQuery from '../queries/snippet.blob.query.graphql';
import { GlLoadingIcon } from '@gitlab/ui';
export default { export default {
components: { components: {
BlobEmbeddable, BlobEmbeddable,
BlobHeader,
GlLoadingIcon,
},
apollo: {
blob: {
query: GetSnippetBlobQuery,
variables() {
return {
ids: this.snippet.id,
};
},
update: data => data.snippets.edges[0].node.blob,
},
}, },
props: { props: {
snippet: { snippet: {
...@@ -12,15 +28,32 @@ export default { ...@@ -12,15 +28,32 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
blob: {},
};
},
computed: { computed: {
embeddable() { embeddable() {
return this.snippet.visibilityLevel === SNIPPET_VISIBILITY_PUBLIC; return this.snippet.visibilityLevel === SNIPPET_VISIBILITY_PUBLIC;
}, },
isBlobLoading() {
return this.$apollo.queries.blob.loading;
},
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<blob-embeddable v-if="embeddable" class="mb-3" :url="snippet.webUrl" /> <blob-embeddable v-if="embeddable" class="mb-3" :url="snippet.webUrl" />
<gl-loading-icon
v-if="isBlobLoading"
:label="__('Loading blob')"
:size="2"
class="prepend-top-20 append-bottom-20"
/>
<article v-else class="file-holder snippet-file-content">
<blob-header :blob="blob" />
</article>
</div> </div>
</template> </template>
#import '~/graphql_shared/fragments/blobviewer.fragment.graphql'
query SnippetBlobFull($ids: [ID!]) {
snippets(ids: $ids) {
edges {
node {
id
blob {
binary
name
path
rawPath
size
simpleViewer {
...BlobViewer
}
richViewer {
...BlobViewer
}
}
}
}
}
}
...@@ -31,12 +31,7 @@ module Projects ...@@ -31,12 +31,7 @@ module Projects
end end
def bulk_destroy def bulk_destroy
unless params[:ids].present? tag_names = params.require(:ids) || []
head :bad_request
return
end
tag_names = params[:ids] || []
if tag_names.size > LIMIT if tag_names.size > LIMIT
head :bad_request head :bad_request
return return
......
...@@ -117,8 +117,10 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -117,8 +117,10 @@ class RegistrationsController < Devise::RegistrationsController
end end
def after_inactive_sign_up_path_for(resource) def after_inactive_sign_up_path_for(resource)
# With the current `allow_unconfirmed_access_for` Devise setting in config/initializers/8_devise.rb,
# this method is never called. Leaving this here in case that value is set to 0.
Gitlab::AppLogger.info(user_created_message) Gitlab::AppLogger.info(user_created_message)
dashboard_projects_path users_almost_there_path
end end
private private
......
...@@ -26,6 +26,11 @@ module Types ...@@ -26,6 +26,11 @@ module Types
description: 'Rendered HTML of the commit signature' description: 'Rendered HTML of the commit signature'
field :author_name, type: GraphQL::STRING_TYPE, null: true, field :author_name, type: GraphQL::STRING_TYPE, null: true,
description: 'Commit authors name' description: 'Commit authors name'
field :author_gravatar, type: GraphQL::STRING_TYPE, null: true,
description: 'Commit authors gravatar',
resolve: -> (commit, args, context) do
GravatarService.new.execute(commit.author_email, 40)
end
# models/commit lazy loads the author by email # models/commit lazy loads the author by email
field :author, type: Types::UserType, null: true, field :author, type: Types::UserType, null: true,
......
...@@ -216,7 +216,7 @@ module Ci ...@@ -216,7 +216,7 @@ module Ci
end end
end end
after_transition created: :pending do |pipeline| after_transition created: any - [:failed] do |pipeline|
next unless pipeline.bridge_triggered? next unless pipeline.bridge_triggered?
next if pipeline.bridge_waiting? next if pipeline.bridge_waiting?
......
- page_title _('CI / CD Charts') - page_title _('CI / CD Charts')
#js-project-pipelines-charts-app{ data: { counts: @counts, success_ratio: success_ratio(@counts), times_chart: { labels: @charts[:pipeline_times].labels, values: @charts[:pipeline_times].pipeline_times } } } #js-project-pipelines-charts-app{ data: { counts: @counts, success_ratio: success_ratio(@counts),
times_chart: { labels: @charts[:pipeline_times].labels, values: @charts[:pipeline_times].pipeline_times },
#charts.ci-charts last_week_chart: { labels: @charts[:week].labels, totals: @charts[:week].total, success: @charts[:week].success },
%hr last_month_chart: { labels: @charts[:month].labels, totals: @charts[:month].total, success: @charts[:month].success },
= render 'projects/pipelines/charts/pipelines' last_year_chart: { labels: @charts[:year].labels, totals: @charts[:year].total, success: @charts[:year].success } } }
%h4.mt-4.mb-4= _("Pipelines charts")
%p
&nbsp;
%span.legend-success
= icon("circle")
= s_("Pipeline|success")
&nbsp;
%span.legend-all
= icon("circle")
= s_("Pipeline|all")
.prepend-top-default
%p.light
= _("Pipelines for last week")
(#{date_from_to(Date.today - 7.days, Date.today)})
%div
%canvas#weekChart{ height: 200 }
.prepend-top-default
%p.light
= _("Pipelines for last month")
(#{date_from_to(Date.today - 30.days, Date.today)})
%div
%canvas#monthChart{ height: 200 }
.prepend-top-default
%p.light
= _("Pipelines for last year")
%div
%canvas#yearChart.padded{ height: 250 }
-# haml-lint:disable InlineJavaScript
%script#pipelinesChartsData{ type: "application/json" }
- chartData = []
- [:week, :month, :year].each do |scope|
- chartData.push({ 'scope' => scope, 'labels' => @charts[scope].labels, 'totalValues' => @charts[scope].total, 'successValues' => @charts[scope].success })
= chartData.to_json.html_safe
---
title: Migrate CI CD pipelines charts to ECharts
merge_request: 24057
author:
type: changed
---
title: Fix upstream bridge stuck when downstream pipeline is not pending
merge_request: 24665
author:
type: fixed
...@@ -165,6 +165,11 @@ type Commit { ...@@ -165,6 +165,11 @@ type Commit {
""" """
author: User author: User
"""
Commit authors gravatar
"""
authorGravatar: String
""" """
Commit authors name Commit authors name
""" """
......
...@@ -13259,6 +13259,20 @@ ...@@ -13259,6 +13259,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "authorGravatar",
"description": "Commit authors gravatar",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "authorName", "name": "authorName",
"description": "Commit authors name", "description": "Commit authors name",
......
...@@ -54,6 +54,7 @@ An emoji awarded by a user. ...@@ -54,6 +54,7 @@ An emoji awarded by a user.
| Name | Type | Description | | Name | Type | Description |
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `author` | User | Author of the commit | | `author` | User | Author of the commit |
| `authorGravatar` | String | Commit authors gravatar |
| `authorName` | String | Commit authors name | | `authorName` | String | Commit authors name |
| `authoredDate` | Time | Timestamp of when the commit was authored | | `authoredDate` | Time | Timestamp of when the commit was authored |
| `description` | String | Description of the commit message | | `description` | String | Description of the commit message |
......
...@@ -11419,6 +11419,9 @@ msgstr "" ...@@ -11419,6 +11419,9 @@ msgstr ""
msgid "Live preview" msgid "Live preview"
msgstr "" msgstr ""
msgid "Loading blob"
msgstr ""
msgid "Loading contribution stats for group members" msgid "Loading contribution stats for group members"
msgstr "" msgstr ""
...@@ -13672,10 +13675,10 @@ msgstr "" ...@@ -13672,10 +13675,10 @@ msgstr ""
msgid "Pipelines emails" msgid "Pipelines emails"
msgstr "" msgstr ""
msgid "Pipelines for last month" msgid "Pipelines for last month (%{oneMonthAgo} - %{today})"
msgstr "" msgstr ""
msgid "Pipelines for last week" msgid "Pipelines for last week (%{oneWeekAgo} - %{today})"
msgstr "" msgstr ""
msgid "Pipelines for last year" msgid "Pipelines for last year"
...@@ -13759,6 +13762,9 @@ msgstr "" ...@@ -13759,6 +13762,9 @@ msgstr ""
msgid "Pipeline|Coverage" msgid "Pipeline|Coverage"
msgstr "" msgstr ""
msgid "Pipeline|Date"
msgstr ""
msgid "Pipeline|Detached merge request pipeline" msgid "Pipeline|Detached merge request pipeline"
msgstr "" msgstr ""
...@@ -13780,6 +13786,9 @@ msgstr "" ...@@ -13780,6 +13786,9 @@ msgstr ""
msgid "Pipeline|Pipeline" msgid "Pipeline|Pipeline"
msgstr "" msgstr ""
msgid "Pipeline|Pipelines"
msgstr ""
msgid "Pipeline|Run Pipeline" msgid "Pipeline|Run Pipeline"
msgstr "" msgstr ""
...@@ -13816,18 +13825,12 @@ msgstr "" ...@@ -13816,18 +13825,12 @@ msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}." msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
msgstr "" msgstr ""
msgid "Pipeline|all"
msgstr ""
msgid "Pipeline|for" msgid "Pipeline|for"
msgstr "" msgstr ""
msgid "Pipeline|on" msgid "Pipeline|on"
msgstr "" msgstr ""
msgid "Pipeline|success"
msgstr ""
msgid "Pipeline|with stage" msgid "Pipeline|with stage"
msgstr "" msgstr ""
......
...@@ -38,7 +38,9 @@ module QA ...@@ -38,7 +38,9 @@ module QA
def visit_saml_sso_settings(group, direct: false) def visit_saml_sso_settings(group, direct: false)
if direct if direct
page.visit "#{group.web_url}/-/saml" url = "#{group.web_url}/-/saml"
Runtime::Logger.debug("Visiting url \"#{url}\" directly")
page.visit url
else else
group.visit! group.visit!
......
...@@ -48,6 +48,12 @@ module QA ...@@ -48,6 +48,12 @@ module QA
feature && feature["state"] == "on" feature && feature["state"] == "on"
end end
def get_features
request = Runtime::API::Request.new(api_client, "/features")
response = get(request.url)
response.body
end
private private
def api_client def api_client
...@@ -76,12 +82,6 @@ module QA ...@@ -76,12 +82,6 @@ module QA
raise SetFeatureError, "Setting feature flag #{key} to #{value} failed with `#{response}`." raise SetFeatureError, "Setting feature flag #{key} to #{value} failed with `#{response}`."
end end
end end
def get_features
request = Runtime::API::Request.new(api_client, "/features")
response = get(request.url)
response.body
end
end end
end end
end end
...@@ -77,14 +77,32 @@ describe RegistrationsController do ...@@ -77,14 +77,32 @@ describe RegistrationsController do
context 'when send_user_confirmation_email is true' do context 'when send_user_confirmation_email is true' do
before do before do
stub_application_setting(send_user_confirmation_email: true) stub_application_setting(send_user_confirmation_email: true)
allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days
end end
it 'authenticates the user and sends a confirmation email' do context 'when a grace period is active for confirming the email address' do
post(:create, params: user_params) before do
allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days
end
it 'sends a confirmation email and redirects to the dashboard' do
post(:create, params: user_params)
expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email])
expect(response).to redirect_to(dashboard_projects_path)
end
end
expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email]) context 'when no grace period is active for confirming the email address' do
expect(response).to redirect_to(dashboard_projects_path) before do
allow(User).to receive(:allow_unconfirmed_access_for).and_return 0
end
it 'sends a confirmation email and redirects to the almost there page' do
post(:create, params: user_params)
expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email])
expect(response).to redirect_to(users_almost_there_path)
end
end end
end end
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Blob Header Default Actions rendering matches the snapshot 1`] = `
<div
class="js-file-title file-title-flex-parent"
>
<blob-filepath-stub
blob="[object Object]"
/>
<div
class="file-actions d-none d-sm-block"
>
<viewer-switcher-stub
activeviewer="rich"
blob="[object Object]"
/>
<default-actions-stub
activeviewer="rich"
blob="[object Object]"
/>
</div>
</div>
`;
...@@ -4,9 +4,11 @@ import { ...@@ -4,9 +4,11 @@ import {
BTN_COPY_CONTENTS_TITLE, BTN_COPY_CONTENTS_TITLE,
BTN_DOWNLOAD_TITLE, BTN_DOWNLOAD_TITLE,
BTN_RAW_TITLE, BTN_RAW_TITLE,
RICH_BLOB_VIEWER,
} from '~/blob/components/constants'; } from '~/blob/components/constants';
import { GlButtonGroup, GlButton } from '@gitlab/ui'; import { GlButtonGroup, GlButton } from '@gitlab/ui';
import { Blob } from './mock_data'; import { Blob } from './mock_data';
import eventHub from '~/blob/event_hub';
describe('Blob Header Default Actions', () => { describe('Blob Header Default Actions', () => {
let wrapper; let wrapper;
...@@ -14,10 +16,11 @@ describe('Blob Header Default Actions', () => { ...@@ -14,10 +16,11 @@ describe('Blob Header Default Actions', () => {
let buttons; let buttons;
const hrefPrefix = 'http://localhost'; const hrefPrefix = 'http://localhost';
function createComponent(props = {}) { function createComponent(blobProps = {}, propsData = {}) {
wrapper = mount(BlobHeaderActions, { wrapper = mount(BlobHeaderActions, {
propsData: { propsData: {
blob: Object.assign({}, Blob, props), blob: Object.assign({}, Blob, blobProps),
...propsData,
}, },
}); });
} }
...@@ -51,14 +54,30 @@ describe('Blob Header Default Actions', () => { ...@@ -51,14 +54,30 @@ describe('Blob Header Default Actions', () => {
it('correct href attribute on Download button', () => { it('correct href attribute on Download button', () => {
expect(buttons.at(2).vm.$el.href).toBe(`${hrefPrefix}${Blob.rawPath}?inline=false`); expect(buttons.at(2).vm.$el.href).toBe(`${hrefPrefix}${Blob.rawPath}?inline=false`);
}); });
it('does not render "Copy file contents" button as disables if the viewer is Simple', () => {
expect(buttons.at(0).attributes('disabled')).toBeUndefined();
});
it('renders "Copy file contents" button as disables if the viewer is Rich', () => {
createComponent(
{},
{
activeViewer: RICH_BLOB_VIEWER,
},
);
buttons = wrapper.findAll(GlButton);
expect(buttons.at(0).attributes('disabled')).toBeTruthy();
});
}); });
describe('functionally', () => { describe('functionally', () => {
it('emits an event when a Copy Contents button is clicked', () => { it('emits an event when a Copy Contents button is clicked', () => {
jest.spyOn(wrapper.vm, '$emit'); jest.spyOn(eventHub, '$emit');
buttons.at(0).vm.$emit('click'); buttons.at(0).vm.$emit('click');
expect(wrapper.vm.$emit).toHaveBeenCalledWith('copy'); expect(eventHub.$emit).toHaveBeenCalledWith('copy');
}); });
}); });
}); });
import { shallowMount, mount } from '@vue/test-utils';
import BlobHeader from '~/blob/components/blob_header.vue';
import ViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue';
import DefaultActions from '~/blob/components/blob_header_default_actions.vue';
import BlobFilepath from '~/blob/components/blob_header_filepath.vue';
import eventHub from '~/blob/event_hub';
import { Blob } from './mock_data';
describe('Blob Header Default Actions', () => {
let wrapper;
function createComponent(blobProps = {}, options = {}, propsData = {}, shouldMount = false) {
const method = shouldMount ? mount : shallowMount;
wrapper = method.call(this, BlobHeader, {
propsData: {
blob: Object.assign({}, Blob, blobProps),
...propsData,
},
...options,
});
}
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('rendering', () => {
const slots = {
prepend: 'Foo Prepend',
actions: 'Actions Bar',
};
it('matches the snapshot', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('renders all components', () => {
createComponent();
expect(wrapper.find(ViewerSwitcher).exists()).toBe(true);
expect(wrapper.find(DefaultActions).exists()).toBe(true);
expect(wrapper.find(BlobFilepath).exists()).toBe(true);
});
it('does not render viewer switcher if the blob has only the simple viewer', () => {
createComponent({
richViewer: null,
});
expect(wrapper.find(ViewerSwitcher).exists()).toBe(false);
});
it('does not render viewer switcher if a corresponding prop is passed', () => {
createComponent(
{},
{},
{
hideViewerSwitcher: true,
},
);
expect(wrapper.find(ViewerSwitcher).exists()).toBe(false);
});
it('does not render default actions is corresponding prop is passed', () => {
createComponent(
{},
{},
{
hideDefaultActions: true,
},
);
expect(wrapper.find(DefaultActions).exists()).toBe(false);
});
Object.keys(slots).forEach(slot => {
it('renders the slots', () => {
const slotContent = slots[slot];
createComponent(
{},
{
scopedSlots: {
[slot]: `<span>${slotContent}</span>`,
},
},
{},
true,
);
expect(wrapper.text()).toContain(slotContent);
});
});
});
describe('functionality', () => {
const newViewer = 'Foo Bar';
it('listens to "switch-view" event when viewer switcher is shown and updates activeViewer', () => {
expect(wrapper.vm.showViewerSwitcher).toBe(true);
eventHub.$emit('switch-viewer', newViewer);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.activeViewer).toBe(newViewer);
});
});
it('does not update active viewer if the switcher is not shown', () => {
const activeViewer = 'Alpha Beta';
createComponent(
{},
{
data() {
return {
activeViewer,
};
},
},
{
hideViewerSwitcher: true,
},
);
expect(wrapper.vm.showViewerSwitcher).toBe(false);
eventHub.$emit('switch-viewer', newViewer);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.activeViewer).toBe(activeViewer);
});
});
});
});
...@@ -8,14 +8,16 @@ import { ...@@ -8,14 +8,16 @@ import {
} from '~/blob/components/constants'; } from '~/blob/components/constants';
import { GlButtonGroup, GlButton } from '@gitlab/ui'; import { GlButtonGroup, GlButton } from '@gitlab/ui';
import { Blob } from './mock_data'; import { Blob } from './mock_data';
import eventHub from '~/blob/event_hub';
describe('Blob Header Viewer Switcher', () => { describe('Blob Header Viewer Switcher', () => {
let wrapper; let wrapper;
function createComponent(props = {}) { function createComponent(blobProps = {}, propsData = {}) {
wrapper = mount(BlobHeaderViewerSwitcher, { wrapper = mount(BlobHeaderViewerSwitcher, {
propsData: { propsData: {
blob: Object.assign({}, Blob, props), blob: Object.assign({}, Blob, blobProps),
...propsData,
}, },
}); });
} }
...@@ -25,14 +27,9 @@ describe('Blob Header Viewer Switcher', () => { ...@@ -25,14 +27,9 @@ describe('Blob Header Viewer Switcher', () => {
}); });
describe('intiialization', () => { describe('intiialization', () => {
it('is initialized with rich viewer as preselected when richViewer exists', () => { it('is initialized with simple viewer as active', () => {
createComponent(); createComponent();
expect(wrapper.vm.viewer).toBe(RICH_BLOB_VIEWER); expect(wrapper.vm.activeViewer).toBe(SIMPLE_BLOB_VIEWER);
});
it('is initialized with simple viewer as preselected when richViewer does not exists', () => {
createComponent({ richViewer: null });
expect(wrapper.vm.viewer).toBe(SIMPLE_BLOB_VIEWER);
}); });
}); });
...@@ -63,47 +60,43 @@ describe('Blob Header Viewer Switcher', () => { ...@@ -63,47 +60,43 @@ describe('Blob Header Viewer Switcher', () => {
let simpleBtn; let simpleBtn;
let richBtn; let richBtn;
beforeEach(() => { function factory(propsOptions = {}) {
createComponent(); createComponent({}, propsOptions);
buttons = wrapper.findAll(GlButton); buttons = wrapper.findAll(GlButton);
simpleBtn = buttons.at(0); simpleBtn = buttons.at(0);
richBtn = buttons.at(1); richBtn = buttons.at(1);
});
jest.spyOn(eventHub, '$emit');
}
it('does not switch the viewer if the selected one is already active', () => { it('does not switch the viewer if the selected one is already active', () => {
jest.spyOn(wrapper.vm, '$emit'); factory();
expect(wrapper.vm.activeViewer).toBe(SIMPLE_BLOB_VIEWER);
simpleBtn.vm.$emit('click');
expect(wrapper.vm.activeViewer).toBe(SIMPLE_BLOB_VIEWER);
expect(eventHub.$emit).not.toHaveBeenCalled();
});
it('emits an event when a Rich Viewer button is clicked', () => {
factory();
expect(wrapper.vm.activeViewer).toBe(SIMPLE_BLOB_VIEWER);
expect(wrapper.vm.viewer).toBe(RICH_BLOB_VIEWER);
richBtn.vm.$emit('click'); richBtn.vm.$emit('click');
expect(wrapper.vm.viewer).toBe(RICH_BLOB_VIEWER);
expect(wrapper.vm.$emit).not.toHaveBeenCalled(); return wrapper.vm.$nextTick().then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('switch-viewer', RICH_BLOB_VIEWER);
});
}); });
it('emits an event when a Simple Viewer button is clicked', () => { it('emits an event when a Simple Viewer button is clicked', () => {
jest.spyOn(wrapper.vm, '$emit'); factory({
activeViewer: RICH_BLOB_VIEWER,
});
simpleBtn.vm.$emit('click'); simpleBtn.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.viewer).toBe(SIMPLE_BLOB_VIEWER); expect(eventHub.$emit).toHaveBeenCalledWith('switch-viewer', SIMPLE_BLOB_VIEWER);
expect(wrapper.vm.$emit).toHaveBeenCalledWith('switch-viewer', SIMPLE_BLOB_VIEWER);
}); });
}); });
it('emits an event when a Rich Viewer button is clicked', () => {
jest.spyOn(wrapper.vm, '$emit');
wrapper.setData({ viewer: SIMPLE_BLOB_VIEWER });
return wrapper.vm
.$nextTick()
.then(() => {
richBtn.vm.$emit('click');
})
.then(() => {
expect(wrapper.vm.viewer).toBe(RICH_BLOB_VIEWER);
expect(wrapper.vm.$emit).toHaveBeenCalledWith('switch-viewer', RICH_BLOB_VIEWER);
});
});
}); });
}); });
...@@ -22,6 +22,7 @@ exports[`Contributors charts should render charts when loading completed and the ...@@ -22,6 +22,7 @@ exports[`Contributors charts should render charts when loading completed and the
legendmaxtext="Max" legendmaxtext="Max"
option="[object Object]" option="[object Object]"
thresholds="" thresholds=""
width="0"
/> />
</div> </div>
...@@ -29,7 +30,7 @@ exports[`Contributors charts should render charts when loading completed and the ...@@ -29,7 +30,7 @@ exports[`Contributors charts should render charts when loading completed and the
class="row" class="row"
> >
<div <div
class="col-6" class="col-lg-6 col-12"
> >
<h4> <h4>
John John
...@@ -39,15 +40,18 @@ exports[`Contributors charts should render charts when loading completed and the ...@@ -39,15 +40,18 @@ exports[`Contributors charts should render charts when loading completed and the
2 commits (jawnnypoo@gmail.com) 2 commits (jawnnypoo@gmail.com)
</p> </p>
<glareachart-stub <div>
data="[object Object]" <glareachart-stub
height="216" data="[object Object]"
includelegendavgmax="true" height="216"
legendaveragetext="Avg" includelegendavgmax="true"
legendmaxtext="Max" legendaveragetext="Avg"
option="[object Object]" legendmaxtext="Max"
thresholds="" option="[object Object]"
/> thresholds=""
width="0"
/>
</div>
</div> </div>
</div> </div>
</div> </div>
......
import Vue from 'vue'; import Vue from 'vue';
import { shallowMount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { createStore } from '~/contributors/stores'; import { createStore } from '~/contributors/stores';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
...@@ -22,7 +22,7 @@ function factory() { ...@@ -22,7 +22,7 @@ function factory() {
mock.onGet().reply(200, chartData); mock.onGet().reply(200, chartData);
store = createStore(); store = createStore();
wrapper = shallowMount(Component, { wrapper = mount(Component, {
propsData: { propsData: {
endpoint, endpoint,
branch, branch,
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PipelinesAreaChart matches the snapshot 1`] = `
<div
class="prepend-top-default"
>
<p>
Some title
</p>
<div>
<glareachart-stub
data="[object Object],[object Object]"
height="300"
legendaveragetext="Avg"
legendmaxtext="Max"
option="[object Object]"
thresholds=""
width="0"
/>
</div>
</div>
`;
...@@ -2,7 +2,14 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,7 +2,14 @@ import { shallowMount } from '@vue/test-utils';
import { GlColumnChart } from '@gitlab/ui/dist/charts'; import { GlColumnChart } from '@gitlab/ui/dist/charts';
import Component from '~/projects/pipelines/charts/components/app.vue'; import Component from '~/projects/pipelines/charts/components/app.vue';
import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue'; import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
import { counts, timesChartData } from '../mock_data'; import PipelinesAreaChart from '~/projects/pipelines/charts/components/pipelines_area_chart.vue';
import {
counts,
timesChartData,
areaChartData as lastWeekChartData,
areaChartData as lastMonthChartData,
lastYearChartData,
} from '../mock_data';
describe('ProjectsPipelinesChartsApp', () => { describe('ProjectsPipelinesChartsApp', () => {
let wrapper; let wrapper;
...@@ -12,6 +19,9 @@ describe('ProjectsPipelinesChartsApp', () => { ...@@ -12,6 +19,9 @@ describe('ProjectsPipelinesChartsApp', () => {
propsData: { propsData: {
counts, counts,
timesChartData, timesChartData,
lastWeekChartData,
lastMonthChartData,
lastYearChartData,
}, },
}); });
}); });
...@@ -39,4 +49,24 @@ describe('ProjectsPipelinesChartsApp', () => { ...@@ -39,4 +49,24 @@ describe('ProjectsPipelinesChartsApp', () => {
expect(chart.props('option')).toBe(wrapper.vm.$options.timesChartOptions); expect(chart.props('option')).toBe(wrapper.vm.$options.timesChartOptions);
}); });
}); });
describe('pipelines charts', () => {
it('displays 3 area charts', () => {
expect(wrapper.findAll(PipelinesAreaChart).length).toBe(3);
});
describe('displays individual correctly', () => {
it('renders with the correct data', () => {
const charts = wrapper.findAll(PipelinesAreaChart);
for (let i = 0; i < charts.length; i += 1) {
const chart = charts.at(i);
expect(chart.exists()).toBeTruthy();
expect(chart.props('chartData')).toBe(wrapper.vm.areaCharts[i].data);
expect(chart.text()).toBe(wrapper.vm.areaCharts[i].title);
}
});
});
});
}); });
import { mount } from '@vue/test-utils';
import Component from '~/projects/pipelines/charts/components/pipelines_area_chart.vue';
import { transformedAreaChartData } from '../mock_data';
describe('PipelinesAreaChart', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(Component, {
propsData: {
chartData: transformedAreaChartData,
},
slots: {
default: 'Some title',
},
stubs: {
GlAreaChart: true,
},
});
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
...@@ -9,3 +9,25 @@ export const timesChartData = { ...@@ -9,3 +9,25 @@ export const timesChartData = {
labels: ['as1234', 'kh423hy', 'ji56bvg', 'th23po'], labels: ['as1234', 'kh423hy', 'ji56bvg', 'th23po'],
values: [5, 3, 7, 4], values: [5, 3, 7, 4],
}; };
export const areaChartData = {
labels: ['01 Jan', '02 Jan', '03 Jan', '04 Jan', '05 Jan'],
totals: [4, 6, 3, 6, 7],
success: [3, 5, 3, 3, 5],
};
export const lastYearChartData = {
...areaChartData,
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
};
export const transformedAreaChartData = [
{
name: 'all',
data: [['01 Jan', 4], ['02 Jan', 6], ['03 Jan', 3], ['04 Jan', 6], ['05 Jan', 7]],
},
{
name: 'success',
data: [['01 Jan', 3], ['02 Jan', 3], ['03 Jan', 3], ['04 Jan', 3], ['05 Jan', 5]],
},
];
...@@ -18,6 +18,7 @@ exports[`Expiration Policy Form renders 1`] = ` ...@@ -18,6 +18,7 @@ exports[`Expiration Policy Form renders 1`] = `
id="expiration-policy-toggle" id="expiration-policy-toggle"
labeloff="Toggle Status: OFF" labeloff="Toggle Status: OFF"
labelon="Toggle Status: ON" labelon="Toggle Status: ON"
labelposition="hidden"
/> />
<span <span
......
...@@ -48,3 +48,52 @@ exports[`Repository table row component renders table row 1`] = ` ...@@ -48,3 +48,52 @@ exports[`Repository table row component renders table row 1`] = `
</td> </td>
</tr> </tr>
`; `;
exports[`Repository table row component renders table row for path with special character 1`] = `
<tr
class="tree-item file_1"
>
<td
class="tree-item-file-name"
>
<i
aria-label="file"
class="fa fa-fw fa-file-text-o"
role="img"
/>
<a
class="str-truncated"
href="https://test.com"
>
test
</a>
<!---->
<!---->
<!---->
</td>
<td
class="d-none d-sm-table-cell tree-commit"
>
<gl-skeleton-loading-stub
class="h-auto"
lines="1"
/>
</td>
<td
class="tree-time-ago text-right"
>
<gl-skeleton-loading-stub
class="ml-auto h-auto w-50"
lines="1"
/>
</td>
</tr>
`;
...@@ -51,6 +51,20 @@ describe('Repository table row component', () => { ...@@ -51,6 +51,20 @@ describe('Repository table row component', () => {
}); });
}); });
it('renders table row for path with special character', () => {
factory({
id: '1',
sha: '123',
path: 'test$/test',
type: 'file',
currentPath: 'test$',
});
return vm.vm.$nextTick().then(() => {
expect(vm.element).toMatchSnapshot();
});
});
it.each` it.each`
type | component | componentName type | component | componentName
${'tree'} | ${RouterLinkStub} | ${'RouterLink'} ${'tree'} | ${RouterLinkStub} | ${'RouterLink'}
......
...@@ -49,6 +49,7 @@ exports[`self monitor component When the self monitor project has not been creat ...@@ -49,6 +49,7 @@ exports[`self monitor component When the self monitor project has not been creat
<gl-toggle-stub <gl-toggle-stub
labeloff="Toggle Status: OFF" labeloff="Toggle Status: OFF"
labelon="Toggle Status: ON" labelon="Toggle Status: ON"
labelposition="hidden"
name="self-monitor-toggle" name="self-monitor-toggle"
/> />
</gl-form-group-stub> </gl-form-group-stub>
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue'; import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue'; import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import { import {
SNIPPET_VISIBILITY_PRIVATE, SNIPPET_VISIBILITY_PRIVATE,
...@@ -15,7 +17,15 @@ describe('Blob Embeddable', () => { ...@@ -15,7 +17,15 @@ describe('Blob Embeddable', () => {
visibilityLevel: SNIPPET_VISIBILITY_PUBLIC, visibilityLevel: SNIPPET_VISIBILITY_PUBLIC,
}; };
function createComponent(props = {}) { function createComponent(props = {}, loading = false) {
const $apollo = {
queries: {
blob: {
loading,
},
},
};
wrapper = shallowMount(SnippetBlobView, { wrapper = shallowMount(SnippetBlobView, {
propsData: { propsData: {
snippet: { snippet: {
...@@ -23,32 +33,44 @@ describe('Blob Embeddable', () => { ...@@ -23,32 +33,44 @@ describe('Blob Embeddable', () => {
...props, ...props,
}, },
}, },
mocks: { $apollo },
}); });
wrapper.vm.$apollo.queries.blob.loading = false;
} }
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('renders blob-embeddable component', () => { describe('rendering', () => {
createComponent(); it('renders correct components', () => {
expect(wrapper.find(BlobEmbeddable).exists()).toBe(true); createComponent();
}); expect(wrapper.find(BlobEmbeddable).exists()).toBe(true);
expect(wrapper.find(BlobHeader).exists()).toBe(true);
it('does not render blob-embeddable for internal snippet', () => {
createComponent({
visibilityLevel: SNIPPET_VISIBILITY_INTERNAL,
}); });
expect(wrapper.find(BlobEmbeddable).exists()).toBe(false);
createComponent({ it.each([SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PRIVATE, 'foo'])(
visibilityLevel: SNIPPET_VISIBILITY_PRIVATE, 'does not render blob-embeddable by default',
visibilityLevel => {
createComponent({
visibilityLevel,
});
expect(wrapper.find(BlobEmbeddable).exists()).toBe(false);
},
);
it('does render blob-embeddable for public snippet', () => {
createComponent({
visibilityLevel: SNIPPET_VISIBILITY_PUBLIC,
});
expect(wrapper.find(BlobEmbeddable).exists()).toBe(true);
}); });
expect(wrapper.find(BlobEmbeddable).exists()).toBe(false);
createComponent({ it('shows loading icon while blob data is in flight', () => {
visibilityLevel: 'foo', createComponent({}, true);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.find('.snippet-file-content').exists()).toBe(false);
}); });
expect(wrapper.find(BlobEmbeddable).exists()).toBe(false);
}); });
}); });
...@@ -10,7 +10,8 @@ describe GitlabSchema.types['Commit'] do ...@@ -10,7 +10,8 @@ describe GitlabSchema.types['Commit'] do
it 'contains attributes related to commit' do it 'contains attributes related to commit' do
expect(described_class).to have_graphql_fields( expect(described_class).to have_graphql_fields(
:id, :sha, :title, :description, :message, :authored_date, :id, :sha, :title, :description, :message, :authored_date,
:author_name, :author, :web_url, :latest_pipeline, :pipelines, :signature_html :author_name, :author_gravatar, :author, :web_url, :latest_pipeline,
:pipelines, :signature_html
) )
end end
end end
...@@ -2953,6 +2953,30 @@ describe Ci::Pipeline, :mailer do ...@@ -2953,6 +2953,30 @@ describe Ci::Pipeline, :mailer do
create(:ci_sources_pipeline, pipeline: pipeline, source_job: bridge) create(:ci_sources_pipeline, pipeline: pipeline, source_job: bridge)
end end
context 'when downstream pipeline status transitions to pending' do
it 'updates bridge status ' do
expect(pipeline).to receive(:update_bridge_status!).once
pipeline.run!
end
end
context 'when the status of downstream pipeline transitions to waiting_for_resource' do
it 'updates bridge status ' do
expect(pipeline).to receive(:update_bridge_status!).once
pipeline.request_resource!
end
end
context 'when the status of downstream pipeline transitions to failed' do
it 'does not update bridge status ' do
expect(pipeline).not_to receive(:update_bridge_status!)
pipeline.drop!
end
end
describe '#bridge_triggered?' do describe '#bridge_triggered?' do
it 'is a pipeline triggered by a bridge' do it 'is a pipeline triggered by a bridge' do
expect(pipeline).to be_bridge_triggered expect(pipeline).to be_bridge_triggered
......
...@@ -740,10 +740,10 @@ ...@@ -740,10 +740,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.96.0.tgz#1d32730389e94358dc245e8336912523446d1269" resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.96.0.tgz#1d32730389e94358dc245e8336912523446d1269"
integrity sha512-mhg6kndxDhwjWChKhs5utO6PowlOyFdaCXUrkkxxe2H3cd8DYa40QOEcJeUrSIhkmgIMVesUawesx5tt4Bnnnw== integrity sha512-mhg6kndxDhwjWChKhs5utO6PowlOyFdaCXUrkkxxe2H3cd8DYa40QOEcJeUrSIhkmgIMVesUawesx5tt4Bnnnw==
"@gitlab/ui@^9.4.1": "@gitlab/ui@^9.6.0":
version "9.4.1" version "9.6.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-9.4.1.tgz#c4128ac07e1d6e4367a1c7a38dbee0aed1a2ae23" resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-9.6.0.tgz#13119a56a34be34fd07e761cab0af3c00462159d"
integrity sha512-Xti1dKWhwzL/3sXdMU2z9P6Liip9UElAHXfAXBnRTEPO3JONhdbwbVXrLnCQzKhkJ6qEaM3cJiC9oIeFhlO/sw== integrity sha512-R0pUa30l/JX/+1K/rZGAjDvCLLoQuodwCxBNzQ5U1ylnnfGclVrM2rBlZT3UlWnMkb9BRhTPn6uoC/HBOAo37g==
dependencies: dependencies:
"@babel/standalone" "^7.0.0" "@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0" "@gitlab/vue-toasted" "^1.3.0"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment