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>
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 {
components: {
......@@ -16,6 +23,11 @@ export default {
type: Object,
required: true,
},
activeViewer: {
type: String,
default: SIMPLE_BLOB_VIEWER,
required: false,
},
},
computed: {
rawUrl() {
......@@ -24,10 +36,13 @@ export default {
downloadUrl() {
return `${this.blob.rawPath}?inline=false`;
},
copyDisabled() {
return this.activeViewer === RICH_BLOB_VIEWER;
},
},
methods: {
requestCopyContents() {
this.$emit('copy');
eventHub.$emit('copy');
},
},
BTN_COPY_CONTENTS_TITLE,
......@@ -41,6 +56,7 @@ export default {
v-gl-tooltip.hover
:aria-label="$options.BTN_COPY_CONTENTS_TITLE"
:title="$options.BTN_COPY_CONTENTS_TITLE"
:disabled="copyDisabled"
@click="requestCopyContents"
>
<gl-icon name="copy-to-clipboard" :size="14" />
......
......@@ -6,6 +6,7 @@ import {
SIMPLE_BLOB_VIEWER,
SIMPLE_BLOB_VIEWER_TITLE,
} from './constants';
import eventHub from '../event_hub';
export default {
components: {
......@@ -21,25 +22,24 @@ export default {
type: Object,
required: true,
},
},
data() {
return {
viewer: this.blob.richViewer ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
};
activeViewer: {
type: String,
default: SIMPLE_BLOB_VIEWER,
required: false,
},
},
computed: {
isSimpleViewer() {
return this.viewer === SIMPLE_BLOB_VIEWER;
return this.activeViewer === SIMPLE_BLOB_VIEWER;
},
isRichViewer() {
return this.viewer === RICH_BLOB_VIEWER;
return this.activeViewer === RICH_BLOB_VIEWER;
},
},
methods: {
switchToViewer(viewer) {
if (viewer !== this.viewer) {
this.viewer = viewer;
this.$emit('switch-viewer', viewer);
if (viewer !== this.activeViewer) {
eventHub.$emit('switch-viewer', viewer);
}
},
},
......
import Vue from 'vue';
export default new Vue();
......@@ -7,11 +7,13 @@ import { __ } from '~/locale';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import { getDatesInRange } from '~/lib/utils/datetime_utility';
import { xAxisLabelFormatter, dateFormatter } from '../utils';
import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
export default {
components: {
GlAreaChart,
GlLoadingIcon,
ResizableChartContainer,
},
props: {
endpoint: {
......@@ -201,25 +203,35 @@ export default {
<div v-else-if="showChart" class="contributors-charts">
<h4>{{ __('Commits to') }} {{ branch }}</h4>
<span>{{ __('Excluding merge commits. Limited to 6,000 commits.') }}</span>
<div>
<resizable-chart-container>
<gl-area-chart
slot-scope="{ width }"
:width="width"
:data="masterChartData"
:option="masterChartOptions"
:height="masterChartHeight"
@created="onMasterChartCreated"
/>
</div>
</resizable-chart-container>
<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>
<p>{{ n__('%d commit', '%d commits', contributor.commits) }} ({{ contributor.email }})</p>
<gl-area-chart
:data="contributor.dates"
:option="individualChartOptions"
:height="individualChartHeight"
@created="onIndividualChartCreated"
/>
<resizable-chart-container>
<gl-area-chart
slot-scope="{ width }"
:width="width"
:data="contributor.dates"
:option="individualChartOptions"
:height="individualChartHeight"
@created="onIndividualChartCreated"
/>
</resizable-chart-container>
</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';
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);
<script>
import dateFormat from 'dateformat';
import { __, sprintf } from '~/locale';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import StatisticsList from './statistics_list.vue';
import PipelinesAreaChart from './pipelines_area_chart.vue';
import {
CHART_CONTAINER_HEIGHT,
INNER_CHART_HEIGHT,
X_AXIS_LABEL_ROTATION,
X_AXIS_TITLE_OFFSET,
CHART_DATE_FORMAT,
ONE_WEEK_AGO_DAYS,
ONE_MONTH_AGO_DAYS,
} from '../constants';
export default {
components: {
StatisticsList,
GlColumnChart,
PipelinesAreaChart,
},
props: {
counts: {
......@@ -22,6 +30,18 @@ export default {
type: Object,
required: true,
},
lastWeekChartData: {
type: Object,
required: true,
},
lastMonthChartData: {
type: Object,
required: true,
},
lastYearChartData: {
type: Object,
required: true,
},
},
data() {
return {
......@@ -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: {
mergeLabelsAndValues(labels, values) {
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,
timesChartOptions: {
......@@ -45,6 +93,22 @@ export default {
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>
<template>
......@@ -68,5 +132,14 @@ export default {
/>
</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>
</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;
export const X_AXIS_LABEL_ROTATION = 45;
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 () => {
successRatio,
timesChartLabels,
timesChartValues,
lastWeekChartLabels,
lastWeekChartTotals,
lastWeekChartSuccess,
lastMonthChartLabels,
lastMonthChartTotals,
lastMonthChartSuccess,
lastYearChartLabels,
lastYearChartTotals,
lastYearChartSuccess,
} = el.dataset;
const parseAreaChartData = (labels, totals, success) => ({
labels: JSON.parse(labels),
totals: JSON.parse(totals),
success: JSON.parse(success),
});
return new Vue({
el,
name: 'ProjectPipelinesChartsApp',
......@@ -31,6 +46,21 @@ export default () => {
labels: JSON.parse(timesChartLabels),
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 {
class="avatar-cell"
/>
<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>
<div class="commit-detail flex-list">
<div class="commit-content qa-commit-content">
......
<script>
import { escapeRegExp } from 'lodash';
import { GlBadge, GlLink, GlSkeletonLoading, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
......@@ -105,7 +106,7 @@ export default {
return this.isFolder ? 'router-link' : 'a';
},
fullPath() {
return this.path.replace(new RegExp(`^${this.currentPath}/`), '');
return this.path.replace(new RegExp(`^${escapeRegExp(this.currentPath)}/`), '');
},
shortSha() {
return this.sha.slice(0, 8);
......
......@@ -48,7 +48,7 @@ const defaultClient = createDefaultClient(
case 'TreeEntry':
case 'Submodule':
case 'Blob':
return `${obj.flatPath}-${obj.id}`;
return `${escape(obj.flatPath)}-${obj.id}`;
default:
// If the type doesn't match any of the above we fallback
// to using the default Apollo ID
......
......@@ -10,6 +10,7 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
webUrl
authoredDate
authorName
authorGravatar
author {
name
avatarUrl
......
<script>
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
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 {
components: {
BlobEmbeddable,
BlobHeader,
GlLoadingIcon,
},
apollo: {
blob: {
query: GetSnippetBlobQuery,
variables() {
return {
ids: this.snippet.id,
};
},
update: data => data.snippets.edges[0].node.blob,
},
},
props: {
snippet: {
......@@ -12,15 +28,32 @@ export default {
required: true,
},
},
data() {
return {
blob: {},
};
},
computed: {
embeddable() {
return this.snippet.visibilityLevel === SNIPPET_VISIBILITY_PUBLIC;
},
isBlobLoading() {
return this.$apollo.queries.blob.loading;
},
},
};
</script>
<template>
<div>
<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>
</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
end
def bulk_destroy
unless params[:ids].present?
head :bad_request
return
end
tag_names = params[:ids] || []
tag_names = params.require(:ids) || []
if tag_names.size > LIMIT
head :bad_request
return
......
......@@ -117,8 +117,10 @@ class RegistrationsController < Devise::RegistrationsController
end
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)
dashboard_projects_path
users_almost_there_path
end
private
......
......@@ -26,6 +26,11 @@ module Types
description: 'Rendered HTML of the commit signature'
field :author_name, type: GraphQL::STRING_TYPE, null: true,
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
field :author, type: Types::UserType, null: true,
......
......@@ -216,7 +216,7 @@ module Ci
end
end
after_transition created: :pending do |pipeline|
after_transition created: any - [:failed] do |pipeline|
next unless pipeline.bridge_triggered?
next if pipeline.bridge_waiting?
......
- 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 } } }
#charts.ci-charts
%hr
= render 'projects/pipelines/charts/pipelines'
#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 },
last_week_chart: { labels: @charts[:week].labels, totals: @charts[:week].total, success: @charts[:week].success },
last_month_chart: { labels: @charts[:month].labels, totals: @charts[:month].total, success: @charts[:month].success },
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 {
"""
author: User
"""
Commit authors gravatar
"""
authorGravatar: String
"""
Commit authors name
"""
......
......@@ -13259,6 +13259,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "authorGravatar",
"description": "Commit authors gravatar",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "authorName",
"description": "Commit authors name",
......
......@@ -54,6 +54,7 @@ An emoji awarded by a user.
| Name | Type | Description |
| --- | ---- | ---------- |
| `author` | User | Author of the commit |
| `authorGravatar` | String | Commit authors gravatar |
| `authorName` | String | Commit authors name |
| `authoredDate` | Time | Timestamp of when the commit was authored |
| `description` | String | Description of the commit message |
......
......@@ -11419,6 +11419,9 @@ msgstr ""
msgid "Live preview"
msgstr ""
msgid "Loading blob"
msgstr ""
msgid "Loading contribution stats for group members"
msgstr ""
......@@ -13672,10 +13675,10 @@ msgstr ""
msgid "Pipelines emails"
msgstr ""
msgid "Pipelines for last month"
msgid "Pipelines for last month (%{oneMonthAgo} - %{today})"
msgstr ""
msgid "Pipelines for last week"
msgid "Pipelines for last week (%{oneWeekAgo} - %{today})"
msgstr ""
msgid "Pipelines for last year"
......@@ -13759,6 +13762,9 @@ msgstr ""
msgid "Pipeline|Coverage"
msgstr ""
msgid "Pipeline|Date"
msgstr ""
msgid "Pipeline|Detached merge request pipeline"
msgstr ""
......@@ -13780,6 +13786,9 @@ msgstr ""
msgid "Pipeline|Pipeline"
msgstr ""
msgid "Pipeline|Pipelines"
msgstr ""
msgid "Pipeline|Run Pipeline"
msgstr ""
......@@ -13816,18 +13825,12 @@ msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
msgstr ""
msgid "Pipeline|all"
msgstr ""
msgid "Pipeline|for"
msgstr ""
msgid "Pipeline|on"
msgstr ""
msgid "Pipeline|success"
msgstr ""
msgid "Pipeline|with stage"
msgstr ""
......
......@@ -38,7 +38,9 @@ module QA
def visit_saml_sso_settings(group, direct: false)
if direct
page.visit "#{group.web_url}/-/saml"
url = "#{group.web_url}/-/saml"
Runtime::Logger.debug("Visiting url \"#{url}\" directly")
page.visit url
else
group.visit!
......
......@@ -48,6 +48,12 @@ module QA
feature && feature["state"] == "on"
end
def get_features
request = Runtime::API::Request.new(api_client, "/features")
response = get(request.url)
response.body
end
private
def api_client
......@@ -76,12 +82,6 @@ module QA
raise SetFeatureError, "Setting feature flag #{key} to #{value} failed with `#{response}`."
end
end
def get_features
request = Runtime::API::Request.new(api_client, "/features")
response = get(request.url)
response.body
end
end
end
end
......@@ -77,14 +77,32 @@ describe RegistrationsController do
context 'when send_user_confirmation_email is true' do
before do
stub_application_setting(send_user_confirmation_email: true)
allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days
end
it 'authenticates the user and sends a confirmation email' do
post(:create, params: user_params)
context 'when a grace period is active for confirming the email address' do
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])
expect(response).to redirect_to(dashboard_projects_path)
context 'when no grace period is active for confirming the email address' do
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
......
// 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 {
BTN_COPY_CONTENTS_TITLE,
BTN_DOWNLOAD_TITLE,
BTN_RAW_TITLE,
RICH_BLOB_VIEWER,
} from '~/blob/components/constants';
import { GlButtonGroup, GlButton } from '@gitlab/ui';
import { Blob } from './mock_data';
import eventHub from '~/blob/event_hub';
describe('Blob Header Default Actions', () => {
let wrapper;
......@@ -14,10 +16,11 @@ describe('Blob Header Default Actions', () => {
let buttons;
const hrefPrefix = 'http://localhost';
function createComponent(props = {}) {
function createComponent(blobProps = {}, propsData = {}) {
wrapper = mount(BlobHeaderActions, {
propsData: {
blob: Object.assign({}, Blob, props),
blob: Object.assign({}, Blob, blobProps),
...propsData,
},
});
}
......@@ -51,14 +54,30 @@ describe('Blob Header Default Actions', () => {
it('correct href attribute on Download button', () => {
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', () => {
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');
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 {
} from '~/blob/components/constants';
import { GlButtonGroup, GlButton } from '@gitlab/ui';
import { Blob } from './mock_data';
import eventHub from '~/blob/event_hub';
describe('Blob Header Viewer Switcher', () => {
let wrapper;
function createComponent(props = {}) {
function createComponent(blobProps = {}, propsData = {}) {
wrapper = mount(BlobHeaderViewerSwitcher, {
propsData: {
blob: Object.assign({}, Blob, props),
blob: Object.assign({}, Blob, blobProps),
...propsData,
},
});
}
......@@ -25,14 +27,9 @@ describe('Blob Header Viewer Switcher', () => {
});
describe('intiialization', () => {
it('is initialized with rich viewer as preselected when richViewer exists', () => {
it('is initialized with simple viewer as active', () => {
createComponent();
expect(wrapper.vm.viewer).toBe(RICH_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);
expect(wrapper.vm.activeViewer).toBe(SIMPLE_BLOB_VIEWER);
});
});
......@@ -63,47 +60,43 @@ describe('Blob Header Viewer Switcher', () => {
let simpleBtn;
let richBtn;
beforeEach(() => {
createComponent();
function factory(propsOptions = {}) {
createComponent({}, propsOptions);
buttons = wrapper.findAll(GlButton);
simpleBtn = buttons.at(0);
richBtn = buttons.at(1);
});
jest.spyOn(eventHub, '$emit');
}
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');
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', () => {
jest.spyOn(wrapper.vm, '$emit');
factory({
activeViewer: RICH_BLOB_VIEWER,
});
simpleBtn.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.viewer).toBe(SIMPLE_BLOB_VIEWER);
expect(wrapper.vm.$emit).toHaveBeenCalledWith('switch-viewer', SIMPLE_BLOB_VIEWER);
expect(eventHub.$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
legendmaxtext="Max"
option="[object Object]"
thresholds=""
width="0"
/>
</div>
......@@ -29,7 +30,7 @@ exports[`Contributors charts should render charts when loading completed and the
class="row"
>
<div
class="col-6"
class="col-lg-6 col-12"
>
<h4>
John
......@@ -39,15 +40,18 @@ exports[`Contributors charts should render charts when loading completed and the
2 commits (jawnnypoo@gmail.com)
</p>
<glareachart-stub
data="[object Object]"
height="216"
includelegendavgmax="true"
legendaveragetext="Avg"
legendmaxtext="Max"
option="[object Object]"
thresholds=""
/>
<div>
<glareachart-stub
data="[object Object]"
height="216"
includelegendavgmax="true"
legendaveragetext="Avg"
legendmaxtext="Max"
option="[object Object]"
thresholds=""
width="0"
/>
</div>
</div>
</div>
</div>
......
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { createStore } from '~/contributors/stores';
import axios from '~/lib/utils/axios_utils';
......@@ -22,7 +22,7 @@ function factory() {
mock.onGet().reply(200, chartData);
store = createStore();
wrapper = shallowMount(Component, {
wrapper = mount(Component, {
propsData: {
endpoint,
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';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import Component from '~/projects/pipelines/charts/components/app.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', () => {
let wrapper;
......@@ -12,6 +19,9 @@ describe('ProjectsPipelinesChartsApp', () => {
propsData: {
counts,
timesChartData,
lastWeekChartData,
lastMonthChartData,
lastYearChartData,
},
});
});
......@@ -39,4 +49,24 @@ describe('ProjectsPipelinesChartsApp', () => {
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 = {
labels: ['as1234', 'kh423hy', 'ji56bvg', 'th23po'],
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`] = `
id="expiration-policy-toggle"
labeloff="Toggle Status: OFF"
labelon="Toggle Status: ON"
labelposition="hidden"
/>
<span
......
......@@ -48,3 +48,52 @@ exports[`Repository table row component renders table row 1`] = `
</td>
</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', () => {
});
});
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`
type | component | componentName
${'tree'} | ${RouterLinkStub} | ${'RouterLink'}
......
......@@ -49,6 +49,7 @@ exports[`self monitor component When the self monitor project has not been creat
<gl-toggle-stub
labeloff="Toggle Status: OFF"
labelon="Toggle Status: ON"
labelposition="hidden"
name="self-monitor-toggle"
/>
</gl-form-group-stub>
......
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
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 {
SNIPPET_VISIBILITY_PRIVATE,
......@@ -15,7 +17,15 @@ describe('Blob Embeddable', () => {
visibilityLevel: SNIPPET_VISIBILITY_PUBLIC,
};
function createComponent(props = {}) {
function createComponent(props = {}, loading = false) {
const $apollo = {
queries: {
blob: {
loading,
},
},
};
wrapper = shallowMount(SnippetBlobView, {
propsData: {
snippet: {
......@@ -23,32 +33,44 @@ describe('Blob Embeddable', () => {
...props,
},
},
mocks: { $apollo },
});
wrapper.vm.$apollo.queries.blob.loading = false;
}
afterEach(() => {
wrapper.destroy();
});
it('renders blob-embeddable component', () => {
createComponent();
expect(wrapper.find(BlobEmbeddable).exists()).toBe(true);
});
it('does not render blob-embeddable for internal snippet', () => {
createComponent({
visibilityLevel: SNIPPET_VISIBILITY_INTERNAL,
describe('rendering', () => {
it('renders correct components', () => {
createComponent();
expect(wrapper.find(BlobEmbeddable).exists()).toBe(true);
expect(wrapper.find(BlobHeader).exists()).toBe(true);
});
expect(wrapper.find(BlobEmbeddable).exists()).toBe(false);
createComponent({
visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
it.each([SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PRIVATE, 'foo'])(
'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({
visibilityLevel: 'foo',
it('shows loading icon while blob data is in flight', () => {
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
it 'contains attributes related to commit' do
expect(described_class).to have_graphql_fields(
: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
......@@ -2953,6 +2953,30 @@ describe Ci::Pipeline, :mailer do
create(:ci_sources_pipeline, pipeline: pipeline, source_job: bridge)
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
it 'is a pipeline triggered by a bridge' do
expect(pipeline).to be_bridge_triggered
......
......@@ -740,10 +740,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.96.0.tgz#1d32730389e94358dc245e8336912523446d1269"
integrity sha512-mhg6kndxDhwjWChKhs5utO6PowlOyFdaCXUrkkxxe2H3cd8DYa40QOEcJeUrSIhkmgIMVesUawesx5tt4Bnnnw==
"@gitlab/ui@^9.4.1":
version "9.4.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-9.4.1.tgz#c4128ac07e1d6e4367a1c7a38dbee0aed1a2ae23"
integrity sha512-Xti1dKWhwzL/3sXdMU2z9P6Liip9UElAHXfAXBnRTEPO3JONhdbwbVXrLnCQzKhkJ6qEaM3cJiC9oIeFhlO/sw==
"@gitlab/ui@^9.6.0":
version "9.6.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-9.6.0.tgz#13119a56a34be34fd07e761cab0af3c00462159d"
integrity sha512-R0pUa30l/JX/+1K/rZGAjDvCLLoQuodwCxBNzQ5U1ylnnfGclVrM2rBlZT3UlWnMkb9BRhTPn6uoC/HBOAo37g==
dependencies:
"@babel/standalone" "^7.0.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