summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-10-28 18:06:15 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-10-28 18:06:15 +0000
commit7515ec41c527c62bfd56f46e388cf6d9fe06479f (patch)
tree614b555ec428b7eac4b836473d43516c41f9da46 /app
parenta77db6bc47d8cdd9edae2ec22f640821d0794404 (diff)
downloadgitlab-ce-7515ec41c527c62bfd56f46e388cf6d9fe06479f.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/contributors/components/contributors.vue227
-rw-r--r--app/assets/javascripts/contributors/index.js23
-rw-r--r--app/assets/javascripts/contributors/services/contributors_service.js7
-rw-r--r--app/assets/javascripts/contributors/stores/actions.js20
-rw-r--r--app/assets/javascripts/contributors/stores/getters.js33
-rw-r--r--app/assets/javascripts/contributors/stores/index.js18
-rw-r--r--app/assets/javascripts/contributors/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/contributors/stores/mutations.js17
-rw-r--r--app/assets/javascripts/contributors/stores/state.js5
-rw-r--r--app/assets/javascripts/contributors/utils.js30
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js23
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/index.js26
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js140
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js379
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js143
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue4
-rw-r--r--app/assets/stylesheets/pages/graph.scss15
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss62
-rw-r--r--app/helpers/issuables_helper.rb5
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/models/zoom_meeting.rb9
-rw-r--r--app/services/issues/update_service.rb2
-rw-r--r--app/services/issues/zoom_link_service.rb71
-rw-r--r--app/services/zoom_notes_service.rb42
-rw-r--r--app/views/clusters/clusters/index.html.haml2
-rw-r--r--app/views/projects/graphs/show.html.haml28
-rw-r--r--app/views/projects/services/edit.html.haml3
-rw-r--r--app/views/search/_category.html.haml2
29 files changed, 464 insertions, 878 deletions
diff --git a/app/assets/javascripts/contributors/components/contributors.vue b/app/assets/javascripts/contributors/components/contributors.vue
new file mode 100644
index 00000000000..7dd6b051cb4
--- /dev/null
+++ b/app/assets/javascripts/contributors/components/contributors.vue
@@ -0,0 +1,227 @@
+<script>
+import { __ } from '~/locale';
+import _ from 'underscore';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { GlAreaChart } from '@gitlab/ui/dist/charts';
+import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
+import { getDatesInRange } from '~/lib/utils/datetime_utility';
+import { xAxisLabelFormatter, dateFormatter } from '../utils';
+
+export default {
+ components: {
+ GlAreaChart,
+ GlLoadingIcon,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ branch: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ masterChart: null,
+ individualCharts: [],
+ svgs: {},
+ masterChartHeight: 264,
+ individualChartHeight: 216,
+ };
+ },
+ computed: {
+ ...mapState(['chartData', 'loading']),
+ ...mapGetters(['showChart', 'parsedData']),
+ masterChartData() {
+ const data = {};
+ this.xAxisRange.forEach(date => {
+ data[date] = this.parsedData.total[date] || 0;
+ });
+ return [
+ {
+ name: __('Commits'),
+ data: Object.entries(data),
+ },
+ ];
+ },
+ masterChartOptions() {
+ return {
+ ...this.getCommonChartOptions(true),
+ yAxis: {
+ name: __('Number of commits'),
+ },
+ grid: {
+ bottom: 64,
+ left: 64,
+ right: 20,
+ top: 20,
+ },
+ };
+ },
+ individualChartsData() {
+ const maxNumberOfIndividualContributorsCharts = 100;
+
+ return Object.keys(this.parsedData.byAuthor)
+ .map(name => {
+ const author = this.parsedData.byAuthor[name];
+ return {
+ name,
+ email: author.email,
+ commits: author.commits,
+ dates: [
+ {
+ name: __('Commits'),
+ data: this.xAxisRange.map(date => [date, author.dates[date] || 0]),
+ },
+ ],
+ };
+ })
+ .sort((a, b) => b.commits - a.commits)
+ .slice(0, maxNumberOfIndividualContributorsCharts);
+ },
+ individualChartOptions() {
+ return {
+ ...this.getCommonChartOptions(false),
+ yAxis: {
+ name: __('Commits'),
+ max: this.individualChartYAxisMax,
+ },
+ grid: {
+ bottom: 27,
+ left: 64,
+ right: 20,
+ top: 8,
+ },
+ };
+ },
+ individualChartYAxisMax() {
+ return this.individualChartsData.reduce((acc, item) => {
+ const values = item.dates[0].data.map(value => value[1]);
+ return Math.max(acc, ...values);
+ }, 0);
+ },
+ xAxisRange() {
+ const dates = Object.keys(this.parsedData.total).sort((a, b) => new Date(a) - new Date(b));
+
+ const firstContributionDate = new Date(dates[0]);
+ const lastContributionDate = new Date(dates[dates.length - 1]);
+
+ return getDatesInRange(firstContributionDate, lastContributionDate, dateFormatter);
+ },
+ firstContributionDate() {
+ return this.xAxisRange[0];
+ },
+ lastContributionDate() {
+ return this.xAxisRange[this.xAxisRange.length - 1];
+ },
+ charts() {
+ return _.uniq(this.individualCharts);
+ },
+ },
+ mounted() {
+ this.fetchChartData(this.endpoint);
+ },
+ methods: {
+ ...mapActions(['fetchChartData']),
+ getCommonChartOptions(isMasterChart) {
+ return {
+ xAxis: {
+ type: 'time',
+ name: '',
+ data: this.xAxisRange,
+ axisLabel: {
+ formatter: xAxisLabelFormatter,
+ showMaxLabel: false,
+ showMinLabel: false,
+ },
+ boundaryGap: false,
+ splitNumber: isMasterChart ? 24 : 18,
+ // 28 days
+ minInterval: 28 * 86400 * 1000,
+ min: this.firstContributionDate,
+ max: this.lastContributionDate,
+ },
+ };
+ },
+ setSvg(name) {
+ return getSvgIconPathContent(name)
+ .then(path => {
+ if (path) {
+ this.$set(this.svgs, name, `path://${path}`);
+ }
+ })
+ .catch(() => {});
+ },
+ onMasterChartCreated(chart) {
+ this.masterChart = chart;
+ this.setSvg('scroll-handle')
+ .then(() => {
+ this.masterChart.setOption({
+ dataZoom: [
+ {
+ type: 'slider',
+ handleIcon: this.svgs['scroll-handle'],
+ },
+ ],
+ });
+ })
+ .catch(() => {});
+ this.masterChart.on('datazoom', _.debounce(this.setIndividualChartsZoom, 200));
+ },
+ onIndividualChartCreated(chart) {
+ this.individualCharts.push(chart);
+ },
+ setIndividualChartsZoom(options) {
+ this.charts.forEach(chart =>
+ chart.setOption(
+ {
+ dataZoom: {
+ start: options.start,
+ end: options.end,
+ show: false,
+ },
+ },
+ { lazyUpdate: true },
+ ),
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div v-if="loading" class="contributors-loader text-center">
+ <gl-loading-icon :inline="true" :size="4" />
+ </div>
+
+ <div v-else-if="showChart" class="contributors-charts">
+ <h4>{{ __('Commits to') }} {{ branch }}</h4>
+ <span>{{ __('Excluding merge commits. Limited to 6,000 commits.') }}</span>
+ <div>
+ <gl-area-chart
+ :data="masterChartData"
+ :option="masterChartOptions"
+ :height="masterChartHeight"
+ @created="onMasterChartCreated"
+ />
+ </div>
+
+ <div class="row">
+ <div v-for="contributor in individualChartsData" :key="contributor.name" class="col-6">
+ <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"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/contributors/index.js b/app/assets/javascripts/contributors/index.js
new file mode 100644
index 00000000000..b6063589734
--- /dev/null
+++ b/app/assets/javascripts/contributors/index.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import ContributorsGraphs from './components/contributors.vue';
+import store from './stores';
+
+export default () => {
+ const el = document.querySelector('.js-contributors-graph');
+
+ if (!el) return null;
+
+ return new Vue({
+ el,
+ store,
+
+ render(createElement) {
+ return createElement(ContributorsGraphs, {
+ props: {
+ endpoint: el.dataset.projectGraphPath,
+ branch: el.dataset.projectBranch,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/contributors/services/contributors_service.js b/app/assets/javascripts/contributors/services/contributors_service.js
new file mode 100644
index 00000000000..5a8bbb66511
--- /dev/null
+++ b/app/assets/javascripts/contributors/services/contributors_service.js
@@ -0,0 +1,7 @@
+import axios from '~/lib/utils/axios_utils';
+
+export default {
+ fetchChartData(endpoint) {
+ return axios.get(endpoint);
+ },
+};
diff --git a/app/assets/javascripts/contributors/stores/actions.js b/app/assets/javascripts/contributors/stores/actions.js
new file mode 100644
index 00000000000..4138ff24f1d
--- /dev/null
+++ b/app/assets/javascripts/contributors/stores/actions.js
@@ -0,0 +1,20 @@
+import flash from '~/flash';
+import { __ } from '~/locale';
+import service from '../services/contributors_service';
+import * as types from './mutation_types';
+
+export const fetchChartData = ({ commit }, endpoint) => {
+ commit(types.SET_LOADING_STATE, true);
+
+ return service
+ .fetchChartData(endpoint)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.SET_CHART_DATA, data);
+ commit(types.SET_LOADING_STATE, false);
+ })
+ .catch(() => flash(__('An error occurred while loading chart data')));
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/contributors/stores/getters.js b/app/assets/javascripts/contributors/stores/getters.js
new file mode 100644
index 00000000000..9e02e3ed9e7
--- /dev/null
+++ b/app/assets/javascripts/contributors/stores/getters.js
@@ -0,0 +1,33 @@
+export const showChart = state => Boolean(!state.loading && state.chartData);
+
+export const parsedData = state => {
+ const byAuthor = {};
+ const total = {};
+
+ state.chartData.forEach(({ date, author_name, author_email }) => {
+ total[date] = total[date] ? total[date] + 1 : 1;
+
+ const authorData = byAuthor[author_name];
+
+ if (!authorData) {
+ byAuthor[author_name] = {
+ email: author_email.toLowerCase(),
+ commits: 1,
+ dates: {
+ [date]: 1,
+ },
+ };
+ } else {
+ authorData.commits += 1;
+ authorData.dates[date] = authorData.dates[date] ? authorData.dates[date] + 1 : 1;
+ }
+ });
+
+ return {
+ total,
+ byAuthor,
+ };
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/contributors/stores/index.js b/app/assets/javascripts/contributors/stores/index.js
new file mode 100644
index 00000000000..bc739851aa7
--- /dev/null
+++ b/app/assets/javascripts/contributors/stores/index.js
@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import state from './state';
+import mutations from './mutations';
+import * as getters from './getters';
+import * as actions from './actions';
+
+Vue.use(Vuex);
+
+export const createStore = () =>
+ new Vuex.Store({
+ actions,
+ mutations,
+ getters,
+ state: state(),
+ });
+
+export default createStore();
diff --git a/app/assets/javascripts/contributors/stores/mutation_types.js b/app/assets/javascripts/contributors/stores/mutation_types.js
new file mode 100644
index 00000000000..62e0a51d5f8
--- /dev/null
+++ b/app/assets/javascripts/contributors/stores/mutation_types.js
@@ -0,0 +1,3 @@
+export const SET_CHART_DATA = 'SET_CHART_DATA';
+export const SET_LOADING_STATE = 'SET_LOADING_STATE';
+export const SET_ACTIVE_BRANCH = 'SET_ACTIVE_BRANCH';
diff --git a/app/assets/javascripts/contributors/stores/mutations.js b/app/assets/javascripts/contributors/stores/mutations.js
new file mode 100644
index 00000000000..f1f460d072d
--- /dev/null
+++ b/app/assets/javascripts/contributors/stores/mutations.js
@@ -0,0 +1,17 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_LOADING_STATE](state, value) {
+ state.loading = value;
+ },
+ [types.SET_CHART_DATA](state, chartData) {
+ Object.assign(state, {
+ chartData,
+ });
+ },
+ [types.SET_ACTIVE_BRANCH](state, branch) {
+ Object.assign(state, {
+ branch,
+ });
+ },
+};
diff --git a/app/assets/javascripts/contributors/stores/state.js b/app/assets/javascripts/contributors/stores/state.js
new file mode 100644
index 00000000000..1dc1a3c7b75
--- /dev/null
+++ b/app/assets/javascripts/contributors/stores/state.js
@@ -0,0 +1,5 @@
+export default () => ({
+ loading: false,
+ chartData: null,
+ branch: 'master',
+});
diff --git a/app/assets/javascripts/contributors/utils.js b/app/assets/javascripts/contributors/utils.js
new file mode 100644
index 00000000000..7d8932ce495
--- /dev/null
+++ b/app/assets/javascripts/contributors/utils.js
@@ -0,0 +1,30 @@
+import { getMonthNames } from '~/lib/utils/datetime_utility';
+
+/**
+ * Converts provided string to date and returns formatted value as a year for date in January and month name for the rest
+ * @param {String}
+ * @returns {String} - formatted value
+ *
+ * xAxisLabelFormatter('01-12-2019') will return '2019'
+ * xAxisLabelFormatter('02-12-2019') will return 'Feb'
+ * xAxisLabelFormatter('07-12-2019') will return 'Jul'
+ */
+export const xAxisLabelFormatter = val => {
+ const date = new Date(val);
+ const month = date.getUTCMonth();
+ const year = date.getUTCFullYear();
+ return month === 0 ? `${year}` : getMonthNames(true)[month];
+};
+
+/**
+ * Formats provided date to YYYY-MM-DD format
+ * @param {Date}
+ * @returns {String} - formatted value
+ */
+export const dateFormatter = date => {
+ const year = date.getUTCFullYear();
+ const month = date.getUTCMonth();
+ const day = date.getUTCDate();
+
+ return `${year}-${`0${month + 1}`.slice(-2)}-${`0${day}`.slice(-2)}`;
+};
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 37b0215f6f9..e2d188103bc 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -564,3 +564,26 @@ export const getDateInPast = (date, daysInPast) => {
export const beginOfDayTime = 'T00:00:00Z';
export const endOfDayTime = 'T23:59:59Z';
+
+/**
+ * @param {Date} d1
+ * @param {Date} d2
+ * @param {Function} formatter
+ * @return {Any[]} an array of formatted dates between 2 given dates (including start&end date)
+ */
+export const getDatesInRange = (d1, d2, formatter = x => x) => {
+ if (!(d1 instanceof Date) || !(d2 instanceof Date)) {
+ return [];
+ }
+ let startDate = d1.getTime();
+ const endDate = d2.getTime();
+ const oneDay = 24 * 3600 * 1000;
+ const range = [d1];
+
+ while (startDate < endDate) {
+ startDate += oneDay;
+ range.push(new Date(startDate));
+ }
+
+ return range.map(formatter);
+};
diff --git a/app/assets/javascripts/pages/projects/graphs/show/index.js b/app/assets/javascripts/pages/projects/graphs/show/index.js
index f79c386b59e..09d9c78c446 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/index.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/index.js
@@ -1,25 +1,3 @@
-import $ from 'jquery';
-import flash from '~/flash';
-import { __ } from '~/locale';
-import axios from '~/lib/utils/axios_utils';
-import ContributorsStatGraph from './stat_graph_contributors';
+import initContributorsGraphs from '~/contributors';
-document.addEventListener('DOMContentLoaded', () => {
- const url = document.querySelector('.js-graphs-show').dataset.projectGraphPath;
-
- axios
- .get(url)
- .then(({ data }) => {
- const graph = new ContributorsStatGraph();
- graph.init(data);
-
- $('#brush_change').change(() => {
- graph.change_date_header();
- graph.redraw_authors();
- });
-
- $('.stat-graph').fadeIn();
- $('.loading-graph').hide();
- })
- .catch(() => flash(__('Error fetching contributors data.')));
-});
+document.addEventListener('DOMContentLoaded', initContributorsGraphs);
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
deleted file mode 100644
index 5b873e6b909..00000000000
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
+++ /dev/null
@@ -1,140 +0,0 @@
-/* eslint-disable func-names, no-var, one-var, camelcase, no-param-reassign, no-return-assign */
-
-import $ from 'jquery';
-import _ from 'underscore';
-import { n__, s__, createDateTimeFormat, sprintf } from '~/locale';
-import {
- ContributorsGraph,
- ContributorsAuthorGraph,
- ContributorsMasterGraph,
-} from './stat_graph_contributors_graph';
-import ContributorsStatGraphUtil from './stat_graph_contributors_util';
-
-export default (function() {
- function ContributorsStatGraph() {
- this.dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' });
- }
-
- ContributorsStatGraph.prototype.init = function(log) {
- var author_commits, total_commits;
- this.parsed_log = ContributorsStatGraphUtil.parse_log(log);
- this.set_current_field('commits');
- total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
- author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field);
- this.add_master_graph(total_commits);
- this.add_authors_graph(author_commits);
- return this.change_date_header();
- };
-
- ContributorsStatGraph.prototype.add_master_graph = function(total_data) {
- this.master_graph = new ContributorsMasterGraph(total_data);
- return this.master_graph.draw();
- };
-
- ContributorsStatGraph.prototype.add_authors_graph = function(author_data) {
- var limited_author_data;
- this.authors = [];
- limited_author_data = author_data.slice(0, 100);
- return _.each(
- limited_author_data,
- (function(_this) {
- return function(d) {
- var author_graph, author_header;
- author_header = _this.create_author_header(d);
- $('.contributors-list').append(author_header);
-
- author_graph = new ContributorsAuthorGraph(d.dates);
- _this.authors[d.author_name] = author_graph;
- return author_graph.draw();
- };
- })(this),
- );
- };
-
- ContributorsStatGraph.prototype.format_author_commit_info = function(author) {
- var commits;
- commits = $('<span/>', {
- class: 'graph-author-commits-count',
- });
- commits.text(n__('%d commit', '%d commits', author.commits));
- return $('<span/>').append(commits);
- };
-
- ContributorsStatGraph.prototype.create_author_header = function(author) {
- var author_commit_info, author_commit_info_span, author_email, author_name, list_item;
- list_item = $('<li/>', {
- class: 'person',
- style: 'display: block;',
- });
- author_name = $(`<h4>${author.author_name}</h4>`);
- author_email = $(`<p class="graph-author-email">${author.author_email}</p>`);
- author_commit_info_span = $('<span/>', {
- class: 'commits',
- });
- author_commit_info = this.format_author_commit_info(author);
- author_commit_info_span.html(author_commit_info);
- list_item.append(author_name);
- list_item.append(author_email);
- list_item.append(author_commit_info_span);
- return list_item;
- };
-
- ContributorsStatGraph.prototype.redraw_master = function() {
- var total_data;
- total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
- this.master_graph.set_data(total_data);
- return this.master_graph.redraw();
- };
-
- ContributorsStatGraph.prototype.redraw_authors = function() {
- $('ol').html('');
-
- const { x_domain } = ContributorsGraph.prototype;
- const author_commits = ContributorsStatGraphUtil.get_author_data(
- this.parsed_log,
- this.field,
- x_domain,
- );
-
- return _.each(
- author_commits,
- (function(_this) {
- return function(d) {
- _this.redraw_author_commit_info(d);
- if (_this.authors[d.author_name] != null) {
- $(_this.authors[d.author_name].list_item).appendTo('ol');
- _this.authors[d.author_name].set_data(d.dates);
- return _this.authors[d.author_name].redraw();
- }
- return '';
- };
- })(this),
- );
- };
-
- ContributorsStatGraph.prototype.set_current_field = function(field) {
- return (this.field = field);
- };
-
- ContributorsStatGraph.prototype.change_date_header = function() {
- const { x_domain } = ContributorsGraph.prototype;
- const formattedDateRange = sprintf(s__('ContributorsPage|%{startDate} – %{endDate}'), {
- startDate: this.dateFormat.format(new Date(x_domain[0])),
- endDate: this.dateFormat.format(new Date(x_domain[1])),
- });
- return $('#date_header').text(formattedDateRange);
- };
-
- ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) {
- var author_commit_info, author_list_item, $author;
- $author = this.authors[author.author_name];
- if ($author != null) {
- author_list_item = $(this.authors[author.author_name].list_item);
- author_commit_info = this.format_author_commit_info(author);
- return author_list_item.find('span').html(author_commit_info);
- }
- return '';
- };
-
- return ContributorsStatGraph;
-})();
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
deleted file mode 100644
index 86794800f87..00000000000
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
+++ /dev/null
@@ -1,379 +0,0 @@
-/* eslint-disable func-names, no-restricted-syntax, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, no-return-assign, no-else-return, no-shadow */
-
-import $ from 'jquery';
-import _ from 'underscore';
-import { extent, max } from 'd3-array';
-import { select, event as d3Event } from 'd3-selection';
-import { scaleTime, scaleLinear } from 'd3-scale';
-import { axisLeft, axisBottom } from 'd3-axis';
-import { area } from 'd3-shape';
-import { brushX } from 'd3-brush';
-import { timeParse } from 'd3-time-format';
-import { dateTickFormat } from '~/lib/utils/tick_formats';
-
-const d3 = {
- extent,
- max,
- select,
- scaleTime,
- scaleLinear,
- axisLeft,
- axisBottom,
- area,
- brushX,
- timeParse,
-};
-
-const hasProp = {}.hasOwnProperty;
-const extend = function(child, parent) {
- for (const key in parent) {
- if (hasProp.call(parent, key)) child[key] = parent[key];
- }
- function ctor() {
- this.constructor = child;
- }
- ctor.prototype = parent.prototype;
- child.prototype = new ctor();
- child.__super__ = parent.prototype;
- return child;
-};
-
-export const ContributorsGraph = (function() {
- function ContributorsGraph() {}
-
- ContributorsGraph.prototype.MARGIN = {
- top: 20,
- right: 10,
- bottom: 30,
- left: 40,
- };
-
- ContributorsGraph.prototype.x_domain = null;
-
- ContributorsGraph.prototype.y_domain = null;
-
- ContributorsGraph.prototype.dates = [];
-
- ContributorsGraph.prototype.determine_width = function(baseWidth, $parentElement) {
- const parentPaddingWidth =
- parseFloat($parentElement.css('padding-left')) +
- parseFloat($parentElement.css('padding-right'));
- const marginWidth = this.MARGIN.left + this.MARGIN.right;
- return baseWidth - parentPaddingWidth - marginWidth;
- };
-
- ContributorsGraph.set_x_domain = function(data) {
- return (ContributorsGraph.prototype.x_domain = data);
- };
-
- ContributorsGraph.set_y_domain = function(data) {
- return (ContributorsGraph.prototype.y_domain = [
- 0,
- d3.max(data, d => (d.commits = d.commits || d.additions || d.deletions)),
- ]);
- };
-
- ContributorsGraph.init_x_domain = function(data) {
- return (ContributorsGraph.prototype.x_domain = d3.extent(data, d => d.date));
- };
-
- ContributorsGraph.init_y_domain = function(data) {
- return (ContributorsGraph.prototype.y_domain = [
- 0,
- d3.max(data, d => (d.commits = d.commits || d.additions || d.deletions)),
- ]);
- };
-
- ContributorsGraph.init_domain = function(data) {
- ContributorsGraph.init_x_domain(data);
- return ContributorsGraph.init_y_domain(data);
- };
-
- ContributorsGraph.set_dates = function(data) {
- return (ContributorsGraph.prototype.dates = data);
- };
-
- ContributorsGraph.prototype.set_x_domain = function() {
- return this.x.domain(this.x_domain);
- };
-
- ContributorsGraph.prototype.set_y_domain = function() {
- return this.y.domain(this.y_domain);
- };
-
- ContributorsGraph.prototype.set_domain = function() {
- this.set_x_domain();
- return this.set_y_domain();
- };
-
- ContributorsGraph.prototype.create_scale = function(width, height) {
- this.x = d3
- .scaleTime()
- .range([0, width])
- .clamp(true);
- return (this.y = d3
- .scaleLinear()
- .range([height, 0])
- .nice());
- };
-
- ContributorsGraph.prototype.draw_x_axis = function() {
- return this.svg
- .append('g')
- .attr('class', 'x axis')
- .attr('transform', `translate(0, ${this.height})`)
- .call(this.x_axis);
- };
-
- ContributorsGraph.prototype.draw_y_axis = function() {
- return this.svg
- .append('g')
- .attr('class', 'y axis')
- .call(this.y_axis);
- };
-
- ContributorsGraph.prototype.set_data = function(data) {
- return (this.data = data);
- };
-
- return ContributorsGraph;
-})();
-
-export const ContributorsMasterGraph = (function(superClass) {
- extend(ContributorsMasterGraph, superClass);
-
- function ContributorsMasterGraph(data1) {
- const $parentElement = $('#contributors-master');
-
- this.data = data1;
- this.update_content = this.update_content.bind(this);
- this.width = this.determine_width($('.js-graphs-show').width(), $parentElement);
- this.height = 200;
- this.x = null;
- this.y = null;
- this.x_axis = null;
- this.y_axis = null;
- this.area = null;
- this.svg = null;
- this.brush = null;
- this.x_max_domain = null;
- }
-
- ContributorsMasterGraph.prototype.process_dates = function(data) {
- const dates = this.get_dates(data);
- this.parse_dates(data);
- return ContributorsGraph.set_dates(dates);
- };
-
- ContributorsMasterGraph.prototype.get_dates = function(data) {
- return _.pluck(data, 'date');
- };
-
- ContributorsMasterGraph.prototype.parse_dates = function(data) {
- const parseDate = d3.timeParse('%Y-%m-%d');
- return data.forEach(d => (d.date = parseDate(d.date)));
- };
-
- ContributorsMasterGraph.prototype.create_scale = function() {
- return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height);
- };
-
- ContributorsMasterGraph.prototype.create_axes = function() {
- this.x_axis = d3
- .axisBottom()
- .scale(this.x)
- .tickFormat(dateTickFormat);
- return (this.y_axis = d3
- .axisLeft()
- .scale(this.y)
- .ticks(5));
- };
-
- ContributorsMasterGraph.prototype.create_svg = function() {
- this.svg = d3
- .select('#contributors-master')
- .append('svg')
- .attr('width', this.width + this.MARGIN.left + this.MARGIN.right)
- .attr('height', this.height + this.MARGIN.top + this.MARGIN.bottom)
- .attr('class', 'tint-box')
- .append('g')
- .attr('transform', `translate(${this.MARGIN.left},${this.MARGIN.top})`);
- return this.svg;
- };
-
- ContributorsMasterGraph.prototype.create_area = function(x, y) {
- return (this.area = d3
- .area()
- .x(d => x(d.date))
- .y0(this.height)
- .y1(d => {
- d.commits = d.commits || d.additions || d.deletions;
- return y(d.commits);
- }));
- };
-
- ContributorsMasterGraph.prototype.create_brush = function() {
- return (this.brush = d3
- .brushX(this.x)
- .extent([[this.x.range()[0], 0], [this.x.range()[1], this.height]])
- .on('end', this.update_content));
- };
-
- ContributorsMasterGraph.prototype.draw_path = function(data) {
- return this.svg
- .append('path')
- .datum(data)
- .attr('class', 'area')
- .attr('d', this.area);
- };
-
- ContributorsMasterGraph.prototype.add_brush = function() {
- return this.svg
- .append('g')
- .attr('class', 'selection')
- .call(this.brush)
- .selectAll('rect')
- .attr('height', this.height);
- };
-
- ContributorsMasterGraph.prototype.update_content = function() {
- // d3Event.selection replaces the function brush.empty() calls
- if (d3Event.selection != null) {
- ContributorsGraph.set_x_domain(d3Event.selection.map(this.x.invert));
- } else {
- ContributorsGraph.set_x_domain(this.x_max_domain);
- }
- return $('#brush_change').trigger('change');
- };
-
- ContributorsMasterGraph.prototype.draw = function() {
- this.process_dates(this.data);
- this.create_scale();
- this.create_axes();
- ContributorsGraph.init_domain(this.data);
- this.x_max_domain = this.x_domain;
- this.set_domain();
- this.create_area(this.x, this.y);
- this.create_svg();
- this.create_brush();
- this.draw_path(this.data);
- this.draw_x_axis();
- this.draw_y_axis();
- return this.add_brush();
- };
-
- ContributorsMasterGraph.prototype.redraw = function() {
- this.process_dates(this.data);
- ContributorsGraph.set_y_domain(this.data);
- this.set_y_domain();
- this.svg.select('path').datum(this.data);
- this.svg.select('path').attr('d', this.area);
- return this.svg.select('.y.axis').call(this.y_axis);
- };
-
- return ContributorsMasterGraph;
-})(ContributorsGraph);
-
-export const ContributorsAuthorGraph = (function(superClass) {
- extend(ContributorsAuthorGraph, superClass);
-
- function ContributorsAuthorGraph(data1) {
- const $parentElements = $('.person');
-
- this.data = data1;
- // Don't split graph size in half for mobile devices.
- if ($(window).width() < 790) {
- this.width = this.determine_width($('.js-graphs-show').width(), $parentElements);
- } else {
- this.width = this.determine_width($('.js-graphs-show').width() / 2, $parentElements);
- }
- this.height = 200;
- this.x = null;
- this.y = null;
- this.x_axis = null;
- this.y_axis = null;
- this.area = null;
- this.svg = null;
- this.list_item = null;
- }
-
- ContributorsAuthorGraph.prototype.create_scale = function() {
- return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height);
- };
-
- ContributorsAuthorGraph.prototype.create_axes = function() {
- this.x_axis = d3
- .axisBottom()
- .scale(this.x)
- .ticks(8)
- .tickFormat(dateTickFormat);
- return (this.y_axis = d3
- .axisLeft()
- .scale(this.y)
- .ticks(5));
- };
-
- ContributorsAuthorGraph.prototype.create_area = function(x, y) {
- return (this.area = d3
- .area()
- .x(d => {
- const parseDate = d3.timeParse('%Y-%m-%d');
- return x(parseDate(d));
- })
- .y0(this.height)
- .y1(
- (function(_this) {
- return function(d) {
- if (_this.data[d] != null) {
- return y(_this.data[d]);
- } else {
- return y(0);
- }
- };
- })(this),
- ));
- };
-
- ContributorsAuthorGraph.prototype.create_svg = function() {
- const persons = document.querySelectorAll('.person');
- this.list_item = persons[persons.length - 1];
- this.svg = d3
- .select(this.list_item)
- .append('svg')
- .attr('width', this.width + this.MARGIN.left + this.MARGIN.right)
- .attr('height', this.height + this.MARGIN.top + this.MARGIN.bottom)
- .attr('class', 'spark')
- .append('g')
- .attr('transform', `translate(${this.MARGIN.left},${this.MARGIN.top})`);
- return this.svg;
- };
-
- ContributorsAuthorGraph.prototype.draw_path = function(data) {
- return this.svg
- .append('path')
- .datum(data)
- .attr('class', 'area-contributor')
- .attr('d', this.area);
- };
-
- ContributorsAuthorGraph.prototype.draw = function() {
- this.create_scale();
- this.create_axes();
- this.set_domain();
- this.create_area(this.x, this.y);
- this.create_svg();
- this.draw_path(this.dates);
- this.draw_x_axis();
- return this.draw_y_axis();
- };
-
- ContributorsAuthorGraph.prototype.redraw = function() {
- this.set_domain();
- this.svg.select('path').datum(this.dates);
- this.svg.select('path').attr('d', this.area);
- this.svg.select('.x.axis').call(this.x_axis);
- return this.svg.select('.y.axis').call(this.y_axis);
- };
-
- return ContributorsAuthorGraph;
-})(ContributorsGraph);
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js
deleted file mode 100644
index a89a13fe37a..00000000000
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js
+++ /dev/null
@@ -1,143 +0,0 @@
-/* eslint-disable func-names, no-var, one-var, camelcase, no-param-reassign, no-return-assign, consistent-return, no-cond-assign, no-else-return */
-import _ from 'underscore';
-
-export default {
- parse_log(log) {
- var by_author, by_email, data, entry, i, len, total, normalized_email;
- total = {};
- by_author = {};
- by_email = {};
- for (i = 0, len = log.length; i < len; i += 1) {
- entry = log[i];
- if (total[entry.date] == null) {
- this.add_date(entry.date, total);
- }
- normalized_email = entry.author_email.toLowerCase();
- data = by_author[entry.author_name] || by_email[normalized_email];
- if (data == null) {
- data = this.add_author(entry, by_author, by_email);
- }
- if (!data[entry.date]) {
- this.add_date(entry.date, data);
- }
- this.store_data(entry, total[entry.date], data[entry.date]);
- }
- total = _.toArray(total);
- by_author = _.toArray(by_author);
- return {
- total,
- by_author,
- };
- },
- add_date(date, collection) {
- collection[date] = {};
- return (collection[date].date = date);
- },
- add_author(author, by_author, by_email) {
- var data, normalized_email;
- data = {};
- data.author_name = author.author_name;
- data.author_email = author.author_email;
- normalized_email = author.author_email.toLowerCase();
- by_author[author.author_name] = data;
- by_email[normalized_email] = data;
- return data;
- },
- store_data(entry, total, by_author) {
- this.store_commits(total, by_author);
- this.store_additions(entry, total, by_author);
- return this.store_deletions(entry, total, by_author);
- },
- store_commits(total, by_author) {
- this.add(total, 'commits', 1);
- return this.add(by_author, 'commits', 1);
- },
- add(collection, field, value) {
- if (collection[field] == null) {
- collection[field] = 0;
- }
- return (collection[field] += value);
- },
- store_additions(entry, total, by_author) {
- if (entry.additions == null) {
- entry.additions = 0;
- }
- this.add(total, 'additions', entry.additions);
- return this.add(by_author, 'additions', entry.additions);
- },
- store_deletions(entry, total, by_author) {
- if (entry.deletions == null) {
- entry.deletions = 0;
- }
- this.add(total, 'deletions', entry.deletions);
- return this.add(by_author, 'deletions', entry.deletions);
- },
- get_total_data(parsed_log, field) {
- var log, total_data;
- log = parsed_log.total;
- total_data = this.pick_field(log, field);
- return _.sortBy(total_data, d => d.date);
- },
- pick_field(log, field) {
- var total_data;
- total_data = [];
- _.each(log, d => total_data.push(_.pick(d, [field, 'date'])));
- return total_data;
- },
- get_author_data(parsed_log, field, date_range) {
- var author_data, log;
- if (date_range == null) {
- date_range = null;
- }
- log = parsed_log.by_author;
- author_data = [];
- _.each(
- log,
- (function(_this) {
- return function(log_entry) {
- var parsed_log_entry;
- parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range);
- if (!_.isEmpty(parsed_log_entry.dates)) {
- return author_data.push(parsed_log_entry);
- }
- };
- })(this),
- );
- return _.sortBy(author_data, d => d[field]).reverse();
- },
- parse_log_entry(log_entry, field, date_range) {
- var parsed_entry;
- parsed_entry = {};
-
- parsed_entry.author_name = log_entry.author_name;
- parsed_entry.author_email = log_entry.author_email;
- parsed_entry.dates = {};
-
- parsed_entry.commits = 0;
- parsed_entry.additions = 0;
- parsed_entry.deletions = 0;
-
- _.each(
- _.omit(log_entry, 'author_name', 'author_email'),
- (function(_this) {
- return function(value) {
- if (_this.in_range(value.date, date_range)) {
- parsed_entry.dates[value.date] = value[field];
- parsed_entry.commits += value.commits;
- parsed_entry.additions += value.additions;
- return (parsed_entry.deletions += value.deletions);
- }
- };
- })(this),
- );
- return parsed_entry;
- },
- in_range(date, date_range) {
- var ref;
- if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) {
- return true;
- } else {
- return false;
- }
- },
-};
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 89cac42abae..fcc57da0649 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -345,8 +345,8 @@ export default {
<project-setting-row
v-if="pagesAvailable && pagesAccessControlEnabled"
:help-path="pagesHelpPath"
- label="Pages access control"
- help-text="Access control for the project's static website"
+ :label="s__('ProjectSettings|Pages')"
+ :help-text="__('With GitLab Pages you can host your static websites on GitLab')"
>
<project-feature-setting
v-model="pagesAccessLevel"
diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss
index 3febf4cf826..a8de8303a19 100644
--- a/app/assets/stylesheets/pages/graph.scss
+++ b/app/assets/stylesheets/pages/graph.scss
@@ -17,21 +17,6 @@
}
}
-.graphs {
- .graph-author-email {
- float: right;
- color: $gl-gray-500;
- }
-
- .graph-additions {
- color: $green-600;
- }
-
- .graph-deletions {
- color: $red-500;
- }
-}
-
.svg-graph-container {
width: 100%;
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
deleted file mode 100644
index 31ccdacbc02..00000000000
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ /dev/null
@@ -1,62 +0,0 @@
-.tint-box {
- background: $stat-graph-common-bg;
- position: relative;
- margin-bottom: 10px;
-}
-
-.area {
- fill: $green-500;
- fill-opacity: 0.5;
-}
-
-.axis {
- font-size: 10px;
-}
-
-#contributors-master {
- @include media-breakpoint-up(md) {
- @include make-col-ready();
- @include make-col(12);
- }
-}
-
-#contributors {
- flex: 1;
-
- .contributors-list {
- margin: 0 0 10px;
- list-style: none;
- padding: 0;
- }
-
- .person {
- @include media-breakpoint-up(md) {
- @include make-col-ready();
- @include make-col(6);
- }
-
- margin-top: 10px;
-
- @include media-breakpoint-down(xs) {
- width: 100%;
- }
-
- .spark {
- display: block;
- background: $stat-graph-common-bg;
- width: 100%;
- }
-
- .area-contributor {
- fill: $orange-500;
- }
- }
-}
-
-.selection rect {
- fill-opacity: 0.1;
- stroke-width: 1px;
- stroke-opacity: 0.4;
- shape-rendering: crispedges;
- stroke-dasharray: 3 3;
-}
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index df9d1933271..3c72f41a4c9 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -281,10 +281,7 @@ module IssuablesHelper
}
data[:hasClosingMergeRequest] = issuable.merge_requests_count(current_user) != 0 if issuable.is_a?(Issue)
-
- zoom_links = Gitlab::ZoomLinkExtractor.new(issuable.description).links
-
- data[:zoomMeetingUrl] = zoom_links.last if zoom_links.any?
+ data[:zoomMeetingUrl] = ZoomMeeting.canonical_meeting_url(issuable) if issuable.is_a?(Issue)
if parent.is_a?(Group)
data[:groupPath] = parent.path
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 978d0a8c8fb..a59cc3282bb 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -273,7 +273,7 @@ module SearchHelper
sanitize(html, tags: %w(a p ol ul li pre code))
end
- def search_tabs?(tab)
+ def show_user_search_tab?
return false if Feature.disabled?(:users_search, default_enabled: true)
if @project
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 91bef81227f..260593c38f4 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -40,6 +40,7 @@ class Issue < ApplicationRecord
has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees
+ has_many :zoom_meetings
validates :project, presence: true
diff --git a/app/models/zoom_meeting.rb b/app/models/zoom_meeting.rb
index cb3b5c60e54..a7ecd1e6a2c 100644
--- a/app/models/zoom_meeting.rb
+++ b/app/models/zoom_meeting.rb
@@ -14,4 +14,13 @@ class ZoomMeeting < ApplicationRecord
scope :added_to_issue, -> { where(issue_status: :added) }
scope :removed_from_issue, -> { where(issue_status: :removed) }
+ scope :canonical, -> (issue) { where(issue: issue).added_to_issue }
+
+ def self.canonical_meeting(issue)
+ canonical(issue)&.take
+ end
+
+ def self.canonical_meeting_url(issue)
+ canonical_meeting(issue)&.url
+ end
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 528b1ea61b3..b98a4d2567f 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -61,8 +61,6 @@ module Issues
if added_mentions.present?
notification_service.async.new_mentions_in_issue(issue, added_mentions, current_user)
end
-
- ZoomNotesService.new(issue, project, current_user, old_description: old_associations[:description]).execute
end
def handle_task_changes(issuable)
diff --git a/app/services/issues/zoom_link_service.rb b/app/services/issues/zoom_link_service.rb
index 561c86475e5..023d7080e88 100644
--- a/app/services/issues/zoom_link_service.rb
+++ b/app/services/issues/zoom_link_service.rb
@@ -6,32 +6,37 @@ module Issues
super(issue.project, user)
@issue = issue
+ @added_meeting = ZoomMeeting.canonical_meeting(@issue)
end
def add_link(link)
if can_add_link? && (link = parse_link(link))
- track_meeting_added_event
- success(_('Zoom meeting added'), append_to_description(link))
+ begin
+ add_zoom_meeting(link)
+ success(_('Zoom meeting added'))
+ rescue ActiveRecord::RecordNotUnique
+ error(_('Failed to add a Zoom meeting'))
+ end
else
error(_('Failed to add a Zoom meeting'))
end
end
- def can_add_link?
- can? && !link_in_issue_description?
- end
-
def remove_link
if can_remove_link?
- track_meeting_removed_event
- success(_('Zoom meeting removed'), remove_from_description)
+ remove_zoom_meeting
+ success(_('Zoom meeting removed'))
else
error(_('Failed to remove a Zoom meeting'))
end
end
+ def can_add_link?
+ can_update_issue? && !@added_meeting
+ end
+
def can_remove_link?
- can? && link_in_issue_description?
+ can_update_issue? && !!@added_meeting
end
def parse_link(link)
@@ -42,10 +47,6 @@ module Issues
attr_reader :issue
- def issue_description
- issue.description || ''
- end
-
def track_meeting_added_event
::Gitlab::Tracking.event('IncidentManagement::ZoomIntegration', 'add_zoom_meeting', label: 'Issue ID', value: issue.id)
end
@@ -54,39 +55,33 @@ module Issues
::Gitlab::Tracking.event('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id)
end
- def success(message, description)
- ServiceResponse
- .success(message: message, payload: { description: description })
- end
-
- def error(message)
- ServiceResponse.error(message: message)
+ def add_zoom_meeting(link)
+ ZoomMeeting.create(
+ issue: @issue,
+ project: @issue.project,
+ issue_status: :added,
+ url: link
+ )
+ track_meeting_added_event
+ SystemNoteService.zoom_link_added(@issue, @project, current_user)
end
- def append_to_description(link)
- "#{issue_description}\n\n#{link}"
+ def remove_zoom_meeting
+ @added_meeting.update(issue_status: :removed)
+ track_meeting_removed_event
+ SystemNoteService.zoom_link_removed(@issue, @project, current_user)
end
- def remove_from_description
- link = parse_link(issue_description)
- return issue_description unless link
-
- issue_description.delete_suffix(link).rstrip
+ def success(message)
+ ServiceResponse.success(message: message)
end
- def link_in_issue_description?
- link = extract_link_from_issue_description
- return unless link
-
- Gitlab::ZoomLinkExtractor.new(link).match?
- end
-
- def extract_link_from_issue_description
- issue_description[/(\S+)\z/, 1]
+ def error(message)
+ ServiceResponse.error(message: message)
end
- def can?
- current_user.can?(:update_issue, project)
+ def can_update_issue?
+ can?(current_user, :update_issue, project)
end
end
end
diff --git a/app/services/zoom_notes_service.rb b/app/services/zoom_notes_service.rb
deleted file mode 100644
index 983a7fcacd1..00000000000
--- a/app/services/zoom_notes_service.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-class ZoomNotesService
- def initialize(issue, project, current_user, old_description: nil)
- @issue = issue
- @project = project
- @current_user = current_user
- @old_description = old_description
- end
-
- def execute
- return if @issue.description == @old_description
-
- if zoom_link_added?
- zoom_link_added_notification
- elsif zoom_link_removed?
- zoom_link_removed_notification
- end
- end
-
- private
-
- def zoom_link_added?
- has_zoom_link?(@issue.description) && !has_zoom_link?(@old_description)
- end
-
- def zoom_link_removed?
- !has_zoom_link?(@issue.description) && has_zoom_link?(@old_description)
- end
-
- def has_zoom_link?(text)
- Gitlab::ZoomLinkExtractor.new(text).match?
- end
-
- def zoom_link_added_notification
- SystemNoteService.zoom_link_added(@issue, @project, @current_user)
- end
-
- def zoom_link_removed_notification
- SystemNoteService.zoom_link_removed(@issue, @project, @current_user)
- end
-end
diff --git a/app/views/clusters/clusters/index.html.haml b/app/views/clusters/clusters/index.html.haml
index 9bab3bf56aa..049010cadf4 100644
--- a/app/views/clusters/clusters/index.html.haml
+++ b/app/views/clusters/clusters/index.html.haml
@@ -16,7 +16,7 @@
.bs-callout.bs-callout-info
= s_('ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters.')
%strong
- = link_to _('More information'), help_page_path('user/group/clusters/', anchor: 'cluster-precedence')
+ = link_to _('More information'), help_page_path('user/group/clusters/index', anchor: 'cluster-precedence')
.clusters-table.js-clusters-list
.gl-responsive-table-row.table-row-header{ role: "row" }
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index 6e5e4607232..a952db0eea3 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,26 +1,8 @@
- page_title _('Contributors')
-.js-graphs-show{ 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) }
- .sub-header-block
- .tree-ref-holder.inline.vertical-align-middle
- = render 'shared/ref_switcher', destination: 'graphs'
- = link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn'
+.sub-header-block.bg-gray-light.gl-p-3
+ .tree-ref-holder.inline.vertical-align-middle
+ = render 'shared/ref_switcher', destination: 'graphs'
+ = link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn'
- .loading-graph
- .center
- %h3.page-title
- %i.fa.fa-spinner.fa-spin
- = s_('ContributorsPage|Building repository graph.')
- %p.slead
- = s_('ContributorsPage|Please wait a moment, this page will automatically refresh when ready.')
-
- .stat-graph.hide
- .header.clearfix
- %h3#date_header.page-title
- %p.light
- = s_('ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits.') % { branch_name: @ref }
- %input#brush_change{ :type => "hidden" }
- .graphs.row
- #contributors-master.svg-w-100
- #contributors.clearfix
- %ol.contributors-list.svg-w-100.row
+.js-contributors-graph{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json),'data-project-branch': current_ref }
diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml
index fc20bc52d1c..1e7903535c6 100644
--- a/app/views/projects/services/edit.html.haml
+++ b/app/views/projects/services/edit.html.haml
@@ -1,6 +1,7 @@
-- breadcrumb_title s_("ProjectService|Integrations")
+- breadcrumb_title @service.title
- page_title @service.title, s_("ProjectService|Services")
- add_to_breadcrumbs(s_("ProjectService|Settings"), edit_project_path(@project))
+- add_to_breadcrumbs(s_("ProjectService|Integrations"), namespace_project_settings_integrations_path)
= render 'deprecated_message' if @service.deprecation_message
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index eae2a491ceb..84198489e41 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -1,5 +1,5 @@
- users = capture_haml do
- - if search_tabs?(:members)
+ - if show_user_search_tab?
= search_filter_link 'users', _("Users")
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller