This instance will be upgraded to Heptapod 0.27.1 on 2021-12-02 at 15:00 UTC+1 (a few minutes of down time)

Commit 6899173f authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 45261a537631
<script>
import ViewerSwitcher from './blob_header_viewer_switcher.vue';
import DefaultActions from './blob_header_default_actions.vue';
import BlobFilepath from './blob_header_filepath.vue';
import eventHub from '../event_hub';
import { RICH_BLOB_VIEWER, SIMPLE_BLOB_VIEWER } from './constants';
export default {
components: {
ViewerSwitcher,
DefaultActions,
BlobFilepath,
},
props: {
blob: {
type: Object,
required: true,
},
hideDefaultActions: {
type: Boolean,
required: false,
default: false,
},
hideViewerSwitcher: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
activeViewer: this.blob.richViewer ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
};
},
computed: {
showViewerSwitcher() {
return !this.hideViewerSwitcher && Boolean(this.blob.simpleViewer && this.blob.richViewer);
},
showDefaultActions() {
return !this.hideDefaultActions;
},
},
created() {
if (this.showViewerSwitcher) {
eventHub.$on('switch-viewer', this.setActiveViewer);
}
},
beforeDestroy() {
if (this.showViewerSwitcher) {
eventHub.$off('switch-viewer', this.setActiveViewer);
}
},
methods: {
setActiveViewer(viewer) {
this.activeViewer = viewer;
},
},
};
</script>
<template>
<div class="js-file-title file-title-flex-parent">
<blob-filepath :blob="blob">
<template #filepathPrepend>
<slot name="prepend"></slot>
</template>
</blob-filepath>
<div class="file-actions d-none d-sm-block">
<viewer-switcher v-if="showViewerSwitcher" :blob="blob" :active-viewer="activeViewer" />
<slot name="actions"></slot>
<default-actions v-if="showDefaultActions" :blob="blob" :active-viewer="activeViewer" />
</div>
</div>
</template>
<script>
import { GlButton, GlButtonGroup, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { BTN_COPY_CONTENTS_TITLE, BTN_DOWNLOAD_TITLE, BTN_RAW_TITLE } from './constants';
import {
BTN_COPY_CONTENTS_TITLE,
BTN_DOWNLOAD_TITLE,
BTN_RAW_TITLE,
RICH_BLOB_VIEWER,
SIMPLE_BLOB_VIEWER,
} from './constants';
import eventHub from '../event_hub';
export default {
components: {
......@@ -16,6 +23,11 @@ export default {
type: Object,
required: true,
},
activeViewer: {
type: String,
default: SIMPLE_BLOB_VIEWER,
required: false,
},
},
computed: {
rawUrl() {
......@@ -24,10 +36,13 @@ export default {
downloadUrl() {
return `${this.blob.rawPath}?inline=false`;
},
copyDisabled() {
return this.activeViewer === RICH_BLOB_VIEWER;
},
},
methods: {
requestCopyContents() {
this.$emit('copy');
eventHub.$emit('copy');
},
},
BTN_COPY_CONTENTS_TITLE,
......@@ -41,6 +56,7 @@ export default {
v-gl-tooltip.hover
:aria-label="$options.BTN_COPY_CONTENTS_TITLE"
:title="$options.BTN_COPY_CONTENTS_TITLE"
:disabled="copyDisabled"
@click="requestCopyContents"
>
<gl-icon name="copy-to-clipboard" :size="14" />
......
......@@ -6,6 +6,7 @@ import {
SIMPLE_BLOB_VIEWER,
SIMPLE_BLOB_VIEWER_TITLE,
} from './constants';
import eventHub from '../event_hub';
export default {
components: {
......@@ -21,25 +22,24 @@ export default {
type: Object,
required: true,
},
},
data() {
return {
viewer: this.blob.richViewer ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
};
activeViewer: {
type: String,
default: SIMPLE_BLOB_VIEWER,
required: false,
},
},
computed: {
isSimpleViewer() {
return this.viewer === SIMPLE_BLOB_VIEWER;
return this.activeViewer === SIMPLE_BLOB_VIEWER;
},
isRichViewer() {
return this.viewer === RICH_BLOB_VIEWER;
return this.activeViewer === RICH_BLOB_VIEWER;
},
},
methods: {
switchToViewer(viewer) {
if (viewer !== this.viewer) {
this.viewer = viewer;
this.$emit('switch-viewer', viewer);
if (viewer !== this.activeViewer) {
eventHub.$emit('switch-viewer', viewer);
}
},
},
......
import Vue from 'vue';
export default new Vue();
......@@ -7,11 +7,13 @@ import { __ } from '~/locale';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import { getDatesInRange } from '~/lib/utils/datetime_utility';
import { xAxisLabelFormatter, dateFormatter } from '../utils';
import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
export default {
components: {
GlAreaChart,
GlLoadingIcon,
ResizableChartContainer,
},
props: {
endpoint: {
......@@ -201,25 +203,35 @@ export default {
<div v-else-if="showChart" class="contributors-charts">
<h4>{{ __('Commits to') }} {{ branch }}</h4>
<span>{{ __('Excluding merge commits. Limited to 6,000 commits.') }}</span>
<div>
<resizable-chart-container>
<gl-area-chart
slot-scope="{ width }"
:width="width"
:data="masterChartData"
:option="masterChartOptions"
:height="masterChartHeight"
@created="onMasterChartCreated"
/>
</div>
</resizable-chart-container>
<div class="row">
<div v-for="contributor in individualChartsData" :key="contributor.name" class="col-6">
<div
v-for="(contributor, index) in individualChartsData"
:key="index"
class="col-lg-6 col-12"
>
<h4>{{ contributor.name }}</h4>
<p>{{ n__('%d commit', '%d commits', contributor.commits) }} ({{ contributor.email }})</p>
<gl-area-chart
:data="contributor.dates"
:option="individualChartOptions"
:height="individualChartHeight"
@created="onIndividualChartCreated"
/>
<resizable-chart-container>
<gl-area-chart
slot-scope="{ width }"
:width="width"
:data="contributor.dates"
:option="individualChartOptions"
:height="individualChartHeight"
@created="onIndividualChartCreated"
/>
</resizable-chart-container>
</div>
</div>
</div>
......
fragment BlobViewer on SnippetBlobViewer {
collapsed
loadingPartialName
renderError
tooLarge
}
import $ from 'jquery';
import Chart from 'chart.js';
import { lineChartOptions } from '~/lib/utils/chart_utils';
import initProjectPipelinesChartsApp from '~/projects/pipelines/charts/index';
const SUCCESS_LINE_COLOR = '#1aaa55';
const TOTAL_LINE_COLOR = '#707070';
const buildChart = (chartScope, shouldAdjustFontSize) => {
const data = {
labels: chartScope.labels,
datasets: [
{
backgroundColor: SUCCESS_LINE_COLOR,
borderColor: SUCCESS_LINE_COLOR,
pointBackgroundColor: SUCCESS_LINE_COLOR,
pointBorderColor: '#fff',
data: chartScope.successValues,
fill: 'origin',
},
{
backgroundColor: TOTAL_LINE_COLOR,
borderColor: TOTAL_LINE_COLOR,
pointBackgroundColor: TOTAL_LINE_COLOR,
pointBorderColor: '#EEE',
data: chartScope.totalValues,
fill: '-1',
},
],
};
const ctx = $(`#${chartScope.scope}Chart`)
.get(0)
.getContext('2d');
return new Chart(ctx, {
type: 'line',
data,
options: lineChartOptions({
width: ctx.canvas.width,
numberOfPoints: chartScope.totalValues.length,
shouldAdjustFontSize,
}),
});
};
document.addEventListener('DOMContentLoaded', () => {
const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML);
// Scale fonts if window width lower than 768px (iPad portrait)
const shouldAdjustFontSize = window.innerWidth < 768;
chartsData.forEach(scope => buildChart(scope, shouldAdjustFontSize));
});
document.addEventListener('DOMContentLoaded', initProjectPipelinesChartsApp);
<script>
import dateFormat from 'dateformat';
import { __, sprintf } from '~/locale';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import StatisticsList from './statistics_list.vue';
import PipelinesAreaChart from './pipelines_area_chart.vue';
import {
CHART_CONTAINER_HEIGHT,
INNER_CHART_HEIGHT,
X_AXIS_LABEL_ROTATION,
X_AXIS_TITLE_OFFSET,
CHART_DATE_FORMAT,
ONE_WEEK_AGO_DAYS,
ONE_MONTH_AGO_DAYS,
} from '../constants';
export default {
components: {
StatisticsList,
GlColumnChart,
PipelinesAreaChart,
},
props: {
counts: {
......@@ -22,6 +30,18 @@ export default {
type: Object,
required: true,
},
lastWeekChartData: {
type: Object,
required: true,
},
lastMonthChartData: {
type: Object,
required: true,
},
lastYearChartData: {
type: Object,
required: true,
},
},
data() {
return {
......@@ -30,10 +50,38 @@ export default {
},
};
},
computed: {
areaCharts() {
const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles;
return [
this.buildAreaChartData(lastWeek, this.lastWeekChartData),
this.buildAreaChartData(lastMonth, this.lastMonthChartData),
this.buildAreaChartData(lastYear, this.lastYearChartData),
];
},
},
methods: {
mergeLabelsAndValues(labels, values) {
return labels.map((label, index) => [label, values[index]]);
},
buildAreaChartData(title, data) {
const { labels, totals, success } = data;
return {
title,
data: [
{
name: 'all',
data: this.mergeLabelsAndValues(labels, totals),
},
{
name: 'success',
data: this.mergeLabelsAndValues(labels, success),
},
],
};
},
},
chartContainerHeight: CHART_CONTAINER_HEIGHT,
timesChartOptions: {
......@@ -45,6 +93,22 @@ export default {
nameGap: X_AXIS_TITLE_OFFSET,
},
},
get chartTitles() {
const today = dateFormat(new Date(), CHART_DATE_FORMAT);
const pastDate = timeScale =>
dateFormat(getDateInPast(new Date(), timeScale), CHART_DATE_FORMAT);
return {
lastWeek: sprintf(__('Pipelines for last week (%{oneWeekAgo} - %{today})'), {
oneWeekAgo: pastDate(ONE_WEEK_AGO_DAYS),
today,
}),
lastMonth: sprintf(__('Pipelines for last month (%{oneMonthAgo} - %{today})'), {
oneMonthAgo: pastDate(ONE_MONTH_AGO_DAYS),
today,
}),
lastYear: __('Pipelines for last year'),
};
},
};
</script>
<template>
......@@ -68,5 +132,14 @@ export default {
/>
</div>
</div>
<hr />
<h4 class="my-4">{{ __('Pipelines charts') }}</h4>
<pipelines-area-chart
v-for="(chart, index) in areaCharts"
:key="index"
:chart-data="chart.data"
>
{{ chart.title }}
</pipelines-area-chart>
</div>
</template>
<script>
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { s__ } from '~/locale';
import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
import { CHART_CONTAINER_HEIGHT } from '../constants';
export default {
components: {
GlAreaChart,
ResizableChartContainer,
},
props: {
chartData: {
type: Array,
required: true,
},
},
areaChartOptions: {
xAxis: {
name: s__('Pipeline|Date'),
type: 'category',
},
yAxis: {
name: s__('Pipeline|Pipelines'),
},
},
chartContainerHeight: CHART_CONTAINER_HEIGHT,
};
</script>
<template>
<div class="prepend-top-default">
<p>
<slot></slot>
</p>
<resizable-chart-container>
<gl-area-chart
slot-scope="{ width }"
:width="width"
:height="$options.chartContainerHeight"
:data="chartData"
:include-legend-avg-max="false"
:option="$options.areaChartOptions"
/>
</resizable-chart-container>
</div>
</template>
......@@ -5,3 +5,9 @@ export const INNER_CHART_HEIGHT = 200;
export const X_AXIS_LABEL_ROTATION = 45;
export const X_AXIS_TITLE_OFFSET = 60;
export const ONE_WEEK_AGO_DAYS = 7;
export const ONE_MONTH_AGO_DAYS = 31;
export const CHART_DATE_FORMAT = 'dd mmm';
......@@ -10,8 +10,23 @@ export default () => {
successRatio,
timesChartLabels,
timesChartValues,
lastWeekChartLabels,
lastWeekChartTotals,
lastWeekChartSuccess,
lastMonthChartLabels,
lastMonthChartTotals,
lastMonthChartSuccess,
lastYearChartLabels,
lastYearChartTotals,
lastYearChartSuccess,
} = el.dataset;
const parseAreaChartData = (labels, totals, success) => ({
labels: JSON.parse(labels),
totals: JSON.parse(totals),
success: JSON.parse(success),
});
return new Vue({
el,
name: 'ProjectPipelinesChartsApp',
......@@ -31,6 +46,21 @@ export default () => {
labels: JSON.parse(timesChartLabels),
values: JSON.parse(timesChartValues),
},
lastWeekChartData: parseAreaChartData(
lastWeekChartLabels,
lastWeekChartTotals,
lastWeekChartSuccess,
),
lastMonthChartData: parseAreaChartData(
lastMonthChartLabels,
lastMonthChartTotals,
lastMonthChartSuccess,
),
lastYearChartData: parseAreaChartData(
lastYearChartLabels,
lastYearChartTotals,
lastYearChartSuccess,
),
},
}),
});
......
......@@ -108,7 +108,12 @@ export default {
class="avatar-cell"
/>
<span v-else class="avatar-cell user-avatar-link">
<img :src="$options.defaultAvatarUrl" width="40" height="40" class="avatar s40" />
<img
:src="commit.authorGravatar || $options.defaultAvatarUrl"
width="40"
height="40"
class="avatar s40"
/>
</span>
<div class="commit-detail flex-list">
<div class="commit-content qa-commit-content">
......
<script>
import { escapeRegExp } from 'lodash';
import { GlBadge, GlLink, GlSkeletonLoading, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
......@@ -105,7 +106,7 @@ export default {
return this.isFolder ? 'router-link' : 'a';
},
fullPath() {
return this.path.replace(new RegExp(`^${this.currentPath}/`), '');
return this.path.replace(new RegExp(`^${escapeRegExp(this.currentPath)}/`), '');
},
shortSha() {
return this.sha.slice(0, 8);
......
......@@ -48,7 +48,7 @@ const defaultClient = createDefaultClient(
case 'TreeEntry':
case 'Submodule':
case 'Blob':
return `${obj.flatPath}-${obj.id}`;
return `${escape(obj.flatPath)}-${obj.id}`;
default:
// If the type doesn't match any of the above we fallback
// to using the default Apollo ID
......
......@@ -10,6 +10,7 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
webUrl
authoredDate
authorName
authorGravatar
author {
name
avatarUrl
......
<script>
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import { SNIPPET_VISIBILITY_PUBLIC } from '../constants';
import BlobHeader from '~/blob/components/blob_header.vue';
import GetSnippetBlobQuery from '../queries/snippet.blob.query.graphql';
import { GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
BlobEmbeddable,
BlobHeader,
GlLoadingIcon,
},
apollo: {
blob: {
query: GetSnippetBlobQuery,