diff options
53 files changed, 525 insertions, 440 deletions
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index df5119ec64e..3b6825376ad 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -8.7.0 +8.8.0 diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index 789a057caf8..137cc7b4669 100644 --- a/app/assets/javascripts/behaviors/markdown/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -18,9 +18,7 @@ $.fn.renderGFM = function renderGFM() { highlightCurrentUser(this.find('.gfm-project_member').get()); initUserPopovers(this.find('.gfm-project_member').get()); initMRPopovers(this.find('.gfm-merge_request').get()); - if (gon.features && gon.features.gfmEmbeddedMetrics) { - renderMetrics(this.find('.js-render-metrics').get()); - } + renderMetrics(this.find('.js-render-metrics').get()); return this; }; diff --git a/app/assets/javascripts/lib/utils/forms.js b/app/assets/javascripts/lib/utils/forms.js new file mode 100644 index 00000000000..106209a2f3a --- /dev/null +++ b/app/assets/javascripts/lib/utils/forms.js @@ -0,0 +1,12 @@ +export const serializeFormEntries = entries => + entries.reduce((acc, { name, value }) => Object.assign(acc, { [name]: value }), {}); + +export const serializeForm = form => { + const fdata = new FormData(form); + const entries = Array.from(fdata.keys()).map(key => { + const val = fdata.getAll(key); + return { name: key, value: val.length === 1 ? val[0] : val }; + }); + + return serializeFormEntries(entries); +}; diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index edf9423c74c..5b950f8c966 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -1,6 +1,7 @@ <script> import { __ } from '~/locale'; -import { GlLink } from '@gitlab/ui'; +import { mapState } from 'vuex'; +import { GlLink, GlButton } from '@gitlab/ui'; import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; import dateFormat from 'dateformat'; import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils'; @@ -15,6 +16,7 @@ let debouncedResize; export default { components: { GlAreaChart, + GlButton, GlChartSeriesLabel, GlLink, Icon, @@ -67,6 +69,7 @@ export default { }; }, computed: { + ...mapState('monitoringDashboard', ['exportMetricsToCsvEnabled']), chartData() { // Transforms & supplements query data to render appropriate labels & styles // Input: [{ queryAttributes1 }, { queryAttributes2 }] @@ -176,6 +179,18 @@ export default { yAxisLabel() { return `${this.graphData.y_label}`; }, + csvText() { + const chartData = this.chartData[0].data; + const header = `timestamp,${this.graphData.y_label}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings + return chartData.reduce((csv, data) => { + const row = data.join(','); + return `${csv}${row}\r\n`; + }, header); + }, + downloadLink() { + const data = new Blob([this.csvText], { type: 'text/plain' }); + return window.URL.createObjectURL(data); + }, }, watch: { containerWidth: 'onResize', @@ -240,10 +255,20 @@ export default { </script> <template> - <div class="col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']"> - <div class="prometheus-graph" :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }"> + <div class="prometheus-graph col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']"> + <div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }"> <div class="prometheus-graph-header"> <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5> + <gl-button + v-if="exportMetricsToCsvEnabled" + :href="downloadLink" + :title="__('Download CSV')" + :aria-label="__('Download CSV')" + style="margin-left: 200px;" + download="chart_metrics.csv" + > + {{ __('Download CSV') }} + </gl-button> <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div> </div> <gl-area-chart diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 5892c18ac91..782e4310f3e 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -3,6 +3,7 @@ import { GlButton, GlDropdown, GlDropdownItem, + GlFormGroup, GlModal, GlModalDirective, GlTooltipDirective, @@ -34,6 +35,7 @@ export default { GlButton, GlDropdown, GlDropdownItem, + GlFormGroup, GlModal, }, directives: { @@ -278,107 +280,136 @@ export default { <template> <div class="prometheus-graphs"> - <div class="gl-p-3 border-bottom bg-gray-light d-flex justify-content-between"> - <div - v-if="environmentsEndpoint" - class="dropdowns d-flex align-items-center justify-content-between" - > - <div v-if="multipleDashboardsEnabled" class="d-flex align-items-center"> - <label class="mb-0">{{ __('Dashboard') }}</label> - <gl-dropdown - class="ml-2 mr-3 js-dashboards-dropdown" - toggle-class="dropdown-menu-toggle" - :text="selectedDashboardText" + <div class="gl-p-3 pb-0 border-bottom bg-gray-light"> + <div class="row"> + <template v-if="environmentsEndpoint"> + <gl-form-group + v-if="multipleDashboardsEnabled" + :label="__('Dashboard')" + label-size="sm" + label-for="monitor-dashboards-dropdown" + class="col-sm-12 col-md-4 col-lg-2" > - <gl-dropdown-item - v-for="dashboard in allDashboards" - :key="dashboard.path" - :active="dashboard.path === currentDashboard" - active-class="is-active" - :href="`?dashboard=${dashboard.path}`" + <gl-dropdown + id="monitor-dashboards-dropdown" + class="mb-0 d-flex js-dashboards-dropdown" + toggle-class="dropdown-menu-toggle" + :text="selectedDashboardText" > - {{ dashboard.display_name || dashboard.path }} - </gl-dropdown-item> - </gl-dropdown> - </div> - <div class="d-flex align-items-center"> - <strong>{{ s__('Metrics|Environment') }}</strong> - <gl-dropdown - class="prepend-left-10 js-environments-dropdown" - toggle-class="dropdown-menu-toggle" - :text="currentEnvironmentName" - :disabled="environments.length === 0" + <gl-dropdown-item + v-for="dashboard in allDashboards" + :key="dashboard.path" + :active="dashboard.path === currentDashboard" + active-class="is-active" + :href="`?dashboard=${dashboard.path}`" + >{{ dashboard.display_name || dashboard.path }}</gl-dropdown-item + > + </gl-dropdown> + </gl-form-group> + + <gl-form-group + :label="s__('Metrics|Environment')" + label-size="sm" + label-for="monitor-environments-dropdown" + class="col-sm-6 col-md-4 col-lg-2" > - <gl-dropdown-item - v-for="environment in environments" - :key="environment.id" - :active="environment.name === currentEnvironmentName" - active-class="is-active" - :href="environment.metrics_path" - >{{ environment.name }}</gl-dropdown-item + <gl-dropdown + id="monitor-environments-dropdown" + class="mb-0 d-flex js-environments-dropdown" + toggle-class="dropdown-menu-toggle" + :text="currentEnvironmentName" + :disabled="environments.length === 0" > - </gl-dropdown> - </div> - <div v-if="!showEmptyState" class="d-flex align-items-center prepend-left-8"> - <strong>{{ s__('Metrics|Show last') }}</strong> - <gl-dropdown - class="prepend-left-10 js-time-window-dropdown" - toggle-class="dropdown-menu-toggle" - :text="selectedTimeWindow" + <gl-dropdown-item + v-for="environment in environments" + :key="environment.id" + :active="environment.name === currentEnvironmentName" + active-class="is-active" + :href="environment.metrics_path" + >{{ environment.name }}</gl-dropdown-item + > + </gl-dropdown> + </gl-form-group> + + <gl-form-group + v-if="!showEmptyState" + :label="s__('Metrics|Show last')" + label-size="sm" + label-for="monitor-time-window-dropdown" + class="col-sm-6 col-md-4 col-lg-2" > - <gl-dropdown-item - v-for="(value, key) in timeWindows" - :key="key" - :active="activeTimeWindow(key)" - :href="setTimeWindowParameter(key)" - active-class="active" - >{{ value }}</gl-dropdown-item + <gl-dropdown + id="monitor-time-window-dropdown" + class="mb-0 d-flex js-time-window-dropdown" + toggle-class="dropdown-menu-toggle" + :text="selectedTimeWindow" > - </gl-dropdown> - </div> - </div> - <div class="d-flex"> - <div v-if="addingMetricsAvailable"> - <gl-button - v-gl-modal="$options.addMetric.modalId" - class="js-add-metric-button text-success border-success" - >{{ $options.addMetric.title }}</gl-button - > - <gl-modal - ref="addMetricModal" - :modal-id="$options.addMetric.modalId" - :title="$options.addMetric.title" - > - <form ref="customMetricsForm" :action="customMetricsPath" method="post"> - <custom-metrics-form-fields - :validate-query-path="validateQueryPath" - form-operation="post" - @formValidation="setFormValidity" - /> - </form> - <div slot="modal-footer"> - <gl-button @click="hideAddMetricModal">{{ __('Cancel') }}</gl-button> - <gl-button - :disabled="!formIsValid" - variant="success" - @click="submitCustomMetricsForm" - >{{ __('Save changes') }}</gl-button + <gl-dropdown-item + v-for="(value, key) in timeWindows" + :key="key" + :active="activeTimeWindow(key)" + :href="setTimeWindowParameter(key)" + active-class="active" + >{{ value }}</gl-dropdown-item > - </div> - </gl-modal> - </div> - <gl-button - v-if="externalDashboardUrl.length" - class="js-external-dashboard-link prepend-left-8" - variant="primary" - :href="externalDashboardUrl" - target="_blank" + </gl-dropdown> + </gl-form-group> + </template> + + <gl-form-group + v-if="addingMetricsAvailable || externalDashboardUrl.length" + label-for="prometheus-graphs-dropdown-buttons" + class="dropdown-buttons col-lg d-lg-flex align-items-end" > - {{ __('View full dashboard') }} - <icon name="external-link" /> - </gl-button> + <div id="prometheus-graphs-dropdown-buttons"> + <gl-button + v-if="addingMetricsAvailable" + v-gl-modal="$options.addMetric.modalId" + class="mr-2 mt-1 js-add-metric-button text-success border-success" + > + {{ $options.addMetric.title }} + </gl-button> + <gl-modal + v-if="addingMetricsAvailable" + ref="addMetricModal" + :modal-id="$options.addMetric.modalId" + :title="$options.addMetric.title" + > + <form ref="customMetricsForm" :action="customMetricsPath" method="post"> + <custom-metrics-form-fields + :validate-query-path="validateQueryPath" + form-operation="post" + @formValidation="setFormValidity" + /> + </form> + <div slot="modal-footer"> + <gl-button @click="hideAddMetricModal">{{ __('Cancel') }}</gl-button> + <gl-button + :disabled="!formIsValid" + variant="success" + @click="submitCustomMetricsForm" + > + {{ __('Save changes') }} + </gl-button> + </div> + </gl-modal> + + <gl-button + v-if="externalDashboardUrl.length" + class="mt-1 js-external-dashboard-link" + variant="primary" + :href="externalDashboardUrl" + target="_blank" + rel="noopener noreferrer" + > + {{ __('View full dashboard') }} + <icon name="external-link" /> + </gl-button> + </div> + </gl-form-group> </div> </div> + <div v-if="!showEmptyState"> <graph-group v-for="(groupData, index) in groups" diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index c0fee1ebb99..366034becd0 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -13,6 +13,7 @@ export default (props = {}) => { prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint, multipleDashboardsEnabled: gon.features.environmentMetricsShowMultipleDashboards, additionalPanelTypesEnabled: gon.features.environmentMetricsAdditionalPanelTypes, + exportMetricsToCsvEnabled: gon.features.exportMetricsToCsvEnabled, }); } diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index 0cbad179f17..a9c491c7c6c 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -37,11 +37,17 @@ export const setEndpoints = ({ commit }, endpoints) => { export const setFeatureFlags = ( { commit }, - { prometheusEndpointEnabled, multipleDashboardsEnabled, additionalPanelTypesEnabled }, + { + prometheusEndpointEnabled, + multipleDashboardsEnabled, + additionalPanelTypesEnabled, + exportMetricsToCsvEnabled, + }, ) => { commit(types.SET_DASHBOARD_ENABLED, prometheusEndpointEnabled); commit(types.SET_MULTIPLE_DASHBOARDS_ENABLED, multipleDashboardsEnabled); commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled); + commit(types.SET_EXPORT_METRICS_TO_CSV_ENABLED, exportMetricsToCsvEnabled); }; export const setShowErrorBanner = ({ commit }, enabled) => { diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js index 4b1aadbcf05..9ec8214b167 100644 --- a/app/assets/javascripts/monitoring/stores/mutation_types.js +++ b/app/assets/javascripts/monitoring/stores/mutation_types.js @@ -17,3 +17,4 @@ export const SET_ENDPOINTS = 'SET_ENDPOINTS'; export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE'; export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE'; export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER'; +export const SET_EXPORT_METRICS_TO_CSV_ENABLED = 'SET_EXPORT_METRICS_TO_CSV_ENABLED'; diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js index b19520d6638..a2dceb21fc0 100644 --- a/app/assets/javascripts/monitoring/stores/mutations.js +++ b/app/assets/javascripts/monitoring/stores/mutations.js @@ -99,4 +99,7 @@ export default { [types.SET_SHOW_ERROR_BANNER](state, enabled) { state.showErrorBanner = enabled; }, + [types.SET_EXPORT_METRICS_TO_CSV_ENABLED](state, enabled) { + state.exportMetricsToCsvEnabled = enabled; + }, }; diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js index 440bdc951e0..a14a25e3a20 100644 --- a/app/assets/javascripts/monitoring/stores/state.js +++ b/app/assets/javascripts/monitoring/stores/state.js @@ -10,6 +10,7 @@ export default () => ({ useDashboardEndpoint: false, multipleDashboardsEnabled: false, additionalPanelTypesEnabled: false, + exportMetricsToCsvEnabled: false, emptyState: 'gettingStarted', showEmptyState: true, showErrorBanner: true, diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index f0d529758d5..332b6811af6 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -1,9 +1,10 @@ -/* eslint-disable func-names, no-var, no-return-assign, one-var, object-shorthand, vars-on-top */ +/* eslint-disable func-names, no-var, no-return-assign, object-shorthand, vars-on-top */ import $ from 'jquery'; import Cookies from 'js-cookie'; import { __ } from '~/locale'; -import { visitUrl } from '~/lib/utils/url_utility'; +import { visitUrl, mergeUrlParams } from '~/lib/utils/url_utility'; +import { serializeForm } from '~/lib/utils/forms'; import axios from '~/lib/utils/axios_utils'; import flash from '~/flash'; import projectSelect from '../../project_select'; @@ -107,9 +108,10 @@ export default class Project { refLink.href = '#'; return $('.js-project-refs-dropdown').each(function() { - var $dropdown, selected; - $dropdown = $(this); - selected = $dropdown.data('selected'); + var $dropdown = $(this); + var selected = $dropdown.data('selected'); + var fieldName = $dropdown.data('fieldName'); + var shouldVisit = Boolean($dropdown.data('visit')); return $dropdown.glDropdown({ data(term, callback) { axios @@ -127,7 +129,7 @@ export default class Project { filterRemote: true, filterByText: true, inputFieldName: $dropdown.data('inputFieldName'), - fieldName: $dropdown.data('fieldName'), + fieldName, renderRow: function(ref) { var li = refListItem.cloneNode(false); @@ -158,15 +160,12 @@ export default class Project { clicked: function(options) { const { e } = options; e.preventDefault(); - if ($('input[name="ref"]').length) { + if ($(`input[name="${fieldName}"]`).length) { var $form = $dropdown.closest('form'); - - var $visit = $dropdown.data('visit'); - var shouldVisit = $visit ? true : $visit; var action = $form.attr('action'); - var divider = action.indexOf('?') === -1 ? '?' : '&'; + if (shouldVisit) { - visitUrl(`${action}${divider}${$form.serialize()}`); + visitUrl(mergeUrlParams(serializeForm($form[0]), action)); } } }, diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js index 7743e05e748..81b6225cb18 100644 --- a/app/assets/javascripts/pages/search/show/search.js +++ b/app/assets/javascripts/pages/search/show/search.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import Flash from '~/flash'; import Api from '~/api'; import { __ } from '~/locale'; +import Project from '~/pages/projects/project'; export default class Search { constructor() { @@ -69,6 +70,8 @@ export default class Search { }, clicked: () => Search.submitSearch(), }); + + Project.initRefSwitcher(); } eventListeners() { diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 3f021a26ec5..e01080b04d6 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -102,7 +102,11 @@ export default { <span v-if="pipeline.flags.detached_merge_request_pipeline" v-gl-tooltip - :title="__('This pipeline is run on the source branch')" + :title=" + __( + 'The code of a detached pipeline is tested against the source branch instead of merged results', + ) + " class="js-pipeline-url-detached badge badge-info" > {{ __('detached') }} diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 262c0bf5ed2..6fc742871e7 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -124,11 +124,6 @@ float: left; padding-left: $gl-padding-8; } - - .section-header ~ .section.line { - margin-left: $gl-padding; - display: block; - } } .build-header { diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss index 05a4cc168a8..72f1b5307ec 100644 --- a/app/assets/stylesheets/pages/prometheus.scss +++ b/app/assets/stylesheets/pages/prometheus.scss @@ -1,17 +1,17 @@ .prometheus-graphs { - .dropdowns { - .dropdown-menu-toggle { - svg { - position: absolute; - right: 5%; - top: 25%; - } + .dropdown-buttons { + > div { + margin-left: auto; } + } - .dropdown-menu-toggle, - .dropdown-menu { - width: 240px; - } + .col-form-label { + line-height: 1; + padding-top: 0; + } + + .form-group { + margin-bottom: map-get($spacing-scale, 3); } } diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index b709ac85e39..df9e55fda2a 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -15,6 +15,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards) push_frontend_feature_flag(:environment_metrics_additional_panel_types) push_frontend_feature_flag(:prometheus_computed_alerts) + push_frontend_feature_flag(:export_metrics_to_csv_enabled) end def index @@ -160,7 +161,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController end def metrics_dashboard - if Feature.enabled?(:gfm_embedded_metrics, project) && params[:embedded] + if params[:embedded] result = dashboard_finder.find( project, current_user, diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb index f85c1a237a6..2cf3278d240 100644 --- a/app/presenters/blob_presenter.rb +++ b/app/presenters/blob_presenter.rb @@ -3,13 +3,12 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated presents :blob - def highlight(since: nil, to: nil, plain: nil) + def highlight(plain: nil) load_all_blob_data Gitlab::Highlight.highlight( blob.path, - limited_blob_data(since: since, to: to), - since: since, + blob.data, language: blob.language_from_gitattributes, plain: plain ) @@ -24,18 +23,4 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated def load_all_blob_data blob.load_all_data! if blob.respond_to?(:load_all_data!) end - - def limited_blob_data(since: nil, to: nil) - return blob.data if since.blank? || to.blank? - - limited_blob_lines(since, to).join - end - - def limited_blob_lines(since, to) - all_lines[since - 1..to - 1] - end - - def all_lines - @all_lines ||= blob.data.lines - end end diff --git a/app/presenters/blobs/unfold_presenter.rb b/app/presenters/blobs/unfold_presenter.rb index f4672d22fc9..21a1e1309e0 100644 --- a/app/presenters/blobs/unfold_presenter.rb +++ b/app/presenters/blobs/unfold_presenter.rb @@ -21,19 +21,20 @@ module Blobs load_all_blob_data @subject = blob + @all_lines = blob.data.lines super(params) if full? - self.attributes = { since: 1, to: all_lines.size, bottom: false, unfold: false, offset: 0, indent: 0 } + self.attributes = { since: 1, to: @all_lines.size, bottom: false, unfold: false, offset: 0, indent: 0 } end end # Returns an array of Gitlab::Diff::Line with match line added def diff_lines - diff_lines = limited_blob_lines(since, to).map.with_index do |line, index| - full_line = line.delete("\n") + diff_lines = lines.map.with_index do |line, index| + full_line = limited_blob_lines[index].delete("\n") - Gitlab::Diff::Line.new(full_line, nil, nil, nil, nil, rich_text: lines[index]) + Gitlab::Diff::Line.new(full_line, nil, nil, nil, nil, rich_text: line) end add_match_line(diff_lines) @@ -42,7 +43,7 @@ module Blobs end def lines - @lines ||= highlight(since: since, to: to).lines.map(&:html_safe) + @lines ||= limit(highlight.lines).map(&:html_safe) end def match_line_text @@ -58,7 +59,7 @@ module Blobs def add_match_line(diff_lines) return unless unfold? - if bottom? && to < all_lines.size + if bottom? && to < @all_lines.size old_pos = to - offset new_pos = to elsif since != 1 @@ -72,5 +73,15 @@ module Blobs bottom? ? diff_lines.push(match_line) : diff_lines.unshift(match_line) end + + def limited_blob_lines + @limited_blob_lines ||= limit(@all_lines) + end + + def limit(lines) + return lines if full? + + lines[since - 1..to - 1] + end end end diff --git a/app/services/members/base_service.rb b/app/services/members/base_service.rb index e78affff797..5d69418fb7d 100644 --- a/app/services/members/base_service.rb +++ b/app/services/members/base_service.rb @@ -51,7 +51,9 @@ module Members def enqueue_delete_todos(member) type = member.is_a?(GroupMember) ? 'Group' : 'Project' # don't enqueue immediately to prevent todos removal in case of a mistake - TodosDestroyer::EntityLeaveWorker.perform_in(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, type) + member.run_after_commit_or_now do + TodosDestroyer::EntityLeaveWorker.perform_in(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, type) + end end end end diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 5d307d6a70d..2b56ada8b73 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -43,7 +43,7 @@ } } Auto DevOps - if @pipeline.detached_merge_request_pipeline? - %span.js-pipeline-url-mergerequest.badge.badge-info.has-tooltip{ title: "This pipeline is run on the source branch" } + %span.js-pipeline-url-mergerequest.badge.badge-info.has-tooltip{ title: "The code of a detached pipeline is tested against the source branch instead of merged results" } detached - if @pipeline.stuck? %span.js-pipeline-url-stuck.badge.badge-warning diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml index 4c4f3e0298b..464db94b7f4 100644 --- a/app/views/search/_form.html.haml +++ b/app/views/search/_form.html.haml @@ -1,6 +1,7 @@ = form_tag search_path, method: :get, class: 'search-page-form js-search-form' do |f| = hidden_field_tag :snippets, params[:snippets] = hidden_field_tag :scope, params[:scope] + = hidden_field_tag :repository_ref, params[:repository_ref] .d-lg-flex.align-items-end .search-field-holder.form-group.mr-lg-1.mb-lg-0 diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index cb8a8a24be8..de9947528cf 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -2,17 +2,25 @@ = render partial: "search/results/empty" = render_if_exists 'shared/promotions/promote_advanced_search' - else - .row-content-block + .row-content-block.d-md-flex.text-left.align-items-center - unless @search_objects.is_a?(Kaminari::PaginatableWithoutCount) = search_entries_info(@search_objects, @scope, @search_term) - unless @show_snippets - if @project - - link_to_project = link_to(@project.full_name, [@project.namespace.becomes(Namespace), @project]) - = _("in project %{link_to_project}").html_safe % { link_to_project: link_to_project } + - link_to_project = link_to(@project.full_name, [@project.namespace.becomes(Namespace), @project], class: 'ml-md-1') + - if @scope == 'blobs' + - repository_ref = params[:repository_ref].to_s.presence || @project.default_branch + = s_("SearchCodeResults|in") + .mx-md-1 + = render partial: "shared/ref_switcher", locals: { ref: repository_ref, form_path: request.fullpath, field_name: 'repository_ref' } + = s_('SearchCodeResults|of %{link_to_project}').html_safe % { link_to_project: link_to_project } + - else + = _("in project %{link_to_project}").html_safe % { link_to_project: link_to_project } - elsif @group - - link_to_group = link_to(@group.name, @group) + - link_to_group = link_to(@group.name, @group, class: 'ml-md-1') = _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group } = render_if_exists 'shared/promotions/promote_advanced_search' + .results.prepend-top-10 - if @scope == 'commits' %ul.content-list.commit-list diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 7cbc5810c10..74e0a088656 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -1,12 +1,19 @@ -- dropdown_toggle_text = @ref || @project.default_branch -= form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do - = hidden_field_tag :destination, destination +- return unless @project + +- ref = local_assigns.fetch(:ref, @ref) +- form_path = local_assigns.fetch(:form_path, switch_project_refs_path(@project)) +- dropdown_toggle_text = ref || @project.default_branch +- field_name = local_assigns.fetch(:field_name, 'ref') + += form_tag form_path, method: :get, class: "project-refs-form" do + - if defined?(destination) + = hidden_field_tag :destination, destination - if defined?(path) = hidden_field_tag :path, path - @options && @options.each do |key, value| = hidden_field_tag key, value, id: nil .dropdown - = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown qa-branches-select" } + = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: field_name, submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown qa-branches-select" } .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging.qa-branches-dropdown{ class: ("dropdown-menu-right" if local_assigns[:align_right]) } .dropdown-page-one = dropdown_title _("Switch branch/tag") diff --git a/changelogs/unreleased/59712-resolve-the-search-problem-issue.yml b/changelogs/unreleased/59712-resolve-the-search-problem-issue.yml new file mode 100644 index 00000000000..964962cb817 --- /dev/null +++ b/changelogs/unreleased/59712-resolve-the-search-problem-issue.yml @@ -0,0 +1,5 @@ +--- +title: Add branch/tags/commits dropdown filter on the search page for searching codes +merge_request: 28282 +author: minghuan lei +type: changed diff --git a/changelogs/unreleased/62137-add-tooltip-to-improve-clarity-of-detached-label-state-in-the-merge-request-pipeline.yml b/changelogs/unreleased/62137-add-tooltip-to-improve-clarity-of-detached-label-state-in-the-merge-request-pipeline.yml new file mode 100644 index 00000000000..ccc3195e6ae --- /dev/null +++ b/changelogs/unreleased/62137-add-tooltip-to-improve-clarity-of-detached-label-state-in-the-merge-request-pipeline.yml @@ -0,0 +1,5 @@ +--- +title: Updated the detached pipeline badge tooltip text to offer a better explanation +merge_request: 31626 +author: +type: other diff --git a/changelogs/unreleased/64730-metrics-dashboard-menu-is-cramped-with-new-features-enabled.yml b/changelogs/unreleased/64730-metrics-dashboard-menu-is-cramped-with-new-features-enabled.yml new file mode 100644 index 00000000000..c564b98bb41 --- /dev/null +++ b/changelogs/unreleased/64730-metrics-dashboard-menu-is-cramped-with-new-features-enabled.yml @@ -0,0 +1,5 @@ +--- +title: Improve layout of dropdowns in the metrics dashboard page +merge_request: 31239 +author: +type: fixed diff --git a/changelogs/unreleased/65152-selective-highlight.yml b/changelogs/unreleased/65152-selective-highlight.yml deleted file mode 100644 index 371dbbd5924..00000000000 --- a/changelogs/unreleased/65152-selective-highlight.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Support selective highlighting of lines -merge_request: 31361 -author: -type: performance diff --git a/changelogs/unreleased/fix-job-log-formatting.yml b/changelogs/unreleased/fix-job-log-formatting.yml new file mode 100644 index 00000000000..0dd545aaecc --- /dev/null +++ b/changelogs/unreleased/fix-job-log-formatting.yml @@ -0,0 +1,5 @@ +--- +title: Fix job logs where style changes were broken down into separate lines +merge_request: 31674 +author: +type: fixed diff --git a/changelogs/unreleased/issue_58494.yml b/changelogs/unreleased/issue_58494.yml new file mode 100644 index 00000000000..c74768fc020 --- /dev/null +++ b/changelogs/unreleased/issue_58494.yml @@ -0,0 +1,5 @@ +--- +title: Prevent turning plain links into embedded when moving issues +merge_request: 31489 +author: +type: fixed diff --git a/changelogs/unreleased/lm-download-csv-of-charts-from-metrics-dashboard.yml b/changelogs/unreleased/lm-download-csv-of-charts-from-metrics-dashboard.yml new file mode 100644 index 00000000000..59f12fca1f1 --- /dev/null +++ b/changelogs/unreleased/lm-download-csv-of-charts-from-metrics-dashboard.yml @@ -0,0 +1,5 @@ +--- +title: Export and download CSV from metrics charts +merge_request: 30760 +author: +type: added diff --git a/changelogs/unreleased/tr-remove-embed-metrics-flag.yml b/changelogs/unreleased/tr-remove-embed-metrics-flag.yml new file mode 100644 index 00000000000..a327a6868d3 --- /dev/null +++ b/changelogs/unreleased/tr-remove-embed-metrics-flag.yml @@ -0,0 +1,5 @@ +--- +title: Link and embed metrics in GitLab Flavored Markdown +merge_request: 31106 +author: +type: added diff --git a/db/migrate/20190806071559_remove_epic_issues_default_relative_position.rb b/db/migrate/20190806071559_remove_epic_issues_default_relative_position.rb new file mode 100644 index 00000000000..f6db90f6637 --- /dev/null +++ b/db/migrate/20190806071559_remove_epic_issues_default_relative_position.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class RemoveEpicIssuesDefaultRelativePosition < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + change_column_null :epic_issues, :relative_position, true + change_column_default :epic_issues, :relative_position, from: 1073741823, to: nil + end +end diff --git a/db/schema.rb b/db/schema.rb index 0e4a3d07fdd..6674f412140 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_08_02_235445) do +ActiveRecord::Schema.define(version: 2019_08_06_071559) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" @@ -1194,7 +1194,7 @@ ActiveRecord::Schema.define(version: 2019_08_02_235445) do create_table "epic_issues", id: :serial, force: :cascade do |t| t.integer "epic_id", null: false t.integer "issue_id", null: false - t.integer "relative_position", default: 1073741823, null: false + t.integer "relative_position" t.index ["epic_id"], name: "index_epic_issues_on_epic_id" t.index ["issue_id"], name: "index_epic_issues_on_issue_id", unique: true end diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index 71891e43dcc..bb1b037c08f 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -59,6 +59,7 @@ module API } override_params = import_params.delete(:override_params) + filter_attributes_using_license!(override_params) if override_params project = ::Projects::GitlabProjectsImportService.new( current_user, project_params, override_params diff --git a/lib/banzai/filter/inline_embeds_filter.rb b/lib/banzai/filter/inline_embeds_filter.rb index 97394fd8f82..9f1ef0796f0 100644 --- a/lib/banzai/filter/inline_embeds_filter.rb +++ b/lib/banzai/filter/inline_embeds_filter.rb @@ -10,8 +10,6 @@ module Banzai # the link, and insert this node after any html content # surrounding the link. def call - return doc unless Feature.enabled?(:gfm_embedded_metrics, context[:project]) - doc.xpath(xpath_search).each do |node| next unless element = element_to_embed(node) diff --git a/lib/banzai/filter/inline_metrics_redactor_filter.rb b/lib/banzai/filter/inline_metrics_redactor_filter.rb index ff91be2cbb7..4d8a5028898 100644 --- a/lib/banzai/filter/inline_metrics_redactor_filter.rb +++ b/lib/banzai/filter/inline_metrics_redactor_filter.rb @@ -13,8 +13,6 @@ module Banzai # uses to identify the embedded content, removing # only unnecessary nodes. def call - return doc unless Feature.enabled?(:gfm_embedded_metrics, context[:project]) - nodes.each do |node| path = paths_by_node[node] user_has_access = user_access_by_path[path] diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb index 2d1c9ac40ae..6b52d6e88e5 100644 --- a/lib/gitlab/gfm/uploads_rewriter.rb +++ b/lib/gitlab/gfm/uploads_rewriter.rb @@ -27,7 +27,15 @@ module Gitlab klass = target_parent.is_a?(Namespace) ? NamespaceFileUploader : FileUploader moved = klass.copy_to(file, target_parent) - moved.markdown_link + + moved_markdown = moved.markdown_link + + # Prevents rewrite of plain links as embedded + if was_embedded?(markdown) + moved_markdown + else + moved_markdown.sub(/\A!/, "") + end end end @@ -43,6 +51,10 @@ module Gitlab referenced_files.compact.select(&:exists?) end + def was_embedded?(markdown) + markdown.starts_with?("!") + end + private def find_file(project, secret, file) diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 41ec8741eb1..92917028851 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -38,11 +38,6 @@ module Gitlab gon.current_user_fullname = current_user.name gon.current_user_avatar_url = current_user.avatar_url end - - # Flag controls a GFM feature used across many routes. - # Pushing the flag from one place simplifies control - # and facilitates easy removal. - push_frontend_feature_flag(:gfm_embedded_metrics) end # Exposes the state of a feature flag to the frontend code. diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index 1f49a26f0a2..381f1dd4e55 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -6,16 +6,15 @@ module Gitlab TIMEOUT_FOREGROUND = 3.seconds MAXIMUM_TEXT_HIGHLIGHT_SIZE = 1.megabyte - def self.highlight(blob_name, blob_content, since: nil, language: nil, plain: false) - new(blob_name, blob_content, since: since, language: language) + def self.highlight(blob_name, blob_content, language: nil, plain: false) + new(blob_name, blob_content, language: language) .highlight(blob_content, continue: false, plain: plain) end attr_reader :blob_name - def initialize(blob_name, blob_content, since: nil, language: nil) + def initialize(blob_name, blob_content, language: nil) @formatter = Rouge::Formatters::HTMLGitlab - @since = since @language = language @blob_name = blob_name @blob_content = blob_content @@ -54,13 +53,13 @@ module Gitlab end def highlight_plain(text) - @formatter.format(Rouge::Lexers::PlainText.lex(text), since: @since).html_safe + @formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe end def highlight_rich(text, continue: true) tag = lexer.tag tokens = lexer.lex(text, continue: continue) - Timeout.timeout(timeout_time) { @formatter.format(tokens, tag: tag, since: @since).html_safe } + Timeout.timeout(timeout_time) { @formatter.format(tokens, tag: tag).html_safe } rescue Timeout::Error => e Gitlab::Sentry.track_exception(e) highlight_plain(text) diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 827f4f77f36..5e77d31760d 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -134,9 +134,11 @@ module Gitlab project.repository.commit(key) if Commit.valid_hash?(key) end + # rubocop: disable CodeReuse/ActiveRecord def project_ids_relation - project + Project.where(id: project).select(:id).reorder(nil) end + # rubocop: enabled CodeReuse/ActiveRecord def filter_milestones_by_project(milestones) return Milestone.none unless Ability.allowed?(@current_user, :read_milestone, @project) diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb index 0d4ac504428..e2a7d3ef5ba 100644 --- a/lib/rouge/formatters/html_gitlab.rb +++ b/lib/rouge/formatters/html_gitlab.rb @@ -8,8 +8,8 @@ module Rouge # Creates a new <tt>Rouge::Formatter::HTMLGitlab</tt> instance. # # [+tag+] The tag (language) of the lexer used to generate the formatted tokens - def initialize(tag: nil, since: nil) - @line_number = since || 1 + def initialize(tag: nil) + @line_number = 1 @tag = tag end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9bc2fd1b8f4..50103c226d4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4007,6 +4007,9 @@ msgstr "" msgid "Download" msgstr "" +msgid "Download CSV" +msgstr "" + msgid "Download artifacts" msgstr "" @@ -9666,6 +9669,12 @@ msgstr "" msgid "SearchAutocomplete|in this project" msgstr "" +msgid "SearchCodeResults|in" +msgstr "" + +msgid "SearchCodeResults|of %{link_to_project}" +msgstr "" + msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for \"%{term}\"" msgstr "" @@ -10888,6 +10897,9 @@ msgstr "" msgid "The character highlighter helps you keep the subject line to %{titleLength} characters and wrap the body at %{bodyLength} so they are readable in git." msgstr "" +msgid "The code of a detached pipeline is tested against the source branch instead of merged results" +msgstr "" + msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." msgstr "" @@ -11383,9 +11395,6 @@ msgstr "" msgid "This page will be removed in a future release." msgstr "" -msgid "This pipeline is run on the source branch" -msgstr "" - msgid "This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}" msgstr "" diff --git a/scripts/utils.sh b/scripts/utils.sh index c1bdef5b847..f0f08e2e1c5 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -158,129 +158,3 @@ function wait_for_job_to_be_done() { echoinfo "The '${job_name}' passed." fi } - -function install_api_client_dependencies_with_apk() { - apk add --update openssl curl jq -} - -function install_api_client_dependencies_with_apt() { - apt update && apt install jq -y -} - -function install_gitlab_gem() { - gem install gitlab --no-document -} - -function echoerr() { - local header="${2}" - - if [ -n "${header}" ]; then - printf "\n\033[0;31m** %s **\n\033[0m" "${1}" >&2; - else - printf "\033[0;31m%s\n\033[0m" "${1}" >&2; - fi -} - -function echoinfo() { - local header="${2}" - - if [ -n "${header}" ]; then - printf "\n\033[0;33m** %s **\n\033[0m" "${1}" >&2; - else - printf "\033[0;33m%s\n\033[0m" "${1}" >&2; - fi -} - -function get_job_id() { - local job_name="${1}" - local query_string="${2:+&${2}}" - local api_token="${API_TOKEN-${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}}" - if [ -z "${api_token}" ]; then - echoerr "Please provide an API token with \$API_TOKEN or \$GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN." - return - fi - - local max_page=3 - local page=1 - - while true; do - local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/pipelines/${CI_PIPELINE_ID}/jobs?per_page=100&page=${page}${query_string}" - echoinfo "GET ${url}" - - local job_id - job_id=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${api_token}" "${url}" | jq "map(select(.name == \"${job_name}\")) | map(.id) | last") - [[ "${job_id}" == "null" && "${page}" -lt "$max_page" ]] || break - - let "page++" - done - - if [[ "${job_id}" == "" ]]; then - echoerr "The '${job_name}' job ID couldn't be retrieved!" - else - echoinfo "The '${job_name}' job ID is ${job_id}" - echo "${job_id}" - fi -} - -function play_job() { - local job_name="${1}" - local job_id - job_id=$(get_job_id "${job_name}" "scope=manual"); - if [ -z "${job_id}" ]; then return; fi - - local api_token="${API_TOKEN-${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}}" - if [ -z "${api_token}" ]; then - echoerr "Please provide an API token with \$API_TOKEN or \$GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN." - return - fi - - local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/jobs/${job_id}/play" - echoinfo "POST ${url}" - - local job_url - job_url=$(curl --silent --show-error --request POST --header "PRIVATE-TOKEN: ${api_token}" "${url}" | jq ".web_url") - echoinfo "Manual job '${job_name}' started at: ${job_url}" -} - -function wait_for_job_to_be_done() { - local job_name="${1}" - local query_string="${2}" - local job_id - job_id=$(get_job_id "${job_name}" "${query_string}") - if [ -z "${job_id}" ]; then return; fi - - local api_token="${API_TOKEN-${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}}" - if [ -z "${api_token}" ]; then - echoerr "Please provide an API token with \$API_TOKEN or \$GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN." - return - fi - - echoinfo "Waiting for the '${job_name}' job to finish..." - - local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/jobs/${job_id}" - echoinfo "GET ${url}" - - # In case the job hasn't finished yet. Keep trying until the job times out. - local interval=30 - local elapsed_seconds=0 - while true; do - local job_status - job_status=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${api_token}" "${url}" | jq ".status" | sed -e s/\"//g) - [[ "${job_status}" == "pending" || "${job_status}" == "running" ]] || break - - printf "." - let "elapsed_seconds+=interval" - sleep ${interval} - done - - local elapsed_minutes=$((elapsed_seconds / 60)) - echoinfo "Waited '${job_name}' for ${elapsed_minutes} minutes." - - if [[ "${job_status}" == "failed" ]]; then - echoerr "The '${job_name}' failed." - elif [[ "${job_status}" == "manual" ]]; then - echoinfo "The '${job_name}' is manual." - else - echoinfo "The '${job_name}' passed." - fi -} diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index b3852355d77..71ee1fd03bf 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -613,31 +613,13 @@ describe Projects::EnvironmentsController do end end - shared_examples_for 'dashboard cannot be embedded' do - context 'when the embedded flag is included' do - let(:dashboard_params) { { format: :json, embedded: true } } - - it_behaves_like 'the default dashboard' - end - end - let(:dashboard_params) { { format: :json } } it_behaves_like 'the default dashboard' it_behaves_like 'dashboard can be specified' it_behaves_like 'dashboard can be embedded' - context 'when multiple dashboards is enabled and embedding metrics is disabled' do - before do - stub_feature_flags(gfm_embedded_metrics: false) - end - - it_behaves_like 'the default dashboard' - it_behaves_like 'dashboard can be specified' - it_behaves_like 'dashboard cannot be embedded' - end - - context 'when multiple dashboards is disabled and embedding metrics is enabled' do + context 'when multiple dashboards is disabled' do before do stub_feature_flags(environment_metrics_show_multiple_dashboards: false) end @@ -646,19 +628,6 @@ describe Projects::EnvironmentsController do it_behaves_like 'dashboard cannot be specified' it_behaves_like 'dashboard can be embedded' end - - context 'when multiple dashboards and embedding metrics are disabled' do - before do - stub_feature_flags( - environment_metrics_show_multiple_dashboards: false, - gfm_embedded_metrics: false - ) - end - - it_behaves_like 'the default dashboard' - it_behaves_like 'dashboard cannot be specified' - it_behaves_like 'dashboard cannot be embedded' - end end describe 'GET #search' do diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb index 71af75640de..5a60991c1bf 100644 --- a/spec/features/search/user_searches_for_code_spec.rb +++ b/spec/features/search/user_searches_for_code_spec.rb @@ -6,6 +6,21 @@ describe 'User searches for code' do let(:user) { create(:user) } let(:project) { create(:project, :repository, namespace: user.namespace) } + def submit_search(search, with_send_keys: false) + page.within('.search') do + field = find_field('search') + field.fill_in(with: search) + + if with_send_keys + field.send_keys(:enter) + else + click_button("Go") + end + end + + click_link('Code') + end + context 'when signed in' do before do project.add_maintainer(user) @@ -15,12 +30,7 @@ describe 'User searches for code' do it 'finds a file' do visit(project_path(project)) - page.within('.search') do - fill_in('search', with: 'application.js') - click_button('Go') - end - - click_link('Code') + submit_search('application.js') expect(page).to have_selector('.file-content .code') expect(page).to have_selector("span.line[lang='javascript']") @@ -48,6 +58,50 @@ describe 'User searches for code' do end end end + + context 'search code within refs', :js do + let(:ref_name) { 'v1.0.0' } + + before do + visit(project_tree_path(project, ref_name)) + submit_search('gitlab-grack', with_send_keys: true) + end + + it 'shows ref switcher in code result summary' do + expect(find('.js-project-refs-dropdown')).to have_text(ref_name) + end + it 'persists branch name across search' do + find('.btn-search').click + expect(find('.js-project-refs-dropdown')).to have_text(ref_name) + end + + # this example is use to test the desgine that the refs is not + # only repersent the branch as well as the tags. + it 'ref swither list all the branchs and tags' do + find('.js-project-refs-dropdown').click + expect(find('.dropdown-page-one .dropdown-content')).to have_link('sha-starting-with-large-number') + expect(find('.dropdown-page-one .dropdown-content')).to have_link('v1.0.0') + end + + it 'search result changes when refs switched' do + expect(find('.search-results')).not_to have_content('path = gitlab-grack') + find('.js-project-refs-dropdown').click + find('.dropdown-page-one .dropdown-content').click_link('master') + expect(find('.search-results')).to have_content('path = gitlab-grack') + end + end + + it 'no ref switcher shown in issue result summary', :js do + issue = create(:issue, title: 'test', project: project) + visit(project_tree_path(project)) + submit_search('test', with_send_keys: true) + expect(page).to have_selector('.js-project-refs-dropdown') + page.within('.search-filter') do + click_link('Issues') + end + expect(find(:css, '.search-results')).to have_link(issue.title) + expect(page).not_to have_selector('.js-project-refs-dropdown') + end end context 'when signed out' do @@ -58,8 +112,7 @@ describe 'User searches for code' do end it 'finds code' do - fill_in('search', with: 'rspec') - click_button('Go') + submit_search('rspec') page.within('.results') do expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions') diff --git a/spec/frontend/lib/utils/forms_spec.js b/spec/frontend/lib/utils/forms_spec.js new file mode 100644 index 00000000000..cac17235f0d --- /dev/null +++ b/spec/frontend/lib/utils/forms_spec.js @@ -0,0 +1,74 @@ +import { serializeForm } from '~/lib/utils/forms'; + +describe('lib/utils/forms', () => { + const createDummyForm = inputs => { + const form = document.createElement('form'); + + form.innerHTML = inputs + .map(({ type, name, value }) => { + let str = ``; + if (type === 'select') { + str = `<select name="${name}">`; + value.forEach(v => { + if (v.length > 0) { + str += `<option value="${v}"></option> `; + } + }); + str += `</select>`; + } else { + str = `<input type="${type}" name="${name}" value="${value}" checked/>`; + } + return str; + }) + .join(''); + + return form; + }; + + describe('serializeForm', () => { + it('returns an object of key values from inputs', () => { + const form = createDummyForm([ + { type: 'text', name: 'foo', value: 'foo-value' }, + { type: 'text', name: 'bar', value: 'bar-value' }, + ]); + + const data = serializeForm(form); + + expect(data).toEqual({ + foo: 'foo-value', + bar: 'bar-value', + }); + }); + + it('works with select', () => { + const form = createDummyForm([ + { type: 'select', name: 'foo', value: ['foo-value1', 'foo-value2'] }, + { type: 'text', name: 'bar', value: 'bar-value1' }, + ]); + + const data = serializeForm(form); + + expect(data).toEqual({ + foo: 'foo-value1', + bar: 'bar-value1', + }); + }); + + it('works with multiple inputs of the same name', () => { + const form = createDummyForm([ + { type: 'checkbox', name: 'foo', value: 'foo-value3' }, + { type: 'checkbox', name: 'foo', value: 'foo-value2' }, + { type: 'checkbox', name: 'foo', value: 'foo-value1' }, + { type: 'text', name: 'bar', value: 'bar-value2' }, + { type: 'text', name: 'bar', value: 'bar-value1' }, + ]); + + const data = serializeForm(form); + + expect(data).toEqual({ + foo: ['foo-value3', 'foo-value2', 'foo-value1'], + bar: ['bar-value2', 'bar-value1'], + }); + }); + }); +}); diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js index d3a76f33679..4541119dd2e 100644 --- a/spec/javascripts/monitoring/charts/area_spec.js +++ b/spec/javascripts/monitoring/charts/area_spec.js @@ -1,9 +1,9 @@ import { shallowMount } from '@vue/test-utils'; +import { createStore } from '~/monitoring/stores'; import { GlLink } from '@gitlab/ui'; import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper'; import Area from '~/monitoring/components/charts/area.vue'; -import { createStore } from '~/monitoring/stores'; import * as types from '~/monitoring/stores/mutation_types'; import { TEST_HOST } from 'spec/test_constants'; import MonitoringMock, { deploymentData } from '../mock_data'; @@ -17,13 +17,14 @@ describe('Area component', () => { let mockGraphData; let areaChart; let spriteSpy; + let store; beforeEach(() => { - const store = createStore(); - + store = createStore(); store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data); store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData); + store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: true }); [mockGraphData] = store.state.monitoringDashboard.groups[0].metrics; areaChart = shallowMount(Area, { @@ -36,6 +37,7 @@ describe('Area component', () => { slots: { default: mockWidgets, }, + store, }); spriteSpy = spyOnDependency(Area, 'getSvgIconPathContent').and.callFake( @@ -107,6 +109,16 @@ describe('Area component', () => { }); }); + describe('when exportMetricsToCsvEnabled is disabled', () => { + beforeEach(() => { + store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: false }); + }); + + it('does not render the Download CSV button', () => { + expect(areaChart.contains('glbutton-stub')).toBe(false); + }); + }); + describe('methods', () => { describe('formatTooltipText', () => { const mockDate = deploymentData[0].created_at; @@ -252,5 +264,23 @@ describe('Area component', () => { expect(areaChart.vm.yAxisLabel).toBe('CPU'); }); }); + + describe('csvText', () => { + it('converts data from json to csv', () => { + const header = `timestamp,${mockGraphData.y_label}`; + const data = mockGraphData.queries[0].result[0].values; + const firstRow = `${data[0][0]},${data[0][1]}`; + + expect(areaChart.vm.csvText).toMatch(`^${header}\r\n${firstRow}`); + }); + }); + + describe('downloadLink', () => { + it('produces a link to download metrics as csv', () => { + const link = areaChart.vm.downloadLink; + + expect(link).toContain('blob:'); + }); + }); }); }); diff --git a/spec/lib/banzai/filter/inline_metrics_filter_spec.rb b/spec/lib/banzai/filter/inline_metrics_filter_spec.rb index 772c94e3180..542a9ced6d7 100644 --- a/spec/lib/banzai/filter/inline_metrics_filter_spec.rb +++ b/spec/lib/banzai/filter/inline_metrics_filter_spec.rb @@ -40,16 +40,6 @@ describe Banzai::Filter::InlineMetricsFilter do expect(doc.at_css('p').to_s).to include paragraph expect(doc.at_css('.js-render-metrics')).to be_present end - - context 'when the feature is disabled' do - before do - stub_feature_flags(gfm_embedded_metrics: false) - end - - it 'does nothing' do - expect(doc.to_s).to eq input - end - end end end end diff --git a/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb b/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb index fb2186e9d12..a99cd7d6076 100644 --- a/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb +++ b/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb @@ -11,16 +11,6 @@ describe Banzai::Filter::InlineMetricsRedactorFilter do let(:input) { %(<a href="#{url}">example</a>) } let(:doc) { filter(input) } - context 'when the feature is disabled' do - before do - stub_feature_flags(gfm_embedded_metrics: false) - end - - it 'does nothing' do - expect(doc.to_s).to eq input - end - end - context 'without a metrics charts placeholder' do it 'leaves regular non-metrics links unchanged' do expect(doc.to_s).to eq input diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb index ef52a25f47e..d24f5c45107 100644 --- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb @@ -55,6 +55,17 @@ describe Gitlab::Gfm::UploadsRewriter do end end + it 'does not rewrite plain links as embedded' do + embedded_link = image_uploader.markdown_link + plain_image_link = embedded_link.sub(/\A!/, "") + text = "#{plain_image_link} and #{embedded_link}" + + moved_text = described_class.new(text, old_project, user).rewrite(new_project) + + expect(moved_text.scan(/!\[.*?\]/).count).to eq(1) + expect(moved_text.scan(/\A\[.*?\]/).count).to eq(1) + end + context "file are stored locally" do include_examples "files are accessible" end diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index a410e4eab45..4676db6b8d8 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -62,14 +62,6 @@ describe Gitlab::Highlight do expect(lines[2].text).to eq(' """') end - context 'since param is present' do - it 'highlights with the LC starting from "since" param' do - lines = described_class.highlight(file_name, content, since: 2).lines - - expect(lines[0]).to include('LC2') - end - end - context 'diff highlighting' do let(:file_name) { 'test.diff' } let(:content) { "+aaa\n+bbb\n- ccc\n ddd\n"} diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb index 95db2ba6a0d..eacf383be7d 100644 --- a/spec/presenters/blob_presenter_spec.rb +++ b/spec/presenters/blob_presenter_spec.rb @@ -28,70 +28,24 @@ describe BlobPresenter, :seed_helper do subject { described_class.new(blob) } it 'returns highlighted content' do - expect(Gitlab::Highlight) - .to receive(:highlight) - .with( - 'files/ruby/regex.rb', - git_blob.data, - since: nil, - plain: nil, - language: nil - ) + expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: nil) subject.highlight end it 'returns plain content when :plain is true' do - expect(Gitlab::Highlight) - .to receive(:highlight) - .with( - 'files/ruby/regex.rb', - git_blob.data, - since: nil, - plain: true, - language: nil - ) + expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: true, language: nil) subject.highlight(plain: true) end - context '"since" and "to" are present' do - before do - allow(git_blob) - .to receive(:data) - .and_return("line one\nline two\nline 3\nline 4") - end - - it 'returns limited highlighted content' do - expect(Gitlab::Highlight) - .to receive(:highlight) - .with( - 'files/ruby/regex.rb', - "line two\nline 3\n", - since: 2, - language: nil, - plain: nil - ) - - subject.highlight(since: 2, to: 3) - end - end - context 'gitlab-language contains a match' do before do allow(blob).to receive(:language_from_gitattributes).and_return('ruby') end it 'passes language to inner call' do - expect(Gitlab::Highlight) - .to receive(:highlight) - .with( - 'files/ruby/regex.rb', - git_blob.data, - since: nil, - plain: nil, - language: 'ruby' - ) + expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: 'ruby') subject.highlight end diff --git a/spec/support/shared_examples/relative_positioning_shared_examples.rb b/spec/support/shared_examples/relative_positioning_shared_examples.rb index 9837ba806db..b7382cea93c 100644 --- a/spec/support/shared_examples/relative_positioning_shared_examples.rb +++ b/spec/support/shared_examples/relative_positioning_shared_examples.rb @@ -17,8 +17,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do describe '.move_nulls_to_end' do it 'moves items with null relative_position to the end' do - skip("#{item1} has a default relative position") if item1.relative_position - skip("#{item2} has a default relative position") if item2.relative_position + item1.update!(relative_position: nil) + item2.update!(relative_position: nil) described_class.move_nulls_to_end([item1, item2]) @@ -28,7 +28,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do end it 'moves the item near the start position when there are no existing positions' do - skip("#{item1} has a default relative position") if item1.relative_position + item1.update!(relative_position: nil) described_class.move_nulls_to_end([item1]) |