diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-11 15:08:52 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-11 15:08:52 +0300 |
commit | 05b5c609cb8c260b10c2eb1b92b711dc82d32c3f (patch) | |
tree | 05253c66806b17c5b1f9f13addab59524d536fc4 | |
parent | 1078b7bf25c2cb6e03c57da9ae25b0512858556f (diff) |
Add latest changes from gitlab-org/gitlab@master
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 - - %span.legend-success - = icon("circle") - = s_("Pipeline|success") - - %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" |