diff options
Diffstat (limited to 'app')
19 files changed, 450 insertions, 144 deletions
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue index 61a66513838..b7d9600ec40 100644 --- a/app/assets/javascripts/blob/components/blob_header.vue +++ b/app/assets/javascripts/blob/components/blob_header.vue @@ -2,8 +2,7 @@ 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'; +import { SIMPLE_BLOB_VIEWER } from './constants'; export default { components: { @@ -26,10 +25,15 @@ export default { required: false, default: false, }, + activeViewerType: { + type: String, + required: false, + default: SIMPLE_BLOB_VIEWER, + }, }, data() { return { - activeViewer: this.blob.richViewer ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER, + viewer: this.hideViewerSwitcher ? null : this.activeViewerType, }; }, computed: { @@ -40,19 +44,16 @@ export default { return !this.hideDefaultActions; }, }, - created() { - if (this.showViewerSwitcher) { - eventHub.$on('switch-viewer', this.setActiveViewer); - } - }, - beforeDestroy() { - if (this.showViewerSwitcher) { - eventHub.$off('switch-viewer', this.setActiveViewer); - } + watch: { + viewer(newVal, oldVal) { + if (!this.hideViewerSwitcher && newVal !== oldVal) { + this.$emit('viewer-changed', newVal); + } + }, }, methods: { - setActiveViewer(viewer) { - this.activeViewer = viewer; + proxyCopyRequest() { + this.$emit('copy'); }, }, }; @@ -66,11 +67,16 @@ export default { </blob-filepath> <div class="file-actions d-none d-sm-block"> - <viewer-switcher v-if="showViewerSwitcher" :blob="blob" :active-viewer="activeViewer" /> + <viewer-switcher v-if="showViewerSwitcher" v-model="viewer" /> <slot name="actions"></slot> - <default-actions v-if="showDefaultActions" :blob="blob" :active-viewer="activeViewer" /> + <default-actions + v-if="showDefaultActions" + :raw-path="blob.rawPath" + :active-viewer="viewer" + @copy="proxyCopyRequest" + /> </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 e526fae0dba..f5157fba819 100644 --- a/app/assets/javascripts/blob/components/blob_header_default_actions.vue +++ b/app/assets/javascripts/blob/components/blob_header_default_actions.vue @@ -7,7 +7,6 @@ import { RICH_BLOB_VIEWER, SIMPLE_BLOB_VIEWER, } from './constants'; -import eventHub from '../event_hub'; export default { components: { @@ -19,8 +18,8 @@ export default { GlTooltip: GlTooltipDirective, }, props: { - blob: { - type: Object, + rawPath: { + type: String, required: true, }, activeViewer: { @@ -30,11 +29,8 @@ export default { }, }, computed: { - rawUrl() { - return this.blob.rawPath; - }, downloadUrl() { - return `${this.blob.rawPath}?inline=false`; + return `${this.rawPath}?inline=false`; }, copyDisabled() { return this.activeViewer === RICH_BLOB_VIEWER; @@ -42,7 +38,7 @@ export default { }, methods: { requestCopyContents() { - eventHub.$emit('copy'); + this.$emit('copy'); }, }, BTN_COPY_CONTENTS_TITLE, @@ -65,7 +61,7 @@ export default { v-gl-tooltip.hover :aria-label="$options.BTN_RAW_TITLE" :title="$options.BTN_RAW_TITLE" - :href="rawUrl" + :href="rawPath" target="_blank" > <gl-icon name="doc-code" :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 13ea87c99b1..689fa7638f0 100644 --- a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue +++ b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue @@ -6,7 +6,6 @@ import { SIMPLE_BLOB_VIEWER, SIMPLE_BLOB_VIEWER_TITLE, } from './constants'; -import eventHub from '../event_hub'; export default { components: { @@ -18,11 +17,7 @@ export default { GlTooltip: GlTooltipDirective, }, props: { - blob: { - type: Object, - required: true, - }, - activeViewer: { + value: { type: String, default: SIMPLE_BLOB_VIEWER, required: false, @@ -30,16 +25,16 @@ export default { }, computed: { isSimpleViewer() { - return this.activeViewer === SIMPLE_BLOB_VIEWER; + return this.value === SIMPLE_BLOB_VIEWER; }, isRichViewer() { - return this.activeViewer === RICH_BLOB_VIEWER; + return this.value === RICH_BLOB_VIEWER; }, }, methods: { switchToViewer(viewer) { - if (viewer !== this.activeViewer) { - eventHub.$emit('switch-viewer', viewer); + if (viewer !== this.value) { + this.$emit('input', viewer); } }, }, diff --git a/app/assets/javascripts/blob/event_hub.js b/app/assets/javascripts/blob/event_hub.js deleted file mode 100644 index 0948c2e5352..00000000000 --- a/app/assets/javascripts/blob/event_hub.js +++ /dev/null @@ -1,3 +0,0 @@ -import Vue from 'vue'; - -export default new Vue(); diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index 8c84b98a108..0fab3ee0f3b 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -1,11 +1,11 @@ import Vue from 'vue'; -import VueRouter from 'vue-router'; +import IdeRouter from '~/ide/ide_router_extension'; import { joinPaths } from '~/lib/utils/url_utility'; import flash from '~/flash'; import store from './stores'; import { __ } from '~/locale'; -Vue.use(VueRouter); +Vue.use(IdeRouter); /** * Routes below /-/ide/: @@ -33,7 +33,7 @@ const EmptyRouterComponent = { }, }; -const router = new VueRouter({ +const router = new IdeRouter({ mode: 'history', base: joinPaths(gon.relative_url_root || '', '/-/ide/'), routes: [ diff --git a/app/assets/javascripts/ide/ide_router_extension.js b/app/assets/javascripts/ide/ide_router_extension.js new file mode 100644 index 00000000000..a146aca7283 --- /dev/null +++ b/app/assets/javascripts/ide/ide_router_extension.js @@ -0,0 +1,21 @@ +import VueRouter from 'vue-router'; +import { escapeFileUrl } from '~/lib/utils/url_utility'; + +// To allow special characters (like "#," for example) in the branch names, we +// should encode all the locations before those get processed by History API. +// Otherwise, paths get messed up so that the router receives incorrect +// branchid. The only way to do it consistently and in a more or less +// future-proof manner is, unfortunately, to monkey-patch VueRouter or, as +// suggested here, achieve the same more reliably by subclassing VueRouter and +// update the methods, used in WebIDE. +// +// More context: https://gitlab.com/gitlab-org/gitlab/issues/35473 + +export default class IDERouter extends VueRouter { + push(location, onComplete, onAbort) { + super.push(escapeFileUrl(location), onComplete, onAbort); + } + resolve(to, current, append) { + return super.resolve(escapeFileUrl(to), current, append); + } +} diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 267b49e9d98..1ff4f7bab97 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -194,12 +194,14 @@ export function redirectTo(url) { return window.location.assign(url); } +export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/'); + export function webIDEUrl(route = undefined) { let returnUrl = `${gon.relative_url_root || ''}/-/ide/`; if (route) { returnUrl += `project${route.replace(new RegExp(`^${gon.relative_url_root || ''}`), '')}`; } - return returnUrl; + return escapeFileUrl(returnUrl); } /** @@ -313,8 +315,6 @@ export const setUrlParams = (params, url = window.location.href, clearParams = f return urlObj.toString(); }; -export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/'); - export function urlIsDifferent(url, compare = String(window.location)) { return url !== compare; } diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js index 1b94fb06107..803f4e37705 100644 --- a/app/assets/javascripts/pages/projects/graphs/charts/index.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -1,47 +1,15 @@ -import $ from 'jquery'; -import Chart from 'chart.js'; -import { barChartOptions, pieChartOptions } from '~/lib/utils/chart_utils'; +import Vue from 'vue'; +import { __ } from '~/locale'; +import { GlColumnChart } from '@gitlab/ui/dist/charts'; +import SeriesDataMixin from './series_data_mixin'; document.addEventListener('DOMContentLoaded', () => { - const projectChartData = JSON.parse(document.getElementById('projectChartData').innerHTML); + const languagesContainer = document.getElementById('js-languages-chart'); + const monthContainer = document.getElementById('js-month-chart'); + const weekdayContainer = document.getElementById('js-weekday-chart'); + const hourContainer = document.getElementById('js-hour-chart'); - const barChart = (selector, data) => { - // get selector by context - const ctx = selector.get(0).getContext('2d'); - // pointing parent container to make chart.js inherit its width - const container = $(selector).parent(); - selector.attr('width', $(container).width()); - - // Scale fonts if window width lower than 768px (iPad portrait) - const shouldAdjustFontSize = window.innerWidth < 768; - return new Chart(ctx, { - type: 'bar', - data, - options: barChartOptions(shouldAdjustFontSize), - }); - }; - - const pieChart = (context, data) => { - const options = pieChartOptions(); - - return new Chart(context, { - type: 'pie', - data, - options, - }); - }; - - const chartData = data => ({ - labels: Object.keys(data), - datasets: [ - { - backgroundColor: 'rgba(220,220,220,0.5)', - borderColor: 'rgba(220,220,220,1)', - borderWidth: 1, - data: Object.values(data), - }, - ], - }); + const LANGUAGE_CHART_HEIGHT = 300; const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => { if (firstDayOfWeek === 0) { @@ -58,28 +26,115 @@ document.addEventListener('DOMContentLoaded', () => { }, {}); }; - const hourData = chartData(projectChartData.hour); - barChart($('#hour-chart'), hourData); - - const weekDays = reorderWeekDays(projectChartData.weekDays, gon.first_day_of_week); - const dayData = chartData(weekDays); - barChart($('#weekday-chart'), dayData); + // eslint-disable-next-line no-new + new Vue({ + el: languagesContainer, + components: { + GlColumnChart, + }, + data() { + return { + chartData: JSON.parse(languagesContainer.dataset.chartData), + }; + }, + computed: { + seriesData() { + return { full: this.chartData.map(d => [d.label, d.value]) }; + }, + }, + render(h) { + return h(GlColumnChart, { + props: { + data: this.seriesData, + xAxisTitle: __('Used programming language'), + yAxisTitle: __('Percentage'), + xAxisType: 'category', + }, + attrs: { + height: LANGUAGE_CHART_HEIGHT, + }, + }); + }, + }); - const monthData = chartData(projectChartData.month); - barChart($('#month-chart'), monthData); + // eslint-disable-next-line no-new + new Vue({ + el: monthContainer, + components: { + GlColumnChart, + }, + mixins: [SeriesDataMixin], + data() { + return { + chartData: JSON.parse(monthContainer.dataset.chartData), + }; + }, + render(h) { + return h(GlColumnChart, { + props: { + data: this.seriesData, + xAxisTitle: __('Day of month'), + yAxisTitle: __('No. of commits'), + xAxisType: 'category', + }, + }); + }, + }); - const data = { - datasets: [ - { - data: projectChartData.languages.map(x => x.value), - backgroundColor: projectChartData.languages.map(x => x.color), - hoverBackgroundColor: projectChartData.languages.map(x => x.highlight), + // eslint-disable-next-line no-new + new Vue({ + el: weekdayContainer, + components: { + GlColumnChart, + }, + data() { + return { + chartData: JSON.parse(weekdayContainer.dataset.chartData), + }; + }, + computed: { + seriesData() { + const weekDays = reorderWeekDays(this.chartData, gon.first_day_of_week); + const data = Object.keys(weekDays).reduce((acc, key) => { + acc.push([key, weekDays[key]]); + return acc; + }, []); + return { full: data }; }, - ], - labels: projectChartData.languages.map(x => x.label), - }; - const ctx = $('#languages-chart') - .get(0) - .getContext('2d'); - pieChart(ctx, data); + }, + render(h) { + return h(GlColumnChart, { + props: { + data: this.seriesData, + xAxisTitle: __('Weekday'), + yAxisTitle: __('No. of commits'), + xAxisType: 'category', + }, + }); + }, + }); + + // eslint-disable-next-line no-new + new Vue({ + el: hourContainer, + components: { + GlColumnChart, + }, + mixins: [SeriesDataMixin], + data() { + return { + chartData: JSON.parse(hourContainer.dataset.chartData), + }; + }, + render(h) { + return h(GlColumnChart, { + props: { + data: this.seriesData, + xAxisTitle: __('Hour (UTC)'), + yAxisTitle: __('No. of commits'), + xAxisType: 'category', + }, + }); + }, + }); }); diff --git a/app/assets/javascripts/pages/projects/graphs/charts/series_data_mixin.js b/app/assets/javascripts/pages/projects/graphs/charts/series_data_mixin.js new file mode 100644 index 00000000000..941427a1ac3 --- /dev/null +++ b/app/assets/javascripts/pages/projects/graphs/charts/series_data_mixin.js @@ -0,0 +1,11 @@ +export default { + computed: { + seriesData() { + const data = Object.keys(this.chartData).reduce((acc, key) => { + acc.push([key, this.chartData[key]]); + return acc; + }, []); + return { full: data }; + }, + }, +}; diff --git a/app/controllers/projects/alerting/notifications_controller.rb b/app/controllers/projects/alerting/notifications_controller.rb new file mode 100644 index 00000000000..1fe31863469 --- /dev/null +++ b/app/controllers/projects/alerting/notifications_controller.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Projects + module Alerting + class NotificationsController < Projects::ApplicationController + respond_to :json + + skip_before_action :verify_authenticity_token + skip_before_action :project + + prepend_before_action :repository, :project_without_auth + + def create + token = extract_alert_manager_token(request) + result = notify_service.execute(token) + + head(response_status(result)) + end + + private + + def project_without_auth + @project ||= Project + .find_by_full_path("#{params[:namespace_id]}/#{params[:project_id]}") + end + + def extract_alert_manager_token(request) + Doorkeeper::OAuth::Token.from_bearer_authorization(request) + end + + def notify_service + Projects::Alerting::NotifyService + .new(project, current_user, notification_payload) + end + + def response_status(result) + return :ok if result.success? + + result.http_status + end + + def notification_payload + params.permit![:notification] + end + end + end +end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 7c0f4da355d..77a320f8925 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -27,7 +27,7 @@ module BlobHelper "#{current_user.namespace.full_path}/#{project.path}" end - segments = [ide_path, 'project', project_path, 'edit', ref] + segments = [ide_path, 'project', project_path, 'edit', encode_ide_path(ref)] segments.concat(['-', encode_ide_path(path)]) if path.present? File.join(segments) end diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 152aa7b3218..fcbfda8fbc2 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -77,7 +77,11 @@ class ContainerRepository < ApplicationRecord end def delete_tag_by_digest(digest) - client.delete_repository_tag(self.path, digest) + client.delete_repository_tag_by_digest(self.path, digest) + end + + def delete_tag_by_name(name) + client.delete_repository_tag_by_name(self.path, name) end def self.build_from_path(path) diff --git a/app/models/project.rb b/app/models/project.rb index 44701ef792a..b2ac9c99ab6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -138,6 +138,7 @@ class Project < ApplicationRecord has_many :boards # Project services + has_one :alerts_service has_one :campfire_service has_one :discord_service has_one :drone_ci_service @@ -2330,6 +2331,10 @@ class Project < ApplicationRecord protected_branches.limit(limit) end + def alerts_service_activated? + false + end + private def closest_namespace_setting(name) diff --git a/app/models/project_services/alerts_service.rb b/app/models/project_services/alerts_service.rb new file mode 100644 index 00000000000..2f7902d9617 --- /dev/null +++ b/app/models/project_services/alerts_service.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'securerandom' + +class AlertsService < Service + has_one :data, class_name: 'AlertsServiceData', autosave: true, + inverse_of: :service, foreign_key: :service_id + + attribute :token, :string + delegate :token, :token=, :token_changed?, :token_was, to: :data + + validates :token, presence: true, if: :activated? + + before_validation :prevent_token_assignment + before_validation :ensure_token, if: :activated? + + def url + url_helpers.project_alerts_notify_url(project, format: :json) + end + + def json_fields + super + %w(token) + end + + def editable? + false + end + + def show_active_box? + false + end + + def can_test? + false + end + + def title + _('Alerts endpoint') + end + + def description + _('Receive alerts on GitLab from any source') + end + + def detailed_description + description + end + + def self.to_param + 'alerts' + end + + def self.supported_events + %w() + end + + def data + super || build_data + end + + private + + def prevent_token_assignment + self.token = token_was if token.present? && token_changed? + end + + def ensure_token + self.token = generate_token if token.blank? + end + + def generate_token + SecureRandom.hex + end + + def url_helpers + Gitlab::Routing.url_helpers + end +end diff --git a/app/models/project_services/alerts_service_data.rb b/app/models/project_services/alerts_service_data.rb new file mode 100644 index 00000000000..5a52ed83455 --- /dev/null +++ b/app/models/project_services/alerts_service_data.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'securerandom' + +class AlertsServiceData < ApplicationRecord + belongs_to :service, class_name: 'AlertsService' + + validates :service, presence: true + + attr_encrypted :token, + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-gcm' +end diff --git a/app/services/projects/alerting/notify_service.rb b/app/services/projects/alerting/notify_service.rb new file mode 100644 index 00000000000..4ca3b154e4b --- /dev/null +++ b/app/services/projects/alerting/notify_service.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Projects + module Alerting + class NotifyService < BaseService + include Gitlab::Utils::StrongMemoize + + def execute(token) + return forbidden unless alerts_service_activated? + return unauthorized unless valid_token?(token) + + process_incident_issues + + ServiceResponse.success + rescue Gitlab::Alerting::NotificationPayloadParser::BadPayloadError + bad_request + end + + private + + delegate :alerts_service, :alerts_service_activated?, to: :project + + def process_incident_issues + IncidentManagement::ProcessAlertWorker + .perform_async(project.id, parsed_payload) + end + + def parsed_payload + Gitlab::Alerting::NotificationPayloadParser.call(params.to_h) + end + + def valid_token?(token) + token == alerts_service.token + end + + def bad_request + ServiceResponse.error(message: 'Bad Request', http_status: 400) + end + + def unauthorized + ServiceResponse.error(message: 'Unauthorized', http_status: 401) + end + + def forbidden + ServiceResponse.error(message: 'Forbidden', http_status: 403) + end + end + end +end diff --git a/app/services/projects/container_repository/delete_tags_service.rb b/app/services/projects/container_repository/delete_tags_service.rb index 88ff3c2c9df..d19f275e928 100644 --- a/app/services/projects/container_repository/delete_tags_service.rb +++ b/app/services/projects/container_repository/delete_tags_service.rb @@ -14,12 +14,25 @@ module Projects private + # Delete tags by name with a single DELETE request. This is only supported + # by the GitLab Container Registry fork. See + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23325 for details. + def fast_delete(container_repository, tag_names) + deleted_tags = tag_names.select do |name| + container_repository.delete_tag_by_name(name) + end + + deleted_tags.any? ? success(deleted: deleted_tags) : error('could not delete tags') + end + # Replace a tag on the registry with a dummy tag. # This is a hack as the registry doesn't support deleting individual # tags. This code effectively pushes a dummy image and assigns the tag to it. # This way when the tag is deleted only the dummy image is affected. + # This is used to preverse compatibility with third-party registries that + # don't support fast delete. # See https://gitlab.com/gitlab-org/gitlab/issues/15737 for a discussion - def smart_delete(container_repository, tag_names) + def slow_delete(container_repository, tag_names) # generates the blobs for the dummy image dummy_manifest = container_repository.client.generate_empty_manifest(container_repository.path) return error('could not generate manifest') if dummy_manifest.nil? @@ -36,6 +49,15 @@ module Projects end end + def smart_delete(container_repository, tag_names) + fast_delete_enabled = Feature.enabled?(:container_registry_fast_tag_delete, default_enabled: true) + if fast_delete_enabled && container_repository.client.supports_tag_delete? + fast_delete(container_repository, tag_names) + else + slow_delete(container_repository, tag_names) + end + end + # update the manifests of the tags with the new dummy image def replace_tag_manifests(container_repository, dummy_manifest, tag_names) deleted_tags = {} diff --git a/app/services/projects/lsif_data_service.rb b/app/services/projects/lsif_data_service.rb index 00103f364bf..1282a0736e7 100644 --- a/app/services/projects/lsif_data_service.rb +++ b/app/services/projects/lsif_data_service.rb @@ -2,7 +2,8 @@ module Projects class LsifDataService - attr_reader :file, :project, :path, :commit_id + attr_reader :file, :project, :path, :commit_id, + :docs, :doc_ranges, :ranges, :def_refs CACHE_EXPIRE_IN = 1.hour @@ -14,19 +15,18 @@ module Projects end def execute - docs, doc_ranges, ranges = - fetch_data.values_at('docs', 'doc_ranges', 'ranges') - - doc_id = doc_id_from(docs) + fetch_data! doc_ranges[doc_id]&.map do |range_id| - line_data, column_data = ranges[range_id]['loc'] + location, ref_id = ranges[range_id].values_at('loc', 'ref_id') + line_data, column_data = location { start_line: line_data.first, end_line: line_data.last, start_char: column_data.first, - end_char: column_data.last + end_char: column_data.last, + definition_url: definition_url_for(def_refs[ref_id]) } end end @@ -47,8 +47,17 @@ module Projects end end - def doc_id_from(docs) - docs.reduce(nil) do |doc_id, (id, doc_path)| + def fetch_data! + data = fetch_data + + @docs = data['docs'] + @doc_ranges = data['doc_ranges'] + @ranges = data['ranges'] + @def_refs = data['def_refs'] + end + + def doc_id + @doc_id ||= docs.reduce(nil) do |doc_id, (id, doc_path)| next doc_id unless doc_path =~ /#{path}$/ if doc_id.nil? || docs[doc_id].size > doc_path.size @@ -58,5 +67,24 @@ module Projects doc_id end end + + def dir_absolute_path + @dir_absolute_path ||= docs[doc_id]&.delete_suffix(path) + end + + def definition_url_for(ref_id) + return unless range = ranges[ref_id] + + def_doc_id, location = range.values_at('doc_id', 'loc') + localized_doc_url = docs[def_doc_id].delete_prefix(dir_absolute_path) + + # location is stored as [[start_line, end_line], [start_char, end_char]] + start_line = location.first.first + + line_anchor = "L#{start_line + 1}" + definition_ref_path = [commit_id, localized_doc_url].join('/') + + Gitlab::Routing.url_helpers.project_blob_path(project, definition_ref_path, anchor: line_anchor) + end end end diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml index 93a43b5d1ea..b38449b3ab9 100644 --- a/app/views/projects/graphs/charts.html.haml +++ b/app/views/projects/graphs/charts.html.haml @@ -7,20 +7,7 @@ %p = _("Measured in bytes of code. Excludes generated and vendored code.") - .row - .col-md-4 - %ul.bordered-list - - @languages.each do |language| - %li - %span{ style: "color: #{language[:color]}" } - = icon('circle') - - = language[:label] - .float-right - = language[:value] - \% - .col-md-8 - %canvas#languages-chart{ height: 400 } + #js-languages-chart{ data: { chart_data: @languages.to_json.html_safe } } .repo-charts .sub-header-block.border-top @@ -60,27 +47,18 @@ %p.slead = _("Commits per day of month") %div - %canvas#month-chart + #js-month-chart{ data: { chart_data: @commits_per_month.to_json.html_safe } } .row .col-md-6 .col-md-6 %p.slead = _("Commits per weekday") %div - %canvas#weekday-chart + #js-weekday-chart{ data: { chart_data: @commits_per_week_days.to_json.html_safe } } .row .col-md-6 .col-md-6 %p.slead = _("Commits per day hour (UTC)") %div - %canvas#hour-chart - --# haml-lint:disable InlineJavaScript -%script#projectChartData{ type: "application/json" } - - projectChartData = {}; - - projectChartData['hour'] = @commits_per_time - - projectChartData['weekDays'] = @commits_per_week_days - - projectChartData['month'] = @commits_per_month - - projectChartData['languages'] = @languages - = projectChartData.to_json.html_safe + #js-hour-chart{ data: { chart_data: @commits_per_time.to_json.html_safe } } |