summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue76
-rw-r--r--app/assets/javascripts/blob/components/blob_header_default_actions.vue20
-rw-r--r--app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue20
-rw-r--r--app/assets/javascripts/blob/event_hub.js3
-rw-r--r--app/assets/javascripts/contributors/components/contributors.vue30
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/blobviewer.fragment.graphql6
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/charts/index.js55
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue73
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/pipelines_area_chart.vue46
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/constants.js6
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/index.js30
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue7
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue3
-rw-r--r--app/assets/javascripts/repository/graphql.js2
-rw-r--r--app/assets/javascripts/repository/queries/pathLastCommit.query.graphql1
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_view.vue33
-rw-r--r--app/assets/javascripts/snippets/queries/snippet.blob.query.graphql24
-rw-r--r--app/controllers/projects/registry/tags_controller.rb7
-rw-r--r--app/controllers/registrations_controller.rb4
-rw-r--r--app/graphql/types/commit_type.rb5
-rw-r--r--app/models/ci/pipeline.rb2
-rw-r--r--app/views/projects/pipelines/charts.html.haml10
-rw-r--r--app/views/projects/pipelines/charts/_pipelines.haml37
-rw-r--r--changelogs/unreleased/196183-convert-pipelines-last-week-chart-to-echarts.yml5
-rw-r--r--changelogs/unreleased/fix-upstream-bridge-stuck-when-non-pending-pipelines.yml5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json14
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--locale/gitlab.pot19
-rw-r--r--package.json2
-rw-r--r--qa/qa/flow/saml.rb4
-rw-r--r--qa/qa/runtime/feature.rb12
-rw-r--r--spec/controllers/registrations_controller_spec.rb28
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap25
-rw-r--r--spec/frontend/blob/components/blob_header_default_actions_spec.js27
-rw-r--r--spec/frontend/blob/components/blob_header_spec.js133
-rw-r--r--spec/frontend/blob/components/blob_header_viewer_switcher_spec.js65
-rw-r--r--spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap24
-rw-r--r--spec/frontend/contributors/component/contributors_spec.js4
-rw-r--r--spec/frontend/projects/pipelines/charts/components/__snapshots__/pipelines_area_chart_spec.js.snap23
-rw-r--r--spec/frontend/projects/pipelines/charts/components/app_spec.js32
-rw-r--r--spec/frontend/projects/pipelines/charts/components/pipelines_area_chart_spec.js30
-rw-r--r--spec/frontend/projects/pipelines/charts/mock_data.js22
-rw-r--r--spec/frontend/registry/shared/components/__snapshots__/expiration_policy_fields_spec.js.snap1
-rw-r--r--spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap49
-rw-r--r--spec/frontend/repository/components/table/row_spec.js14
-rw-r--r--spec/frontend/self_monitor/components/__snapshots__/self_monitor_spec.js.snap1
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js54
-rw-r--r--spec/graphql/types/commit_type_spec.rb3
-rw-r--r--spec/models/ci/pipeline_spec.rb24
-rw-r--r--yarn.lock8
51 files changed, 910 insertions, 224 deletions
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
new file mode 100644
index 00000000000..61a66513838
--- /dev/null
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -0,0 +1,76 @@
+<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>
diff --git a/app/assets/javascripts/blob/components/blob_header_default_actions.vue b/app/assets/javascripts/blob/components/blob_header_default_actions.vue
index b0522c08a65..e526fae0dba 100644
--- a/app/assets/javascripts/blob/components/blob_header_default_actions.vue
+++ b/app/assets/javascripts/blob/components/blob_header_default_actions.vue
@@ -1,6 +1,13 @@
<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" />
diff --git a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
index 7acdd574359..13ea87c99b1 100644
--- a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
+++ b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
@@ -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);
}
},
},
diff --git a/app/assets/javascripts/blob/event_hub.js b/app/assets/javascripts/blob/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/blob/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/contributors/components/contributors.vue b/app/assets/javascripts/contributors/components/contributors.vue
index fb7000ee9ed..caad2a835fa 100644
--- a/app/assets/javascripts/contributors/components/contributors.vue
+++ b/app/assets/javascripts/contributors/components/contributors.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>
diff --git a/app/assets/javascripts/graphql_shared/fragments/blobviewer.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/blobviewer.fragment.graphql
new file mode 100644
index 00000000000..64c894df115
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/fragments/blobviewer.fragment.graphql
@@ -0,0 +1,6 @@
+fragment BlobViewer on SnippetBlobViewer {
+ collapsed
+ loadingPartialName
+ renderError
+ tooLarge
+}
diff --git a/app/assets/javascripts/pages/projects/pipelines/charts/index.js b/app/assets/javascripts/pages/projects/pipelines/charts/index.js
index 2e7af11c39e..d77b84a3b24 100644
--- a/app/assets/javascripts/pages/projects/pipelines/charts/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/charts/index.js
@@ -1,58 +1,3 @@
-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);
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
index 4bd72c405ee..4dc1c512689 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
@@ -1,17 +1,25 @@
<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>
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipelines_area_chart.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipelines_area_chart.vue
new file mode 100644
index 00000000000..d701f238a2e
--- /dev/null
+++ b/app/assets/javascripts/projects/pipelines/charts/components/pipelines_area_chart.vue
@@ -0,0 +1,46 @@
+<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>
diff --git a/app/assets/javascripts/projects/pipelines/charts/constants.js b/app/assets/javascripts/projects/pipelines/charts/constants.js
index eeb29370e51..5dbe3c01100 100644
--- a/app/assets/javascripts/projects/pipelines/charts/constants.js
+++ b/app/assets/javascripts/projects/pipelines/charts/constants.js
@@ -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';
diff --git a/app/assets/javascripts/projects/pipelines/charts/index.js b/app/assets/javascripts/projects/pipelines/charts/index.js
index b0f5f549980..4ae2b729200 100644
--- a/app/assets/javascripts/projects/pipelines/charts/index.js
+++ b/app/assets/javascripts/projects/pipelines/charts/index.js
@@ -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,
+ ),
},
}),
});
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index c0c599f4b3c..968bd9af84f 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -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">
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index f97c8ae1f74..8703796b116 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -1,4 +1,5 @@
<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);
diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js
index 6936c08d852..265df20636b 100644
--- a/app/assets/javascripts/repository/graphql.js
+++ b/app/assets/javascripts/repository/graphql.js
@@ -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
diff --git a/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql b/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql
index c812614e94d..a22cadf0e8d 100644
--- a/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql
+++ b/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql
@@ -10,6 +10,7 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
webUrl
authoredDate
authorName
+ authorGravatar
author {
name
avatarUrl
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
index b91e08a4251..49e0ef35cb8 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
@@ -1,10 +1,26 @@
<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>
diff --git a/app/assets/javascripts/snippets/queries/snippet.blob.query.graphql b/app/assets/javascripts/snippets/queries/snippet.blob.query.graphql
new file mode 100644
index 00000000000..785c88c185a
--- /dev/null
+++ b/app/assets/javascripts/snippets/queries/snippet.blob.query.graphql
@@ -0,0 +1,24 @@
+#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
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/controllers/projects/registry/tags_controller.rb b/app/controllers/projects/registry/tags_controller.rb
index e572c56adf5..c42e3f6bdba 100644
--- a/app/controllers/projects/registry/tags_controller.rb
+++ b/app/controllers/projects/registry/tags_controller.rb
@@ -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
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index ed5e39478f1..c54687c432c 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -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
diff --git a/app/graphql/types/commit_type.rb b/app/graphql/types/commit_type.rb
index 392e3ae41c5..eb25e3651a8 100644
--- a/app/graphql/types/commit_type.rb
+++ b/app/graphql/types/commit_type.rb
@@ -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,
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 94b99835a53..bb59fc937b1 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -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?
diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml
index 9542f8c9766..7496ca97d56 100644
--- a/app/views/projects/pipelines/charts.html.haml
+++ b/app/views/projects/pipelines/charts.html.haml
@@ -1,7 +1,7 @@
- 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 } } }
diff --git a/app/views/projects/pipelines/charts/_pipelines.haml b/app/views/projects/pipelines/charts/_pipelines.haml
deleted file mode 100644
index afff9e82e45..00000000000
--- a/app/views/projects/pipelines/charts/_pipelines.haml
+++ /dev/null
@@ -1,37 +0,0 @@
-%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
diff --git a/changelogs/unreleased/196183-convert-pipelines-last-week-chart-to-echarts.yml b/changelogs/unreleased/196183-convert-pipelines-last-week-chart-to-echarts.yml
new file mode 100644
index 00000000000..a2a4e57c677
--- /dev/null
+++ b/changelogs/unreleased/196183-convert-pipelines-last-week-chart-to-echarts.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate CI CD pipelines charts to ECharts
+merge_request: 24057
+author:
+type: changed
diff --git a/changelogs/unreleased/fix-upstream-bridge-stuck-when-non-pending-pipelines.yml b/changelogs/unreleased/fix-upstream-bridge-stuck-when-non-pending-pipelines.yml
new file mode 100644
index 00000000000..eed4e811092
--- /dev/null
+++ b/changelogs/unreleased/fix-upstream-bridge-stuck-when-non-pending-pipelines.yml
@@ -0,0 +1,5 @@
+---
+title: Fix upstream bridge stuck when downstream pipeline is not pending
+merge_request: 24665
+author:
+type: fixed
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index c8a91d830a0..c0600d45d13 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -166,6 +166,11 @@ type Commit {
author: User
"""
+ Commit authors gravatar
+ """
+ authorGravatar: String
+
+ """
Commit authors name
"""
authorName: String
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 08b84a1ca35..5b80c8d74a6 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -13260,6 +13260,20 @@
"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",
"args": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 5e0b7465c20..ee3c05eb6a0 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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 |
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f5630209f49..8298d575827 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/package.json b/package.json
index c259a7dfb68..3a5f4edb4ef 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,7 @@
"@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/preset-env": "^7.6.2",
"@gitlab/svgs": "^1.96.0",
- "@gitlab/ui": "^9.4.1",
+ "@gitlab/ui": "^9.6.0",
"@gitlab/visual-review-tools": "1.5.1",
"@sentry/browser": "^5.10.2",
"@sourcegraph/code-host-integration": "0.0.30",
diff --git a/qa/qa/flow/saml.rb b/qa/qa/flow/saml.rb
index 0b9f9f94fbd..676be2beb01 100644
--- a/qa/qa/flow/saml.rb
+++ b/qa/qa/flow/saml.rb
@@ -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!
diff --git a/qa/qa/runtime/feature.rb b/qa/qa/runtime/feature.rb
index 9cb2c925b19..5e948b5b850 100644
--- a/qa/qa/runtime/feature.rb
+++ b/qa/qa/runtime/feature.rb
@@ -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
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index e36d87a7224..8d79e505e5d 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -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
diff --git a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
new file mode 100644
index 00000000000..b77ca28b9d8
--- /dev/null
+++ b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
@@ -0,0 +1,25 @@
+// 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>
+`;
diff --git a/spec/frontend/blob/components/blob_header_default_actions_spec.js b/spec/frontend/blob/components/blob_header_default_actions_spec.js
index 348d68514a3..fe0edffd12d 100644
--- a/spec/frontend/blob/components/blob_header_default_actions_spec.js
+++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js
@@ -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');
});
});
});
diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js
new file mode 100644
index 00000000000..7d1443fb069
--- /dev/null
+++ b/spec/frontend/blob/components/blob_header_spec.js
@@ -0,0 +1,133 @@
+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);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
index ff0b005f441..88e9eeea994 100644
--- a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
+++ b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
@@ -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);
- });
- });
});
});
diff --git a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
index 184d0321dc1..a4a0b98de1b 100644
--- a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
+++ b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
@@ -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>
diff --git a/spec/frontend/contributors/component/contributors_spec.js b/spec/frontend/contributors/component/contributors_spec.js
index 3e4924ed906..24816e4e8ac 100644
--- a/spec/frontend/contributors/component/contributors_spec.js
+++ b/spec/frontend/contributors/component/contributors_spec.js
@@ -1,5 +1,5 @@
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,
diff --git a/spec/frontend/projects/pipelines/charts/components/__snapshots__/pipelines_area_chart_spec.js.snap b/spec/frontend/projects/pipelines/charts/components/__snapshots__/pipelines_area_chart_spec.js.snap
new file mode 100644
index 00000000000..c15971912dd
--- /dev/null
+++ b/spec/frontend/projects/pipelines/charts/components/__snapshots__/pipelines_area_chart_spec.js.snap
@@ -0,0 +1,23 @@
+// 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>
+`;
diff --git a/spec/frontend/projects/pipelines/charts/components/app_spec.js b/spec/frontend/projects/pipelines/charts/components/app_spec.js
index 8aeeb4d5d7d..883f2bec5f7 100644
--- a/spec/frontend/projects/pipelines/charts/components/app_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/app_spec.js
@@ -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);
+ }
+ });
+ });
+ });
});
diff --git a/spec/frontend/projects/pipelines/charts/components/pipelines_area_chart_spec.js b/spec/frontend/projects/pipelines/charts/components/pipelines_area_chart_spec.js
new file mode 100644
index 00000000000..aea25903023
--- /dev/null
+++ b/spec/frontend/projects/pipelines/charts/components/pipelines_area_chart_spec.js
@@ -0,0 +1,30 @@
+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();
+ });
+});
diff --git a/spec/frontend/projects/pipelines/charts/mock_data.js b/spec/frontend/projects/pipelines/charts/mock_data.js
index 93e53125679..db5164c8f99 100644
--- a/spec/frontend/projects/pipelines/charts/mock_data.js
+++ b/spec/frontend/projects/pipelines/charts/mock_data.js
@@ -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]],
+ },
+];
diff --git a/spec/frontend/registry/shared/components/__snapshots__/expiration_policy_fields_spec.js.snap b/spec/frontend/registry/shared/components/__snapshots__/expiration_policy_fields_spec.js.snap
index c5f5ea68d9e..687ff709fd7 100644
--- a/spec/frontend/registry/shared/components/__snapshots__/expiration_policy_fields_spec.js.snap
+++ b/spec/frontend/registry/shared/components/__snapshots__/expiration_policy_fields_spec.js.snap
@@ -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
diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
index 22e353dddc5..49feae0884e 100644
--- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
+++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
@@ -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>
+`;
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index 71709e7dd83..fec9ba3aa2e 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -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'}
diff --git a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_spec.js.snap b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_spec.js.snap
index d5546021430..955716ccbca 100644
--- a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_spec.js.snap
+++ b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_spec.js.snap
@@ -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>
diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js
index 8401c08b1da..efc1c6dcef9 100644
--- a/spec/frontend/snippets/components/snippet_blob_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js
@@ -1,5 +1,7 @@
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);
});
});
diff --git a/spec/graphql/types/commit_type_spec.rb b/spec/graphql/types/commit_type_spec.rb
index 1c3b46ecfde..f5f99229f3a 100644
--- a/spec/graphql/types/commit_type_spec.rb
+++ b/spec/graphql/types/commit_type_spec.rb
@@ -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
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 6efec87464b..42d304d9116 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -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
diff --git a/yarn.lock b/yarn.lock
index da2c8d43670..1a4a8f42074 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"