summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/blob/components/blob_content.vue51
-rw-r--r--app/assets/javascripts/blob/components/blob_content_error.vue15
-rw-r--r--app/assets/javascripts/blob/components/blob_header_default_actions.vue7
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js3
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/blobviewer.fragment.graphql3
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_view.vue44
-rw-r--r--app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql13
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/constants.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/index.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue68
-rw-r--r--app/assets/stylesheets/framework/highlight.scss15
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss8
-rw-r--r--app/controllers/concerns/metrics_dashboard.rb12
-rw-r--r--app/helpers/environments_helper.rb3
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml5
-rw-r--r--app/views/projects/snippets/show.html.haml6
-rw-r--r--app/views/snippets/show.html.haml6
-rw-r--r--changelogs/unreleased/196184-convert-insights-to-echarts.yml5
-rw-r--r--changelogs/unreleased/196797-blob-content.yml5
-rw-r--r--changelogs/unreleased/204740-migrate-fa-spinner-to-spinner-within-app-views-projects-cycle_anal.yml5
-rw-r--r--changelogs/unreleased/defect-safari-line-numbers-height.yml5
-rw-r--r--doc/api/epics.md2
-rw-r--r--doc/ci/review_apps/img/enable_review_app_v12_8.pngbin0 -> 46424 bytes
-rw-r--r--doc/ci/review_apps/index.md24
-rw-r--r--doc/user/permissions.md1
-rw-r--r--doc/user/project/insights/img/insights_example_pie_chart.pngbin6985 -> 0 bytes
-rw-r--r--doc/user/project/insights/index.md3
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb38
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb59
-rw-r--r--qa/qa/support/repeater.rb4
-rw-r--r--qa/qa/support/wait_for_requests.rb2
-rw-r--r--qa/qa/support/waiter.rb25
-rw-r--r--qa/spec/support/repeater_spec.rb30
-rw-r--r--qa/spec/support/waiter_spec.rb5
-rw-r--r--spec/controllers/concerns/metrics_dashboard_spec.rb20
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb2
-rw-r--r--spec/frontend/blob/components/blob_content_error_spec.js27
-rw-r--r--spec/frontend/blob/components/blob_content_spec.js70
-rw-r--r--spec/frontend/blob/components/blob_header_default_actions_spec.js9
-rw-r--r--spec/frontend/blob/components/mock_data.js46
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js117
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap86
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js81
-rw-r--r--spec/helpers/environments_helper_spec.rb2
47 files changed, 858 insertions, 126 deletions
diff --git a/app/assets/javascripts/blob/components/blob_content.vue b/app/assets/javascripts/blob/components/blob_content.vue
new file mode 100644
index 00000000000..2639a099093
--- /dev/null
+++ b/app/assets/javascripts/blob/components/blob_content.vue
@@ -0,0 +1,51 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
+import BlobContentError from './blob_content_error.vue';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ BlobContentError,
+ },
+ props: {
+ content: {
+ type: String,
+ default: '',
+ required: false,
+ },
+ loading: {
+ type: Boolean,
+ default: true,
+ required: false,
+ },
+ activeViewer: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ viewer() {
+ switch (this.activeViewer.type) {
+ case 'rich':
+ return RichViewer;
+ default:
+ return SimpleViewer;
+ }
+ },
+ viewerError() {
+ return this.activeViewer.renderError;
+ },
+ },
+};
+</script>
+<template>
+ <div class="blob-viewer" :data-type="activeViewer.type">
+ <gl-loading-icon v-if="loading" size="md" color="dark" class="my-4 mx-auto" />
+
+ <template v-else>
+ <blob-content-error v-if="viewerError" :viewer-error="viewerError" />
+ <component :is="viewer" v-else ref="contentViewer" :content="content" />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/blob/components/blob_content_error.vue b/app/assets/javascripts/blob/components/blob_content_error.vue
new file mode 100644
index 00000000000..0f1af0a962d
--- /dev/null
+++ b/app/assets/javascripts/blob/components/blob_content_error.vue
@@ -0,0 +1,15 @@
+<script>
+export default {
+ props: {
+ viewerError: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div class="file-content code">
+ <div class="text-center py-4" v-html="viewerError"></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 f5157fba819..6b38b871606 100644
--- a/app/assets/javascripts/blob/components/blob_header_default_actions.vue
+++ b/app/assets/javascripts/blob/components/blob_header_default_actions.vue
@@ -36,11 +36,6 @@ export default {
return this.activeViewer === RICH_BLOB_VIEWER;
},
},
- methods: {
- requestCopyContents() {
- this.$emit('copy');
- },
- },
BTN_COPY_CONTENTS_TITLE,
BTN_DOWNLOAD_TITLE,
BTN_RAW_TITLE,
@@ -53,7 +48,7 @@ export default {
:aria-label="$options.BTN_COPY_CONTENTS_TITLE"
:title="$options.BTN_COPY_CONTENTS_TITLE"
:disabled="copyDisabled"
- @click="requestCopyContents"
+ data-clipboard-target="#blob-code-content"
>
<gl-icon name="copy-to-clipboard" :size="14" />
</gl-button>
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index 43f5e9954ce..6d2b11e39d3 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import Vue from 'vue';
import Cookies from 'js-cookie';
-import { GlEmptyState } from '@gitlab/ui';
+import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import filterMixins from 'ee_else_ce/analytics/cycle_analytics/mixins/filter_mixins';
import Flash from '../flash';
import { __ } from '~/locale';
@@ -28,6 +28,7 @@ export default () => {
name: 'CycleAnalytics',
components: {
GlEmptyState,
+ GlLoadingIcon,
banner,
'stage-issue-component': stageComponent,
'stage-plan-component': stageComponent,
diff --git a/app/assets/javascripts/graphql_shared/fragments/blobviewer.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/blobviewer.fragment.graphql
index 64c894df115..b202ed12f80 100644
--- a/app/assets/javascripts/graphql_shared/fragments/blobviewer.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/blobviewer.fragment.graphql
@@ -1,6 +1,7 @@
fragment BlobViewer on SnippetBlobViewer {
collapsed
- loadingPartialName
renderError
tooLarge
+ type
+ fileType
}
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
index 49e0ef35cb8..4703a940e08 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
@@ -2,13 +2,19 @@
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import { SNIPPET_VISIBILITY_PUBLIC } from '../constants';
import BlobHeader from '~/blob/components/blob_header.vue';
-import GetSnippetBlobQuery from '../queries/snippet.blob.query.graphql';
+import BlobContent from '~/blob/components/blob_content.vue';
import { GlLoadingIcon } from '@gitlab/ui';
+import GetSnippetBlobQuery from '../queries/snippet.blob.query.graphql';
+import GetBlobContent from '../queries/snippet.blob.content.query.graphql';
+
+import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
+
export default {
components: {
BlobEmbeddable,
BlobHeader,
+ BlobContent,
GlLoadingIcon,
},
apollo: {
@@ -20,6 +26,23 @@ export default {
};
},
update: data => data.snippets.edges[0].node.blob,
+ result(res) {
+ const viewer = res.data.snippets.edges[0].node.blob.richViewer
+ ? RICH_BLOB_VIEWER
+ : SIMPLE_BLOB_VIEWER;
+ this.switchViewer(viewer, true);
+ },
+ },
+ blobContent: {
+ query: GetBlobContent,
+ variables() {
+ return {
+ ids: this.snippet.id,
+ rich: this.activeViewerType === RICH_BLOB_VIEWER,
+ };
+ },
+ update: data =>
+ data.snippets.edges[0].node.blob.richData || data.snippets.edges[0].node.blob.plainData,
},
},
props: {
@@ -31,6 +54,8 @@ export default {
data() {
return {
blob: {},
+ blobContent: '',
+ activeViewerType: window.location.hash ? SIMPLE_BLOB_VIEWER : '',
};
},
computed: {
@@ -40,6 +65,18 @@ export default {
isBlobLoading() {
return this.$apollo.queries.blob.loading;
},
+ isContentLoading() {
+ return this.$apollo.queries.blobContent.loading;
+ },
+ viewer() {
+ const { richViewer, simpleViewer } = this.blob;
+ return this.activeViewerType === RICH_BLOB_VIEWER ? richViewer : simpleViewer;
+ },
+ },
+ methods: {
+ switchViewer(newViewer, respectHash = false) {
+ this.activeViewerType = respectHash && window.location.hash ? SIMPLE_BLOB_VIEWER : newViewer;
+ },
},
};
</script>
@@ -49,11 +86,12 @@ export default {
<gl-loading-icon
v-if="isBlobLoading"
:label="__('Loading blob')"
- :size="2"
+ size="lg"
class="prepend-top-20 append-bottom-20"
/>
<article v-else class="file-holder snippet-file-content">
- <blob-header :blob="blob" />
+ <blob-header :blob="blob" :active-viewer-type="viewer.type" @viewer-changed="switchViewer" />
+ <blob-content :loading="isContentLoading" :content="blobContent" :active-viewer="viewer" />
</article>
</div>
</template>
diff --git a/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql b/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql
new file mode 100644
index 00000000000..889a88dd93c
--- /dev/null
+++ b/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql
@@ -0,0 +1,13 @@
+query SnippetBlobContent($ids: [ID!], $rich: Boolean!) {
+ snippets(ids: $ids) {
+ edges {
+ node {
+ id
+ blob {
+ richData @include(if: $rich)
+ plainData @skip(if: $rich)
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js b/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js
new file mode 100644
index 00000000000..d4c1808eec2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js
@@ -0,0 +1,3 @@
+export const HIGHLIGHT_CLASS_NAME = 'hll';
+
+export default {};
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/index.js b/app/assets/javascripts/vue_shared/components/blob_viewers/index.js
new file mode 100644
index 00000000000..72fba9392f9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/index.js
@@ -0,0 +1,4 @@
+import RichViewer from './rich_viewer.vue';
+import SimpleViewer from './simple_viewer.vue';
+
+export { RichViewer, SimpleViewer };
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
new file mode 100644
index 00000000000..582213ee8d3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
@@ -0,0 +1,8 @@
+export default {
+ props: {
+ content: {
+ type: String,
+ required: true,
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
new file mode 100644
index 00000000000..b3a1df8f303
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
@@ -0,0 +1,10 @@
+<script>
+import ViewerMixin from './mixins';
+
+export default {
+ mixins: [ViewerMixin],
+};
+</script>
+<template>
+ <div v-html="content"></div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
new file mode 100644
index 00000000000..e64c7132117
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
@@ -0,0 +1,68 @@
+<script>
+import ViewerMixin from './mixins';
+import { GlIcon } from '@gitlab/ui';
+import { HIGHLIGHT_CLASS_NAME } from './constants';
+
+export default {
+ components: {
+ GlIcon,
+ },
+ mixins: [ViewerMixin],
+ data() {
+ return {
+ highlightedLine: null,
+ };
+ },
+ computed: {
+ lineNumbers() {
+ return this.content.split('\n').length;
+ },
+ },
+ mounted() {
+ const { hash } = window.location;
+ if (hash) this.scrollToLine(hash, true);
+ },
+ methods: {
+ scrollToLine(hash, scroll = false) {
+ const lineToHighlight = hash && this.$el.querySelector(hash);
+ const currentlyHighlighted = this.highlightedLine;
+ if (lineToHighlight) {
+ if (currentlyHighlighted) {
+ currentlyHighlighted.classList.remove(HIGHLIGHT_CLASS_NAME);
+ }
+
+ lineToHighlight.classList.add(HIGHLIGHT_CLASS_NAME);
+ this.highlightedLine = lineToHighlight;
+ if (scroll) {
+ lineToHighlight.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }
+ }
+ },
+ },
+ userColorScheme: window.gon.user_color_scheme,
+};
+</script>
+<template>
+ <div
+ class="file-content code js-syntax-highlight qa-file-content"
+ :class="$options.userColorScheme"
+ >
+ <div class="line-numbers">
+ <a
+ v-for="line in lineNumbers"
+ :id="`L${line}`"
+ :key="line"
+ class="diff-line-num js-line-number"
+ :href="`#LC${line}`"
+ :data-line-number="line"
+ @click="scrollToLine(`#LC${line}`)"
+ >
+ <gl-icon :size="12" name="link" />
+ {{ line }}
+ </a>
+ </div>
+ <div class="blob-content">
+ <pre class="code highlight"><code id="blob-code-content" v-html="content"></code></pre>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index ee6e53adaf7..73a2170fc68 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -30,7 +30,6 @@
.line {
display: block;
width: 100%;
- min-height: 1.5em;
padding-left: 10px;
padding-right: 10px;
white-space: pre;
@@ -48,10 +47,10 @@
font-family: $monospace-font;
display: block;
font-size: $code-font-size !important;
- min-height: 1.5em;
white-space: nowrap;
- i {
+ i,
+ svg {
float: left;
margin-top: 3px;
margin-right: 5px;
@@ -62,12 +61,20 @@
&:focus {
outline: none;
- i {
+ i,
+ svg {
visibility: visible;
}
}
}
}
+
+ pre .line,
+ .line-numbers a {
+ font-size: 0.8125rem;
+ line-height: 1.1875rem;
+ min-height: 1.1875rem;
+ }
}
// Vertically aligns <table> line numbers (eg. blame view)
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 76cd4f34865..21b23feb57f 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -109,14 +109,6 @@
top: $gl-padding-top;
}
- .fa-spinner {
- font-size: 28px;
- position: relative;
- margin-left: -20px;
- left: 50%;
- margin-top: 36px;
- }
-
.stage-panel-body {
display: flex;
flex-wrap: wrap;
diff --git a/app/controllers/concerns/metrics_dashboard.rb b/app/controllers/concerns/metrics_dashboard.rb
index dc392147cb8..fa79f3bc4e6 100644
--- a/app/controllers/concerns/metrics_dashboard.rb
+++ b/app/controllers/concerns/metrics_dashboard.rb
@@ -5,6 +5,7 @@
module MetricsDashboard
include RenderServiceResults
include ChecksCollaboration
+ include EnvironmentsHelper
extend ActiveSupport::Concern
@@ -15,8 +16,9 @@ module MetricsDashboard
metrics_dashboard_params.to_h.symbolize_keys
)
- if include_all_dashboards? && result
- result[:all_dashboards] = all_dashboards
+ if result
+ result[:all_dashboards] = all_dashboards if include_all_dashboards?
+ result[:metrics_data] = metrics_data(project_for_dashboard, environment_for_dashboard) if project_for_dashboard && environment_for_dashboard
end
respond_to do |format|
@@ -76,10 +78,14 @@ module MetricsDashboard
defined?(project) ? project : nil
end
+ def environment_for_dashboard
+ defined?(environment) ? environment : nil
+ end
+
def dashboard_success_response(result)
{
status: :ok,
- json: result.slice(:all_dashboards, :dashboard, :status)
+ json: result.slice(:all_dashboards, :dashboard, :status, :metrics_data)
}
end
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 579a57a27de..fd330d4efd9 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
module EnvironmentsHelper
+ include ActionView::Helpers::AssetUrlHelper
prepend_if_ee('::EE::EnvironmentsHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
def environments_list_data
@@ -21,7 +22,7 @@ module EnvironmentsHelper
{
"settings-path" => edit_project_service_path(project, 'prometheus'),
"clusters-path" => project_clusters_path(project),
- "current-environment-name": environment.name,
+ "current-environment-name" => environment.name,
"documentation-path" => help_page_path('administration/monitoring/prometheus/index.md'),
"empty-getting-started-svg-path" => image_path('illustrations/monitoring/getting_started.svg'),
"empty-loading-svg-path" => image_path('illustrations/monitoring/loading.svg'),
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index 70409774a50..b0d9dfb0d37 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -5,7 +5,7 @@
%banner{ "v-if" => "!isOverviewDialogDismissed",
"documentation-link": help_page_path('user/analytics/value_stream_analytics.md'),
"v-on:dismiss-overview-dialog" => "dismissOverviewDialog()" }
- = icon("spinner spin", "v-show" => "isLoading")
+ %gl-loading-icon{ "v-show" => "isLoading", "size" => "lg" }
.wrapper{ "v-show" => "!isLoading && !hasError" }
.card
.card-header
@@ -57,8 +57,7 @@
%ul
%stage-nav-item{ "v-for" => "stage in state.stages", ":key" => '`ca-stage-title-${stage.title}`', '@select' => 'selectStage(stage)', ":title" => "stage.title", ":is-user-allowed" => "stage.isUserAllowed", ":value" => "stage.value", ":is-active" => "stage.active" }
.section.stage-events
- %template{ "v-if" => "isLoadingStage" }
- = icon("spinner spin")
+ %gl-loading-icon{ "v-show" => "isLoadingStage", "size" => "lg" }
%template{ "v-if" => "currentStage && !currentStage.isUserAllowed" }
= render partial: "no_access"
%template{ "v-else" => true }
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 768e4422206..422a467574b 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -12,7 +12,7 @@
%article.file-holder.snippet-file-content
= render 'shared/snippets/blob'
- .row-content-block.top-block.content-component-block
- = render 'award_emoji/awards_block', awardable: @snippet, inline: true
+.row-content-block.top-block.content-component-block
+ = render 'award_emoji/awards_block', awardable: @snippet, inline: true
- #notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => true
+#notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => true
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 080c0ab6ece..30f760f2122 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -13,7 +13,7 @@
%article.file-holder.snippet-file-content
= render 'shared/snippets/blob'
- .row-content-block.top-block.content-component-block
- = render 'award_emoji/awards_block', awardable: @snippet, inline: true
+.row-content-block.top-block.content-component-block
+ = render 'award_emoji/awards_block', awardable: @snippet, inline: true
- #notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => false
+#notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => false
diff --git a/changelogs/unreleased/196184-convert-insights-to-echarts.yml b/changelogs/unreleased/196184-convert-insights-to-echarts.yml
new file mode 100644
index 00000000000..87219b3ff52
--- /dev/null
+++ b/changelogs/unreleased/196184-convert-insights-to-echarts.yml
@@ -0,0 +1,5 @@
+---
+title: Move insights charts to echarts
+merge_request: 24661
+author:
+type: other
diff --git a/changelogs/unreleased/196797-blob-content.yml b/changelogs/unreleased/196797-blob-content.yml
new file mode 100644
index 00000000000..b1443f60f62
--- /dev/null
+++ b/changelogs/unreleased/196797-blob-content.yml
@@ -0,0 +1,5 @@
+---
+title: Refactored snippets view to Vue
+merge_request: 25188
+author:
+type: other
diff --git a/changelogs/unreleased/204740-migrate-fa-spinner-to-spinner-within-app-views-projects-cycle_anal.yml b/changelogs/unreleased/204740-migrate-fa-spinner-to-spinner-within-app-views-projects-cycle_anal.yml
new file mode 100644
index 00000000000..e417b770668
--- /dev/null
+++ b/changelogs/unreleased/204740-migrate-fa-spinner-to-spinner-within-app-views-projects-cycle_anal.yml
@@ -0,0 +1,5 @@
+---
+title: Update loading icon in Value Stream Analytics view
+merge_request: 24861
+author:
+type: other
diff --git a/changelogs/unreleased/defect-safari-line-numbers-height.yml b/changelogs/unreleased/defect-safari-line-numbers-height.yml
new file mode 100644
index 00000000000..e72a7ffc107
--- /dev/null
+++ b/changelogs/unreleased/defect-safari-line-numbers-height.yml
@@ -0,0 +1,5 @@
+---
+title: Fix code line and line number alignment in Safari
+merge_request: 24820
+author:
+type: fixed
diff --git a/doc/api/epics.md b/doc/api/epics.md
index 078bdfdba69..b8eb1ab9f9a 100644
--- a/doc/api/epics.md
+++ b/doc/api/epics.md
@@ -1,4 +1,4 @@
-# Epics API **(ULTIMATE)**
+# Epics API **(PREMIUM)**
Every API call to epic must be authenticated.
diff --git a/doc/ci/review_apps/img/enable_review_app_v12_8.png b/doc/ci/review_apps/img/enable_review_app_v12_8.png
new file mode 100644
index 00000000000..364fe402787
--- /dev/null
+++ b/doc/ci/review_apps/img/enable_review_app_v12_8.png
Binary files differ
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
index 23cb0362867..cc081f2543a 100644
--- a/doc/ci/review_apps/index.md
+++ b/doc/ci/review_apps/index.md
@@ -55,9 +55,31 @@ The process of configuring Review Apps is as follows:
1. Set up the infrastructure to host and deploy the Review Apps (check the [examples](#review-apps-examples) below).
1. [Install](https://docs.gitlab.com/runner/install/) and [configure](https://docs.gitlab.com/runner/commands/) a Runner to do deployment.
-1. Set up a job in `.gitlab-ci.yml` that uses the [predefined CI environment variable](../variables/README.md) `${CI_COMMIT_REF_NAME}` to create dynamic environments and restrict it to run only on branches.
+1. Set up a job in `.gitlab-ci.yml` that uses the [predefined CI environment variable](../variables/README.md) `${CI_COMMIT_REF_NAME}`
+ to create dynamic environments and restrict it to run only on branches.
+ Alternatively, you can get a YML template for this job by [enabling review apps](#enable-review-apps-button) for your project.
1. Optionally, set a job that [manually stops](../environments.md#stopping-an-environment) the Review Apps.
+### Enable Review Apps button
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/118844) in GitLab 12.8.
+
+When configuring Review Apps for a project, you need to add a new job to `.gitlab-ci.yml`,
+as mentioned above. To facilitate this and if you are using Kubernetes, you can click
+the **Enable Review Apps** button and GitLab will prompt you with a template code block that
+you can copy and paste into `.gitlab-ci.yml` as a starting point. To do so:
+
+1. Go to the project your want to create a Review App job for.
+1. From the left nav, go to **Operations** > **Environments**.
+1. Click on the **Enable Review Apps** button. It is available to you
+ if you have Developer or higher [permissions](../../user/permissions.md) to that project.
+1. Copy the provided code snippet and paste it into your
+ `.gitlab-ci.yml` file:
+
+ ![Enable Review Apps modal](img/enable_review_app_v12_8.png)
+
+1. Feel free to tune this template to your own needs.
+
## Review Apps examples
The following are example projects that demonstrate Review App configuration:
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 4f42afe4e79..8cf60342446 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -93,6 +93,7 @@ The following table depicts the various user permission levels in a project.
| Manage/Accept merge requests | | | ✓ | ✓ | ✓ |
| Create new environments | | | ✓ | ✓ | ✓ |
| Stop environments | | | ✓ | ✓ | ✓ |
+| Enable Review Apps | | | ✓ | ✓ | ✓ |
| Add tags | | | ✓ | ✓ | ✓ |
| Cancel and retry jobs | | | ✓ | ✓ | ✓ |
| Create or update commit status | | | ✓ (*5*) | ✓ | ✓ |
diff --git a/doc/user/project/insights/img/insights_example_pie_chart.png b/doc/user/project/insights/img/insights_example_pie_chart.png
deleted file mode 100644
index 3480bce6738..00000000000
--- a/doc/user/project/insights/img/insights_example_pie_chart.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/insights/index.md b/doc/user/project/insights/index.md
index ec3831f2d27..3fb661f9349 100644
--- a/doc/user/project/insights/index.md
+++ b/doc/user/project/insights/index.md
@@ -96,7 +96,7 @@ The following table lists available parameters for charts:
| Keyword | Description |
|:---------------------------------------------------|:------------|
| [`title`](#title) | The title of the chart. This will displayed on the Insights page. |
-| [`type`](#type) | The type of chart: `bar`, `line`, `stacked-bar`, `pie` etc. |
+| [`type`](#type) | The type of chart: `bar`, `line` or `stacked-bar`. |
| [`query`](#query) | A hash that defines the conditions for issues / merge requests to be part of the chart. |
## Parameter details
@@ -132,7 +132,6 @@ Supported values are:
| ----- | ------- |
| `bar` | ![Insights example bar chart](img/insights_example_bar_chart.png) |
| `bar` (time series, i.e. when `group_by` is used) | ![Insights example bar time series chart](img/insights_example_bar_time_series_chart.png) |
-| `pie` | ![Insights example pie chart](img/insights_example_pie_chart.png) |
| `line` | ![Insights example stacked bar chart](img/insights_example_line_chart.png) |
| `stacked-bar` | ![Insights example stacked bar chart](img/insights_example_stacked_bar_chart.png) |
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb
deleted file mode 100644
index c813484347e..00000000000
--- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- context 'Verify' do
- describe 'CI variable support' do
- it 'user adds a CI variable', :smoke do
- Flow::Login.sign_in
-
- project = Resource::Project.fabricate_via_api! do |project|
- project.name = 'project-with-ci-variables'
- project.description = 'project with CI variables'
- end
-
- Resource::CiVariable.fabricate_via_api! do |resource|
- resource.project = project
- resource.key = 'VARIABLE_KEY'
- resource.value = 'some_CI_variable'
- resource.masked = false
- end
-
- project.visit!
-
- Page::Project::Menu.perform(&:go_to_ci_cd_settings)
-
- Page::Project::Settings::CICD.perform do |settings|
- settings.expand_ci_variables do |page|
- expect(page).to have_field(with: 'VARIABLE_KEY')
- expect(page).not_to have_field(with: 'some_CI_variable')
-
- page.reveal_variables
-
- expect(page).to have_field(with: 'some_CI_variable')
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb
new file mode 100644
index 00000000000..9dad5ad8fb5
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Verify' do
+ describe 'Add or Remove CI variable via UI', :smoke do
+ let!(:project) do
+ Resource::Project.fabricate_via_api! do |project|
+ project.name = 'project-with-ci-variables'
+ project.description = 'project with CI variables'
+ end
+ end
+
+ before do
+ Flow::Login.sign_in
+ add_ci_variable
+ open_ci_cd_settings
+ end
+
+ it 'user adds a CI variable' do
+ Page::Project::Settings::CICD.perform do |settings|
+ settings.expand_ci_variables do |page|
+ expect(page).to have_field(with: 'VARIABLE_KEY')
+ expect(page).not_to have_field(with: 'some_CI_variable')
+
+ page.reveal_variables
+
+ expect(page).to have_field(with: 'some_CI_variable')
+ end
+ end
+ end
+
+ it 'user removes a CI variable' do
+ Page::Project::Settings::CICD.perform do |settings|
+ settings.expand_ci_variables do |page|
+ page.remove_variable
+
+ expect(page).not_to have_field(with: 'VARIABLE_KEY')
+ end
+ end
+ end
+
+ private
+
+ def add_ci_variable
+ Resource::CiVariable.fabricate_via_browser_ui! do |ci_variable|
+ ci_variable.project = project
+ ci_variable.key = 'VARIABLE_KEY'
+ ci_variable.value = 'some_CI_variable'
+ ci_variable.masked = false
+ end
+ end
+
+ def open_ci_cd_settings
+ project.visit!
+ Page::Project::Menu.perform(&:go_to_ci_cd_settings)
+ end
+ end
+ end
+end
diff --git a/qa/qa/support/repeater.rb b/qa/qa/support/repeater.rb
index 53d72f2f410..14771243beb 100644
--- a/qa/qa/support/repeater.rb
+++ b/qa/qa/support/repeater.rb
@@ -10,13 +10,13 @@ module QA
RetriesExceededError = Class.new(RuntimeError)
WaitExceededError = Class.new(RuntimeError)
- def repeat_until(max_attempts: nil, max_duration: nil, reload_page: nil, sleep_interval: 0, raise_on_failure: true, retry_on_exception: false)
+ def repeat_until(max_attempts: nil, max_duration: nil, reload_page: nil, sleep_interval: 0, raise_on_failure: true, retry_on_exception: false, log: true)
attempts = 0
start = Time.now
begin
while remaining_attempts?(attempts, max_attempts) && remaining_time?(start, max_duration)
- QA::Runtime::Logger.debug("Attempt number #{attempts + 1}") if max_attempts
+ QA::Runtime::Logger.debug("Attempt number #{attempts + 1}") if max_attempts && log
result = yield
return result if result
diff --git a/qa/qa/support/wait_for_requests.rb b/qa/qa/support/wait_for_requests.rb
index 5d5ba70a0c2..c58882a11ea 100644
--- a/qa/qa/support/wait_for_requests.rb
+++ b/qa/qa/support/wait_for_requests.rb
@@ -6,7 +6,7 @@ module QA
module_function
def wait_for_requests
- Waiter.wait_until do
+ Waiter.wait_until(log: false) do
finished_all_ajax_requests? && finished_all_axios_requests?
end
end
diff --git a/qa/qa/support/waiter.rb b/qa/qa/support/waiter.rb
index 18ccee80074..9ccc0d9484f 100644
--- a/qa/qa/support/waiter.rb
+++ b/qa/qa/support/waiter.rb
@@ -7,15 +7,17 @@ module QA
module_function
- def wait_until(max_duration: singleton_class::DEFAULT_MAX_WAIT_TIME, reload_page: nil, sleep_interval: 0.1, raise_on_failure: true, retry_on_exception: false)
- QA::Runtime::Logger.debug(
- <<~MSG.tr("\n", ' ')
- with wait_until: max_duration: #{max_duration};
- reload_page: #{reload_page};
- sleep_interval: #{sleep_interval};
- raise_on_failure: #{raise_on_failure}
- MSG
- )
+ def wait_until(max_duration: singleton_class::DEFAULT_MAX_WAIT_TIME, reload_page: nil, sleep_interval: 0.1, raise_on_failure: true, retry_on_exception: false, log: true)
+ if log
+ QA::Runtime::Logger.debug(
+ <<~MSG.tr("\n", ' ')
+ with wait_until: max_duration: #{max_duration};
+ reload_page: #{reload_page};
+ sleep_interval: #{sleep_interval};
+ raise_on_failure: #{raise_on_failure}
+ MSG
+ )
+ end
result = nil
self.repeat_until(
@@ -23,11 +25,12 @@ module QA
reload_page: reload_page,
sleep_interval: sleep_interval,
raise_on_failure: raise_on_failure,
- retry_on_exception: retry_on_exception
+ retry_on_exception: retry_on_exception,
+ log: log
) do
result = yield
end
- QA::Runtime::Logger.debug("ended wait_until")
+ QA::Runtime::Logger.debug("ended wait_until") if log
result
end
diff --git a/qa/spec/support/repeater_spec.rb b/qa/spec/support/repeater_spec.rb
index 20dca6608f6..b5d5058ef49 100644
--- a/qa/spec/support/repeater_spec.rb
+++ b/qa/spec/support/repeater_spec.rb
@@ -381,5 +381,35 @@ describe QA::Support::Repeater do
end
end
end
+
+ it 'logs attempts' do
+ attempted = false
+
+ expect do
+ subject.repeat_until(max_attempts: 1) do
+ unless attempted
+ attempted = true
+ break false
+ end
+
+ true
+ end
+ end.to output(/Attempt number/).to_stdout_from_any_process
+ end
+
+ it 'allows logging to be silenced' do
+ attempted = false
+
+ expect do
+ subject.repeat_until(max_attempts: 1, log: false) do
+ unless attempted
+ attempted = true
+ break false
+ end
+
+ true
+ end
+ end.not_to output.to_stdout_from_any_process
+ end
end
end
diff --git a/qa/spec/support/waiter_spec.rb b/qa/spec/support/waiter_spec.rb
index 40abf0d72a2..35f1e01289a 100644
--- a/qa/spec/support/waiter_spec.rb
+++ b/qa/spec/support/waiter_spec.rb
@@ -34,6 +34,11 @@ describe QA::Support::Waiter do
end
end
+ it 'allows logs to be silenced' do
+ expect { subject.wait_until(max_duration: 0, raise_on_failure: false, log: false) { false } }
+ .not_to output.to_stdout_from_any_process
+ end
+
it 'sets max_duration to 60 by default' do
expect(subject).to receive(:repeat_until).with(hash_including(max_duration: 60))
diff --git a/spec/controllers/concerns/metrics_dashboard_spec.rb b/spec/controllers/concerns/metrics_dashboard_spec.rb
index 466021d6ecd..4e42171e3d3 100644
--- a/spec/controllers/concerns/metrics_dashboard_spec.rb
+++ b/spec/controllers/concerns/metrics_dashboard_spec.rb
@@ -45,6 +45,7 @@ describe MetricsDashboard do
it 'returns the specified dashboard' do
expect(json_response['dashboard']['dashboard']).to eq('Environment metrics')
expect(json_response).not_to have_key('all_dashboards')
+ expect(json_response).not_to have_key('metrics_data')
end
context 'when the params are in an alternate format' do
@@ -53,6 +54,25 @@ describe MetricsDashboard do
it 'returns the specified dashboard' do
expect(json_response['dashboard']['dashboard']).to eq('Environment metrics')
expect(json_response).not_to have_key('all_dashboards')
+ expect(json_response).not_to have_key('metrics_data')
+ end
+ end
+
+ context 'when environment for dashboard is available' do
+ let(:params) { { environment: environment } }
+
+ before do
+ allow(controller).to receive(:project).and_return(project)
+ allow(controller).to receive(:environment).and_return(environment)
+ allow(controller)
+ .to receive(:metrics_dashboard_params)
+ .and_return(params)
+ end
+
+ it 'returns the specified dashboard' do
+ expect(json_response['dashboard']['dashboard']).to eq('Environment metrics')
+ expect(json_response).not_to have_key('all_dashboards')
+ expect(json_response).to have_key('metrics_data')
end
end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 7f0f8e36564..6c63b220322 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -489,7 +489,7 @@ describe Projects::EnvironmentsController do
end
shared_examples_for '200 response' do
- let(:expected_keys) { %w(dashboard status) }
+ let(:expected_keys) { %w(dashboard status metrics_data) }
it_behaves_like 'correctly formatted response', :ok
end
diff --git a/spec/frontend/blob/components/blob_content_error_spec.js b/spec/frontend/blob/components/blob_content_error_spec.js
new file mode 100644
index 00000000000..58a9ee761df
--- /dev/null
+++ b/spec/frontend/blob/components/blob_content_error_spec.js
@@ -0,0 +1,27 @@
+import { shallowMount } from '@vue/test-utils';
+import BlobContentError from '~/blob/components/blob_content_error.vue';
+
+describe('Blob Content Error component', () => {
+ let wrapper;
+ const viewerError = '<h1 id="error">Foo Error</h1>';
+
+ function createComponent() {
+ wrapper = shallowMount(BlobContentError, {
+ propsData: {
+ viewerError,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the passed error without transformations', () => {
+ expect(wrapper.html()).toContain(viewerError);
+ });
+});
diff --git a/spec/frontend/blob/components/blob_content_spec.js b/spec/frontend/blob/components/blob_content_spec.js
new file mode 100644
index 00000000000..6a130c9c43d
--- /dev/null
+++ b/spec/frontend/blob/components/blob_content_spec.js
@@ -0,0 +1,70 @@
+import { shallowMount } from '@vue/test-utils';
+import BlobContent from '~/blob/components/blob_content.vue';
+import BlobContentError from '~/blob/components/blob_content_error.vue';
+import {
+ RichViewerMock,
+ SimpleViewerMock,
+ RichBlobContentMock,
+ SimpleBlobContentMock,
+} from './mock_data';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
+
+describe('Blob Content component', () => {
+ let wrapper;
+
+ function createComponent(propsData = {}, activeViewer = SimpleViewerMock) {
+ wrapper = shallowMount(BlobContent, {
+ propsData: {
+ loading: false,
+ activeViewer,
+ ...propsData,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('rendering', () => {
+ it('renders loader if `loading: true`', () => {
+ createComponent({ loading: true });
+ expect(wrapper.contains(GlLoadingIcon)).toBe(true);
+ expect(wrapper.contains(BlobContentError)).toBe(false);
+ expect(wrapper.contains(RichViewer)).toBe(false);
+ expect(wrapper.contains(SimpleViewer)).toBe(false);
+ });
+
+ it('renders error if there is any in the viewer', () => {
+ const renderError = 'Oops';
+ const viewer = Object.assign({}, SimpleViewerMock, { renderError });
+ createComponent({}, viewer);
+ expect(wrapper.contains(GlLoadingIcon)).toBe(false);
+ expect(wrapper.contains(BlobContentError)).toBe(true);
+ expect(wrapper.contains(RichViewer)).toBe(false);
+ expect(wrapper.contains(SimpleViewer)).toBe(false);
+ });
+
+ it.each`
+ type | mock | viewer
+ ${'simple'} | ${SimpleViewerMock} | ${SimpleViewer}
+ ${'rich'} | ${RichViewerMock} | ${RichViewer}
+ `(
+ 'renders $type viewer when activeViewer is $type and no loading or error detected',
+ ({ mock, viewer }) => {
+ createComponent({}, mock);
+ expect(wrapper.contains(viewer)).toBe(true);
+ },
+ );
+
+ it.each`
+ content | mock | viewer
+ ${SimpleBlobContentMock.plainData} | ${SimpleViewerMock} | ${SimpleViewer}
+ ${RichBlobContentMock.richData} | ${RichViewerMock} | ${RichViewer}
+ `('renders correct content that is passed to the component', ({ content, mock, viewer }) => {
+ createComponent({ content }, mock);
+ expect(wrapper.find(viewer).html()).toContain(content);
+ });
+ });
+});
diff --git a/spec/frontend/blob/components/blob_header_default_actions_spec.js b/spec/frontend/blob/components/blob_header_default_actions_spec.js
index 5da0d40ab14..39d627e71c5 100644
--- a/spec/frontend/blob/components/blob_header_default_actions_spec.js
+++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js
@@ -67,13 +67,4 @@ describe('Blob Header Default Actions', () => {
expect(buttons.at(0).attributes('disabled')).toBeTruthy();
});
});
-
- describe('functionally', () => {
- it('emits an event when a Copy Contents button is clicked', () => {
- jest.spyOn(wrapper.vm, '$emit');
- buttons.at(0).vm.$emit('click');
-
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('copy');
- });
- });
});
diff --git a/spec/frontend/blob/components/mock_data.js b/spec/frontend/blob/components/mock_data.js
index 4f7b297aba0..bfcca14324f 100644
--- a/spec/frontend/blob/components/mock_data.js
+++ b/spec/frontend/blob/components/mock_data.js
@@ -1,29 +1,43 @@
+import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
+
+export const SimpleViewerMock = {
+ collapsed: false,
+ loadingPartialName: 'loading',
+ renderError: null,
+ tooLarge: false,
+ type: SIMPLE_BLOB_VIEWER,
+ fileType: 'text',
+};
+
+export const RichViewerMock = {
+ collapsed: false,
+ loadingPartialName: 'loading',
+ renderError: null,
+ tooLarge: false,
+ type: RICH_BLOB_VIEWER,
+ fileType: 'markdown',
+};
+
export const Blob = {
binary: false,
- highlightedData:
- '<h1 data-sourcepos="1:1-1:19" dir="auto">\n<a id="user-content-this-one-is-dummy" class="anchor" href="#this-one-is-dummy" aria-hidden="true"></a>This one is dummy</h1>\n<h2 data-sourcepos="3:1-3:21" dir="auto">\n<a id="user-content-and-has-sub-header" class="anchor" href="#and-has-sub-header" aria-hidden="true"></a>And has sub-header</h2>\n<p data-sourcepos="5:1-5:27" dir="auto">Even some stupid text here</p>',
name: 'dummy.md',
path: 'dummy.md',
rawPath: '/flightjs/flight/snippets/51/raw',
size: 75,
simpleViewer: {
- collapsed: false,
- fileType: 'text',
- loadAsync: true,
- loadingPartialName: 'loading',
- renderError: null,
- tooLarge: false,
- type: 'simple',
+ ...SimpleViewerMock,
},
richViewer: {
- collapsed: false,
- fileType: 'markup',
- loadAsync: true,
- loadingPartialName: 'loading',
- renderError: null,
- tooLarge: false,
- type: 'rich',
+ ...RichViewerMock,
},
};
+export const RichBlobContentMock = {
+ richData: '<h1>Rich</h1>',
+};
+
+export const SimpleBlobContentMock = {
+ plainData: 'Plain',
+};
+
export default {};
diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js
index efc1c6dcef9..c4f1dd0ca35 100644
--- a/spec/frontend/snippets/components/snippet_blob_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js
@@ -1,14 +1,18 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
+import BlobContent from '~/blob/components/blob_content.vue';
+import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
import {
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
} from '~/snippets/constants';
+import { Blob as BlobMock, SimpleViewerMock, RichViewerMock } from 'jest/blob/components/mock_data';
+
describe('Blob Embeddable', () => {
let wrapper;
const snippet = {
@@ -16,27 +20,42 @@ describe('Blob Embeddable', () => {
webUrl: 'https://foo.bar',
visibilityLevel: SNIPPET_VISIBILITY_PUBLIC,
};
+ const dataMock = {
+ blob: BlobMock,
+ activeViewerType: SimpleViewerMock.type,
+ };
- function createComponent(props = {}, loading = false) {
+ function createComponent(
+ props = {},
+ data = dataMock,
+ blobLoading = false,
+ contentLoading = false,
+ ) {
const $apollo = {
queries: {
blob: {
- loading,
+ loading: blobLoading,
+ },
+ blobContent: {
+ loading: contentLoading,
},
},
};
- wrapper = shallowMount(SnippetBlobView, {
+ wrapper = mount(SnippetBlobView, {
propsData: {
snippet: {
...snippet,
...props,
},
},
+ data() {
+ return {
+ ...data,
+ };
+ },
mocks: { $apollo },
});
-
- wrapper.vm.$apollo.queries.blob.loading = false;
}
afterEach(() => {
@@ -48,6 +67,7 @@ describe('Blob Embeddable', () => {
createComponent();
expect(wrapper.find(BlobEmbeddable).exists()).toBe(true);
expect(wrapper.find(BlobHeader).exists()).toBe(true);
+ expect(wrapper.find(BlobContent).exists()).toBe(true);
});
it.each([SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PRIVATE, 'foo'])(
@@ -68,9 +88,92 @@ describe('Blob Embeddable', () => {
});
it('shows loading icon while blob data is in flight', () => {
- createComponent({}, true);
+ createComponent({}, dataMock, true);
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.find('.snippet-file-content').exists()).toBe(false);
});
+
+ it('sets simple viewer correctly', () => {
+ createComponent();
+ expect(wrapper.find(SimpleViewer).exists()).toBe(true);
+ });
+
+ it('sets rich viewer correctly', () => {
+ const data = Object.assign({}, dataMock, {
+ activeViewerType: RichViewerMock.type,
+ });
+ createComponent({}, data);
+ expect(wrapper.find(RichViewer).exists()).toBe(true);
+ });
+
+ it('correctly switches viewer type', () => {
+ createComponent();
+ expect(wrapper.find(SimpleViewer).exists()).toBe(true);
+
+ wrapper.vm.switchViewer(RichViewerMock.type);
+
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(wrapper.find(RichViewer).exists()).toBe(true);
+ wrapper.vm.switchViewer(SimpleViewerMock.type);
+ })
+ .then(() => {
+ expect(wrapper.find(SimpleViewer).exists()).toBe(true);
+ });
+ });
+
+ describe('URLS with hash', () => {
+ beforeEach(() => {
+ window.location.hash = '#LC2';
+ });
+
+ afterEach(() => {
+ window.location.hash = '';
+ });
+
+ it('renders simple viewer by default if URL contains hash', () => {
+ createComponent();
+
+ expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
+ expect(wrapper.find(SimpleViewer).exists()).toBe(true);
+ });
+
+ describe('switchViewer()', () => {
+ it('by default switches to the passed viewer', () => {
+ createComponent();
+
+ wrapper.vm.switchViewer(RichViewerMock.type);
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(wrapper.vm.activeViewerType).toBe(RichViewerMock.type);
+ expect(wrapper.find(RichViewer).exists()).toBe(true);
+
+ wrapper.vm.switchViewer(SimpleViewerMock.type);
+ })
+ .then(() => {
+ expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
+ expect(wrapper.find(SimpleViewer).exists()).toBe(true);
+ });
+ });
+
+ it('respects hash over richViewer in the blob when corresponding parameter is passed', () => {
+ createComponent(
+ {},
+ {
+ blob: BlobMock,
+ },
+ );
+ expect(wrapper.vm.blob.richViewer).toEqual(expect.any(Object));
+
+ wrapper.vm.switchViewer(RichViewerMock.type, true);
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
+ expect(wrapper.find(SimpleViewer).exists()).toBe(true);
+ });
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap b/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap
new file mode 100644
index 00000000000..87f2a8f9eff
--- /dev/null
+++ b/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap
@@ -0,0 +1,86 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Blob Simple Viewer component rendering matches the snapshot 1`] = `
+<div
+ class="file-content code js-syntax-highlight qa-file-content"
+>
+ <div
+ class="line-numbers"
+ >
+ <a
+ class="diff-line-num js-line-number"
+ data-line-number="1"
+ href="#LC1"
+ id="L1"
+ >
+ <gl-icon-stub
+ name="link"
+ size="12"
+ />
+
+ 1
+
+ </a>
+ <a
+ class="diff-line-num js-line-number"
+ data-line-number="2"
+ href="#LC2"
+ id="L2"
+ >
+ <gl-icon-stub
+ name="link"
+ size="12"
+ />
+
+ 2
+
+ </a>
+ <a
+ class="diff-line-num js-line-number"
+ data-line-number="3"
+ href="#LC3"
+ id="L3"
+ >
+ <gl-icon-stub
+ name="link"
+ size="12"
+ />
+
+ 3
+
+ </a>
+ </div>
+
+ <div
+ class="blob-content"
+ >
+ <pre
+ class="code highlight"
+ >
+ <code
+ id="blob-code-content"
+ >
+ <span
+ id="LC1"
+ >
+ First
+ </span>
+
+
+ <span
+ id="LC2"
+ >
+ Second
+ </span>
+
+
+ <span
+ id="LC3"
+ >
+ Third
+ </span>
+ </code>
+ </pre>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
new file mode 100644
index 00000000000..17ea78b5826
--- /dev/null
+++ b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
@@ -0,0 +1,27 @@
+import { shallowMount } from '@vue/test-utils';
+import RichViewer from '~/vue_shared/components/blob_viewers/rich_viewer.vue';
+
+describe('Blob Rich Viewer component', () => {
+ let wrapper;
+ const content = '<h1 id="markdown">Foo Bar</h1>';
+
+ function createComponent() {
+ wrapper = shallowMount(RichViewer, {
+ propsData: {
+ content,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the passed content without transformations', () => {
+ expect(wrapper.html()).toContain(content);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
new file mode 100644
index 00000000000..d12bfc5c686
--- /dev/null
+++ b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
@@ -0,0 +1,81 @@
+import { shallowMount } from '@vue/test-utils';
+import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue';
+import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants';
+
+describe('Blob Simple Viewer component', () => {
+ let wrapper;
+ const contentMock = `<span id="LC1">First</span>\n<span id="LC2">Second</span>\n<span id="LC3">Third</span>`;
+
+ function createComponent(content = contentMock) {
+ wrapper = shallowMount(SimpleViewer, {
+ propsData: {
+ content,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('does not fail if content is empty', () => {
+ const spy = jest.spyOn(window.console, 'error');
+ createComponent('');
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ describe('rendering', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('matches the snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders exactly three lines', () => {
+ expect(wrapper.findAll('.js-line-number')).toHaveLength(3);
+ });
+
+ it('renders the content without transformations', () => {
+ expect(wrapper.html()).toContain(contentMock);
+ });
+ });
+
+ describe('functionality', () => {
+ const scrollIntoViewMock = jest.fn();
+ HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
+
+ beforeEach(() => {
+ window.location.hash = '#LC2';
+ createComponent();
+ });
+
+ afterEach(() => {
+ window.location.hash = '';
+ });
+
+ it('scrolls to requested line when rendered', () => {
+ const linetoBeHighlighted = wrapper.find('#LC2');
+ expect(scrollIntoViewMock).toHaveBeenCalled();
+ expect(wrapper.vm.highlightedLine).toBe(linetoBeHighlighted.element);
+ expect(linetoBeHighlighted.classes()).toContain(HIGHLIGHT_CLASS_NAME);
+ });
+
+ it('switches highlighting when another line is selected', () => {
+ const currentlyHighlighted = wrapper.find('#LC2');
+ const hash = '#LC3';
+ const linetoBeHighlighted = wrapper.find(hash);
+
+ expect(wrapper.vm.highlightedLine).toBe(currentlyHighlighted.element);
+
+ wrapper.vm.scrollToLine(hash);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.highlightedLine).toBe(linetoBeHighlighted.element);
+ expect(currentlyHighlighted.classes()).not.toContain(HIGHLIGHT_CLASS_NAME);
+ expect(linetoBeHighlighted.classes()).toContain(HIGHLIGHT_CLASS_NAME);
+ });
+ });
+ });
+});
diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb
index ca0360b363e..b72fbc9fd3c 100644
--- a/spec/helpers/environments_helper_spec.rb
+++ b/spec/helpers/environments_helper_spec.rb
@@ -20,7 +20,7 @@ describe EnvironmentsHelper do
expect(metrics_data).to include(
'settings-path' => edit_project_service_path(project, 'prometheus'),
'clusters-path' => project_clusters_path(project),
- 'current-environment-name': environment.name,
+ 'current-environment-name' => environment.name,
'documentation-path' => help_page_path('administration/monitoring/prometheus/index.md'),
'empty-getting-started-svg-path' => match_asset_path('/assets/illustrations/monitoring/getting_started.svg'),
'empty-loading-svg-path' => match_asset_path('/assets/illustrations/monitoring/loading.svg'),