diff options
55 files changed, 1133 insertions, 549 deletions
diff --git a/app/assets/javascripts/blob/components/blob_edit_content.vue b/app/assets/javascripts/blob/components/blob_edit_content.vue new file mode 100644 index 00000000000..83303a373f3 --- /dev/null +++ b/app/assets/javascripts/blob/components/blob_edit_content.vue @@ -0,0 +1,49 @@ +<script> +import { initEditorLite } from '~/blob/utils'; + +export default { + props: { + value: { + type: String, + required: true, + }, + fileName: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + content: this.value, + editor: null, + }; + }, + watch: { + fileName(newVal) { + this.editor.updateModelLanguage(newVal); + }, + }, + mounted() { + this.editor = initEditorLite({ + el: this.$refs.editor, + blobPath: this.fileName, + blobContent: this.content, + }); + }, + methods: { + triggerFileChange() { + const val = this.editor.getValue(); + this.content = val; + this.$emit('input', val); + }, + }, +}; +</script> +<template> + <div class="file-content code"> + <pre id="editor" ref="editor" data-editor-loading @focusout="triggerFileChange">{{ + content + }}</pre> + </div> +</template> diff --git a/app/assets/javascripts/blob/utils.js b/app/assets/javascripts/blob/utils.js new file mode 100644 index 00000000000..dc2ec642e59 --- /dev/null +++ b/app/assets/javascripts/blob/utils.js @@ -0,0 +1,24 @@ +/* global ace */ +import Editor from '~/editor/editor_lite'; + +export function initEditorLite({ el, blobPath, blobContent }) { + if (!el) { + throw new Error(`"el" parameter is required to initialize Editor`); + } + let editor; + + if (window?.gon?.features?.monacoSnippets) { + editor = new Editor(); + editor.createInstance({ + el, + blobPath, + blobContent, + }); + } else { + editor = ace.edit(el); + } + + return editor; +} + +export default () => ({}); diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index 274565adab0..00da039c235 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -1,7 +1,7 @@ <script> import _ from 'underscore'; import { mapState } from 'vuex'; -import { GlTooltipDirective } from '@gitlab/ui'; +import { GlLabel, GlTooltipDirective } from '@gitlab/ui'; import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner'; import { sprintf, __ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; @@ -10,18 +10,17 @@ import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_ import IssueDueDate from './issue_due_date.vue'; import IssueTimeEstimate from './issue_time_estimate.vue'; import boardsStore from '../stores/boards_store'; -import IssueCardInnerScopedLabel from './issue_card_inner_scoped_label.vue'; import { isScopedLabel } from '~/lib/utils/common_utils'; export default { components: { + GlLabel, Icon, UserAvatarLink, TooltipOnTruncate, IssueDueDate, IssueTimeEstimate, IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'), - IssueCardInnerScopedLabel, }, directives: { GlTooltip: GlTooltipDirective, @@ -145,12 +144,6 @@ export default { boardsStore.toggleFilter(filter); }, - labelStyle(label) { - return { - backgroundColor: label.color, - color: label.textColor, - }; - }, showScopedLabel(label) { return boardsStore.scopedLabels.enabled && isScopedLabel(label); }, @@ -184,27 +177,16 @@ export default { </div> <div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap"> <template v-for="label in orderedLabels"> - <issue-card-inner-scoped-label - v-if="showScopedLabel(label)" + <gl-label :key="label.id" - :label="label" - :label-style="labelStyle(label)" + :background-color="label.color" + :title="label.title" + :description="label.description" + size="sm" + :scoped="showScopedLabel(label)" :scoped-labels-documentation-link="helpLink" - @scoped-label-click="filterByLabel($event)" - /> - - <button - v-else - :key="label.id" - v-gl-tooltip - :style="labelStyle(label)" - :title="label.description" - class="badge color-label append-right-4 prepend-top-4" - type="button" @click="filterByLabel(label)" - > - {{ label.title }} - </button> + /> </template> </div> <div class="board-card-footer d-flex justify-content-between align-items-end"> diff --git a/app/assets/javascripts/boards/components/issue_card_inner_scoped_label.vue b/app/assets/javascripts/boards/components/issue_card_inner_scoped_label.vue deleted file mode 100644 index fa4c68964cb..00000000000 --- a/app/assets/javascripts/boards/components/issue_card_inner_scoped_label.vue +++ /dev/null @@ -1,45 +0,0 @@ -<script> -import { GlLink, GlTooltip } from '@gitlab/ui'; - -export default { - components: { - GlTooltip, - GlLink, - }, - props: { - label: { - type: Object, - required: true, - }, - labelStyle: { - type: Object, - required: true, - }, - scopedLabelsDocumentationLink: { - type: String, - required: true, - }, - }, -}; -</script> - -<template> - <span - class="d-inline-block position-relative scoped-label-wrapper append-right-4 prepend-top-4 board-label" - > - <a @click="$emit('scoped-label-click', label)"> - <span :ref="'labelTitleRef'" :style="labelStyle" class="badge label color-label"> - {{ label.title }} - </span> - <gl-tooltip :target="() => $refs.labelTitleRef" placement="top" boundary="viewport"> - <span class="font-weight-bold scoped-label-tooltip-title">{{ __('Scoped label') }}</span - ><br /> - {{ label.description }} - </gl-tooltip> - </a> - - <gl-link :href="scopedLabelsDocumentationLink" target="_blank" class="label scoped-label" - ><i class="fa fa-question-circle" :style="labelStyle"></i - ></gl-link> - </span> -</template> diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue index 9322423370b..a1b581dc627 100644 --- a/app/assets/javascripts/clusters_list/components/clusters.vue +++ b/app/assets/javascripts/clusters_list/components/clusters.vue @@ -1,8 +1,9 @@ <script> import { mapState, mapActions } from 'vuex'; import { GlTable, GlLoadingIcon, GlBadge } from '@gitlab/ui'; -import { CLUSTER_TYPES } from '../constants'; -import { __ } from '~/locale'; +import tooltip from '~/vue_shared/directives/tooltip'; +import { CLUSTER_TYPES, STATUSES } from '../constants'; +import { __, sprintf } from '~/locale'; export default { components: { @@ -10,6 +11,9 @@ export default { GlLoadingIcon, GlBadge, }, + directives: { + tooltip, + }, fields: [ { key: 'name', @@ -38,6 +42,13 @@ export default { }, methods: { ...mapActions(['fetchClusters']), + statusClass(status) { + return STATUSES[status].className; + }, + statusTitle(status) { + const { title } = STATUSES[status]; + return sprintf(__('Status: %{title}'), { title }, false); + }, }, }; </script> @@ -52,6 +63,25 @@ export default { variant="light" class="qa-clusters-table" > + <template #cell(name)="{ item }"> + <div class="d-flex flex-row-reverse flex-md-row js-status"> + {{ item.name }} + <gl-loading-icon + v-if="item.status === 'deleting'" + v-tooltip + :title="statusTitle(item.status)" + size="sm" + class="mr-2 ml-md-2" + /> + <div + v-else + v-tooltip + class="cluster-status-indicator rounded-circle align-self-center gl-w-8 gl-h-8 mr-2 ml-md-2" + :class="statusClass(item.status)" + :title="statusTitle(item.status)" + ></div> + </div> + </template> <template #cell(clusterType)="{value}"> <gl-badge variant="light"> {{ value }} diff --git a/app/assets/javascripts/clusters_list/constants.js b/app/assets/javascripts/clusters_list/constants.js index 4125288b5a5..9428f08176c 100644 --- a/app/assets/javascripts/clusters_list/constants.js +++ b/app/assets/javascripts/clusters_list/constants.js @@ -6,6 +6,10 @@ export const CLUSTER_TYPES = { instance_type: __('Instance'), }; -export default { - CLUSTER_TYPES, +export const STATUSES = { + disabled: { className: 'disabled', title: __('Disabled') }, + connected: { className: 'bg-success', title: __('Connected') }, + unreachable: { className: 'bg-danger', title: __('Unreachable') }, + authentication_failure: { className: 'bg-warning', title: __('Authentication Failure') }, + deleting: { title: __('Deleting') }, }; diff --git a/app/assets/javascripts/clusters_list/store/state.js b/app/assets/javascripts/clusters_list/store/state.js index e6cdf9d67db..ed032ed8435 100644 --- a/app/assets/javascripts/clusters_list/store/state.js +++ b/app/assets/javascripts/clusters_list/store/state.js @@ -1,19 +1,5 @@ export default (initialState = {}) => ({ endpoint: initialState.endpoint, loading: false, // TODO - set this to true once integrated with BE - clusters: [ - // TODO - remove mock data once integrated with BE - // { - // name: 'My Cluster', - // environmentScope: '*', - // size: '3', - // clusterType: 'group_type', - // }, - // { - // name: 'My other cluster', - // environmentScope: 'production', - // size: '12', - // clusterType: 'project_type', - // }, - ], + clusters: [], }); diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index 42da65a941d..a3ed8d9c632 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -1,14 +1,15 @@ -/* global ace */ -import Editor from '~/editor/editor_lite'; +import { initEditorLite } from '~/blob/utils'; import setupCollapsibleInputs from './collapsible_input'; let editor; const initAce = () => { - editor = ace.edit('editor'); - + const editorEl = document.getElementById('editor'); const form = document.querySelector('.snippet-form-holder form'); const content = document.querySelector('.snippet-file-content'); + + editor = initEditorLite({ el: editorEl }); + form.addEventListener('submit', () => { content.value = editor.getValue(); }); @@ -20,8 +21,7 @@ const initMonaco = () => { const fileNameEl = document.querySelector('.js-snippet-file-name'); const form = document.querySelector('.snippet-form-holder form'); - editor = new Editor(); - editor.createInstance({ + editor = initEditorLite({ el: editorEl, blobPath: fileNameEl.value, blobContent: contentEl.value, diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue new file mode 100644 index 00000000000..af1574f98d9 --- /dev/null +++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue @@ -0,0 +1,36 @@ +<script> +import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue'; +import BlobContentEdit from '~/blob/components/blob_edit_content.vue'; + +export default { + components: { + BlobHeaderEdit, + BlobContentEdit, + }, + props: { + content: { + type: String, + required: true, + }, + fileName: { + type: String, + required: true, + }, + }, + data() { + return { + name: this.fileName, + blobContent: this.content, + }; + }, +}; +</script> +<template> + <div class="form-group file-editor"> + <label>{{ s__('Snippets|File') }}</label> + <div class="file-holder snippet"> + <blob-header-edit v-model="name" /> + <blob-content-edit v-model="blobContent" :file-name="name" /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/tracking.js b/app/assets/javascripts/tracking.js index a17b8a047c0..ab5acd83b01 100644 --- a/app/assets/javascripts/tracking.js +++ b/app/assets/javascripts/tracking.js @@ -111,4 +111,6 @@ export function initUserTracking() { if (opts.linkClickTracking) window.snowplow('enableLinkClickTracking'); Tracking.bindDocument(); + + document.dispatchEvent(new Event('SnowplowInitialized')); } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue index 4abf7c478ee..fe43f77b1ee 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue @@ -1,12 +1,10 @@ <script> -import DropdownValueScopedLabel from './dropdown_value_scoped_label.vue'; -import DropdownValueRegularLabel from './dropdown_value_regular_label.vue'; +import { GlLabel } from '@gitlab/ui'; import { isScopedLabel } from '~/lib/utils/common_utils'; export default { components: { - DropdownValueScopedLabel, - DropdownValueRegularLabel, + GlLabel, }, props: { labels: { @@ -37,12 +35,6 @@ export default { labelFilterUrl(label) { return `${this.labelFilterBasePath}?label_name[]=${encodeURIComponent(label.title)}`; }, - labelStyle(label) { - return { - color: label.textColor, - backgroundColor: label.color, - }; - }, scopedLabelsDescription({ description = '' }) { return `<span class="font-weight-bold scoped-label-tooltip-title">Scoped label</span><br />${description}`; }, @@ -65,22 +57,15 @@ export default { </span> <template v-for="label in labels" v-else> - <dropdown-value-scoped-label - v-if="showScopedLabels(label)" + <gl-label :key="label.id" - :label="label" - :label-filter-url="labelFilterUrl(label)" - :label-style="labelStyle(label)" + :target="labelFilterUrl(label)" + :background-color="label.color" + :title="label.title" + :description="label.description" + :scoped="showScopedLabels(label)" :scoped-labels-documentation-link="scopedLabelsDocumentationLink" /> - - <dropdown-value-regular-label - v-else - :key="label.id" - :label="label" - :label-filter-url="labelFilterUrl(label)" - :label-style="labelStyle(label)" - /> </template> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue deleted file mode 100644 index 839117becd9..00000000000 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_regular_label.vue +++ /dev/null @@ -1,39 +0,0 @@ -<script> -import { GlTooltip } from '@gitlab/ui'; - -export default { - components: { - GlTooltip, - }, - props: { - label: { - type: Object, - required: true, - }, - labelStyle: { - type: Object, - required: true, - }, - labelFilterUrl: { - type: String, - required: true, - }, - }, -}; -</script> - -<template> - <a ref="regularLabelRef" :href="labelFilterUrl"> - <span :style="labelStyle" class="badge color-label"> - {{ label.title }} - </span> - <gl-tooltip - v-if="label.description" - :target="() => $refs.regularLabelRef" - placement="top" - boundary="viewport" - > - {{ label.description }} - </gl-tooltip> - </a> -</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue deleted file mode 100644 index 94587e1cbab..00000000000 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue +++ /dev/null @@ -1,52 +0,0 @@ -<script> -import { GlLink, GlTooltip } from '@gitlab/ui'; - -export default { - components: { - GlTooltip, - GlLink, - }, - props: { - label: { - type: Object, - required: true, - }, - labelStyle: { - type: Object, - required: true, - }, - scopedLabelsDocumentationLink: { - type: String, - required: true, - }, - labelFilterUrl: { - type: String, - required: true, - }, - }, -}; -</script> - -<template> - <span class="d-inline-block position-relative scoped-label-wrapper"> - <a :href="labelFilterUrl"> - <span :ref="`labelTitleRef`" :style="labelStyle" class="badge color-label label"> - {{ label.title }} - </span> - <gl-tooltip - v-if="label.description" - :target="() => $refs.labelTitleRef" - placement="top" - boundary="viewport" - > - <span class="font-weight-bold scoped-label-tooltip-title">{{ __('Scoped label') }}</span - ><br /> - {{ label.description }} - </gl-tooltip> - </a> - - <gl-link :href="scopedLabelsDocumentationLink" target="_blank" class="label scoped-label" - ><i class="fa fa-question-circle" :style="labelStyle"></i - ></gl-link> - </span> -</template> diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 42d7b0d08f7..a9079f036ab 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -266,20 +266,9 @@ background-color: $blue-50; } - .badge { - border: 0; - outline: 0; - - &:hover { - text-decoration: underline; - } - - @include media-breakpoint-down(lg) { - font-size: $gl-font-size-xs; - padding-left: $gl-padding-4; - padding-right: $gl-padding-4; - font-weight: $gl-font-weight-bold; - } + .gl-label { + margin-top: 4px; + margin-right: 4px; } .confidential-icon { diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index 88d6b0d3746..b9d415ae237 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -163,3 +163,9 @@ color: $black; font-weight: $gl-font-weight-bold; } + +.cluster-status-indicator { + &.disabled { + background-color: $gray-600; + } +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index fd56f655c0a..85a8113ff1b 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -158,6 +158,10 @@ a:not(.btn) { color: inherit; + .gl-label-text:hover { + color: inherit; + } + &:hover { color: $blue-800; diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index 882ce29c671..1eff21401a2 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -54,8 +54,10 @@ .mh-50vh { max-height: 50vh; } .font-size-inherit { font-size: inherit; } +.gl-w-8 { width: px-to-rem($grid-size); } .gl-w-16 { width: px-to-rem($grid-size * 2); } .gl-w-64 { width: px-to-rem($grid-size * 8); } +.gl-h-8 { height: px-to-rem($grid-size); } .gl-h-32 { height: px-to-rem($grid-size * 4); } .gl-h-64 { height: px-to-rem($grid-size * 8); } diff --git a/app/finders/ci/pipelines_for_merge_request_finder.rb b/app/finders/ci/pipelines_for_merge_request_finder.rb new file mode 100644 index 00000000000..c01a68d6749 --- /dev/null +++ b/app/finders/ci/pipelines_for_merge_request_finder.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +module Ci + # A state object to centralize logic related to merge request pipelines + class PipelinesForMergeRequestFinder + include Gitlab::Utils::StrongMemoize + + EVENT = 'merge_request_event' + + def initialize(merge_request) + @merge_request = merge_request + end + + attr_reader :merge_request + + delegate :commit_shas, :source_project, :source_branch, to: :merge_request + + def all + strong_memoize(:all_pipelines) do + next Ci::Pipeline.none unless source_project + + pipelines = + if merge_request.persisted? + pipelines_using_cte + else + triggered_for_branch.for_sha(commit_shas) + end + + sort(pipelines) + end + end + + private + + def pipelines_using_cte + cte = Gitlab::SQL::CTE.new(:shas, merge_request.all_commits.select(:sha)) + + source_pipelines_join = cte.table[:sha].eq(Ci::Pipeline.arel_table[:source_sha]) + source_pipelines = filter_by(triggered_by_merge_request, cte, source_pipelines_join) + detached_pipelines = filter_by_sha(triggered_by_merge_request, cte) + pipelines_for_branch = filter_by_sha(triggered_for_branch, cte) + + Ci::Pipeline.with(cte.to_arel) # rubocop: disable CodeReuse/ActiveRecord + .from_union([source_pipelines, detached_pipelines, pipelines_for_branch]) + end + + def filter_by_sha(pipelines, cte) + hex = Arel::Nodes::SqlLiteral.new("'hex'") + string_sha = Arel::Nodes::NamedFunction.new('encode', [cte.table[:sha], hex]) + join_condition = string_sha.eq(Ci::Pipeline.arel_table[:sha]) + + filter_by(pipelines, cte, join_condition) + end + + def filter_by(pipelines, cte, join_condition) + shas_table = + Ci::Pipeline.arel_table + .join(cte.table, Arel::Nodes::InnerJoin) + .on(join_condition) + .join_sources + + pipelines.joins(shas_table) # rubocop: disable CodeReuse/ActiveRecord + end + + # NOTE: this method returns only parent merge request pipelines. + # Child merge request pipelines have a different source. + def triggered_by_merge_request + source_project.ci_pipelines + .where(source: :merge_request_event, merge_request: merge_request) # rubocop: disable CodeReuse/ActiveRecord + end + + def triggered_for_branch + source_project.ci_pipelines + .where(source: branch_pipeline_sources, ref: source_branch, tag: false) # rubocop: disable CodeReuse/ActiveRecord + end + + def branch_pipeline_sources + strong_memoize(:branch_pipeline_sources) do + Ci::Pipeline.sources.reject { |source| source == EVENT }.values + end + end + + def sort(pipelines) + sql = 'CASE ci_pipelines.source WHEN (?) THEN 0 ELSE 1 END, ci_pipelines.id DESC' + query = ApplicationRecord.send(:sanitize_sql_array, [sql, Ci::Pipeline.sources[:merge_request_event]]) # rubocop:disable GitlabSecurity/PublicSend + + pipelines.order(Arel.sql(query)) # rubocop: disable CodeReuse/ActiveRecord + end + end +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 0bc4e550678..26e3c8f38f6 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1251,7 +1251,7 @@ class MergeRequest < ApplicationRecord def all_pipelines strong_memoize(:all_pipelines) do - MergeRequest::Pipelines.new(self).all + Ci::PipelinesForMergeRequestFinder.new(self).all end end diff --git a/app/models/merge_request/pipelines.rb b/app/models/merge_request/pipelines.rb deleted file mode 100644 index 72756e8e9d0..00000000000 --- a/app/models/merge_request/pipelines.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -# A state object to centralize logic related to merge request pipelines -class MergeRequest::Pipelines - include Gitlab::Utils::StrongMemoize - - EVENT = 'merge_request_event' - - def initialize(merge_request) - @merge_request = merge_request - end - - attr_reader :merge_request - - delegate :commit_shas, :source_project, :source_branch, to: :merge_request - - def all - strong_memoize(:all_pipelines) do - next Ci::Pipeline.none unless source_project - - pipelines = - if merge_request.persisted? - pipelines_using_cte - else - triggered_for_branch.for_sha(commit_shas) - end - - sort(pipelines) - end - end - - private - - def pipelines_using_cte - cte = Gitlab::SQL::CTE.new(:shas, merge_request.all_commits.select(:sha)) - - source_pipelines_join = cte.table[:sha].eq(Ci::Pipeline.arel_table[:source_sha]) - source_pipelines = filter_by(triggered_by_merge_request, cte, source_pipelines_join) - detached_pipelines = filter_by_sha(triggered_by_merge_request, cte) - pipelines_for_branch = filter_by_sha(triggered_for_branch, cte) - - Ci::Pipeline.with(cte.to_arel) - .from_union([source_pipelines, detached_pipelines, pipelines_for_branch]) - end - - def filter_by_sha(pipelines, cte) - hex = Arel::Nodes::SqlLiteral.new("'hex'") - string_sha = Arel::Nodes::NamedFunction.new('encode', [cte.table[:sha], hex]) - join_condition = string_sha.eq(Ci::Pipeline.arel_table[:sha]) - - filter_by(pipelines, cte, join_condition) - end - - def filter_by(pipelines, cte, join_condition) - shas_table = - Ci::Pipeline.arel_table - .join(cte.table, Arel::Nodes::InnerJoin) - .on(join_condition) - .join_sources - - pipelines.joins(shas_table) - end - - # NOTE: this method returns only parent merge request pipelines. - # Child merge request pipelines have a different source. - def triggered_by_merge_request - source_project.ci_pipelines - .where(source: :merge_request_event, merge_request: merge_request) - end - - def triggered_for_branch - source_project.ci_pipelines - .where(source: branch_pipeline_sources, ref: source_branch, tag: false) - end - - def branch_pipeline_sources - strong_memoize(:branch_pipeline_sources) do - Ci::Pipeline.sources.reject { |source| source == EVENT }.values - end - end - - def sort(pipelines) - sql = 'CASE ci_pipelines.source WHEN (?) THEN 0 ELSE 1 END, ci_pipelines.id DESC' - query = ApplicationRecord.send(:sanitize_sql_array, [sql, Ci::Pipeline.sources[:merge_request_event]]) # rubocop:disable GitlabSecurity/PublicSend - - pipelines.order(Arel.sql(query)) - end -end diff --git a/changelogs/unreleased/207367-change-link-icons-on-security-configuration-page-to-follow-design-.yml b/changelogs/unreleased/207367-change-link-icons-on-security-configuration-page-to-follow-design-.yml new file mode 100644 index 00000000000..3cb599d175e --- /dev/null +++ b/changelogs/unreleased/207367-change-link-icons-on-security-configuration-page-to-follow-design-.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Change link-icons on security configuration page to follow design system +merge_request: 26340 +author: +type: other diff --git a/changelogs/unreleased/35475-add-prometheus-ci-vars.yml b/changelogs/unreleased/35475-add-prometheus-ci-vars.yml new file mode 100644 index 00000000000..2f6d2f2b331 --- /dev/null +++ b/changelogs/unreleased/35475-add-prometheus-ci-vars.yml @@ -0,0 +1,5 @@ +--- +title: Support more query variables in custom dashboards per project +merge_request: 25732 +author: +type: added diff --git a/changelogs/unreleased/38143-replace-labels-in-vue-with-gitlab-ui-component.yml b/changelogs/unreleased/38143-replace-labels-in-vue-with-gitlab-ui-component.yml new file mode 100644 index 00000000000..8f5af1bcc54 --- /dev/null +++ b/changelogs/unreleased/38143-replace-labels-in-vue-with-gitlab-ui-component.yml @@ -0,0 +1,5 @@ +--- +title: Update labels in Vue with GlLabel component +merge_request: 21465 +author: +type: changed diff --git a/changelogs/unreleased/show-cluster-status-fe.yml b/changelogs/unreleased/show-cluster-status-fe.yml new file mode 100644 index 00000000000..b0ec9fa702b --- /dev/null +++ b/changelogs/unreleased/show-cluster-status-fe.yml @@ -0,0 +1,5 @@ +--- +title: Show cluster status (FE) +merge_request: 26368 +author: +type: added diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index a441dc9c4e1..c12826c80cd 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -1006,6 +1006,12 @@ unset http_proxy unset https_proxy ``` +### Gitaly not listening on new address after reconfiguring + +When updating the `gitaly['listen_addr']` or `gitaly['prometheus_listen_addr']` values, Gitaly may continue to listen on the old address after a `sudo gitlab-ctl reconfigure`. + +When this occurs, performing a `sudo gitlab-ctl restart` will resolve the issue. This will no longer be necessary after [this issue](https://gitlab.com/gitlab-org/gitaly/issues/2521) is resolved. + ### Praefect Praefect is an experimental daemon that allows for replication of the Git data. diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md index 783ae1e0317..42437ecd7f4 100644 --- a/doc/administration/gitaly/praefect.md +++ b/doc/administration/gitaly/praefect.md @@ -84,6 +84,8 @@ with secure tokens as you complete the setup process. Praefect cluster directly; that could lead to data loss. 1. `PRAEFECT_SQL_PASSWORD`: this password is used by Praefect to connect to PostgreSQL. +1. `GRAFANA_PASSWORD`: this password is used to access the `admin` + account in the Grafana dashboards. We will note in the instructions below where these secrets are required. @@ -184,6 +186,10 @@ application server, or a Gitaly node. # Make Praefect accept connections on all network interfaces. # Use firewalls to restrict access to this address/port. praefect['listen_addr'] = '0.0.0.0:2305' + + # Enable Prometheus metrics access to Praefect. You must use firewalls + # to restrict access to this address/port. + praefect['prometheus_listen_addr'] = '0.0.0.0:9652' ``` 1. Configure a strong `auth_token` for **Praefect** by editing @@ -354,6 +360,10 @@ documentation](index.md#3-gitaly-server-configuration). # Make Gitaly accept connections on all network interfaces. # Use firewalls to restrict access to this address/port. gitaly['listen_addr'] = '0.0.0.0:8075' + + # Enable Prometheus metrics access to Gitaly. You must use firewalls + # to restrict access to this address/port. + gitaly['prometheus_listen_addr'] = '0.0.0.0:9236' ``` 1. Configure a strong `auth_token` for **Gitaly** by editing @@ -453,7 +463,7 @@ Particular attention should be shown to: You will need to replace: - - `PRAEFECT_URL_OR_IP` with the IP/host address of the Praefect node + - `PRAEFECT_HOST` with the IP address or hostname of the Praefect node - `PRAEFECT_EXTERNAL_TOKEN` with the real secret ```ruby @@ -462,7 +472,7 @@ Particular attention should be shown to: "path" => "/var/opt/gitlab/git-data" }, "praefect" => { - "gitaly_address" => "tcp://PRAEFECT_URL_OR_IP:2305", + "gitaly_address" => "tcp://PRAEFECT_HOST:2305", "gitaly_token" => 'PRAEFECT_EXTERNAL_TOKEN' } }) @@ -478,6 +488,38 @@ Particular attention should be shown to: gitlab_shell['secret_token'] = 'GITLAB_SHELL_SECRET_TOKEN' ``` +1. Add Prometheus monitoring settings by editing `/etc/gitlab/gitlab.rb`. + + You will need to replace: + + - `PRAEFECT_HOST` with the IP address or hostname of the Praefect node + - `GITALY_HOST` with the IP address or hostname of each Gitaly node + + ```ruby + prometheus['scrape_configs'] = [ + { + 'job_name' => 'praefect', + 'static_configs' => [ + 'targets' => [ + 'PRAEFECT_HOST:9652' # praefect + ] + ] + }, + { + 'job_name' => 'praefect-gitaly', + 'static_configs' => [ + 'targets' => [ + 'GITALY_HOST:9236', # gitaly-1 + 'GITALY_HOST:9236', # gitaly-2 + 'GITALY_HOST:9236', # gitaly-3 + ] + ] + } + ] + + grafana['disable_login_form'] = false + ``` + 1. Save the changes to `/etc/gitlab/gitlab.rb` and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure): ```shell @@ -490,6 +532,12 @@ Particular attention should be shown to: sudo gitlab-rake gitlab:gitaly:check ``` +1. Set the Grafana admin password. This command will prompt you to enter a new password: + + ```shell + sudo gitlab-ctl set-grafana-password + ``` + 1. Update the **Repository storage** settings from **Admin Area > Settings > Repository > Repository storage** to make the newly configured Praefect cluster the storage location for new Git repositories. @@ -502,7 +550,12 @@ Particular attention should be shown to: repository that viewed. If the project is created, and you can see the README file, it works! -Congratulations! You have configured a highly available Praefect cluster, and +1. Inspect metrics by browsing to `/-/grafana` on your GitLab server. + Log in with `admin` / `GRAFANA_PASSWORD`. Go to 'Explore' and query + `gitlab_build_info` to verify that you are getting metrics from all your + machines. + +Congratulations! You have configured a highly available Praefect cluster. ## Migrating existing repositories to Praefect diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md index 6dc5542466f..b45f1ae529d 100644 --- a/doc/administration/raketasks/maintenance.md +++ b/doc/administration/raketasks/maintenance.md @@ -58,6 +58,7 @@ Runs the following rake tasks: - `gitlab:app:check` It will check that each component was set up according to the installation guide and suggest fixes for issues found. +This command must be run from your app server and will not work correctly on component servers like [Gitaly](../gitaly/index.md#running-gitaly-on-its-own-server). You may also have a look at our Troubleshooting Guides: diff --git a/doc/development/packages.md b/doc/development/packages.md index 848693d368a..0880e053901 100644 --- a/doc/development/packages.md +++ b/doc/development/packages.md @@ -23,7 +23,7 @@ The existing database model requires the following: - A package can have one or more package files. - The package model is based on storing information about the package and its version. -## API endpoints +### API endpoints Package systems work with GitLab via API. For example `ee/lib/api/npm_packages.rb` implements API endpoints to work with NPM clients. So, the first thing to do is to @@ -45,7 +45,7 @@ PUT https://gitlab.com/api/v4/projects/<your_project_id>/packages/npm/ Group-level and instance-level endpoints are good to have but are optional. -### Remote hierarchy +#### Remote hierarchy Packages are scoped within various levels of access, which is generally configured by setting your remote. A remote endpoint may be set at the project level, meaning when installing packages, only packages belonging to that @@ -68,7 +68,7 @@ NOTE: **Note:** NPM is currently a hybrid of the instance level and group level. It is using the top-level group or namespace as the defining portion of the name (for example, `@my-group-name/my-package-name`). -## Naming conventions +### Naming conventions To avoid name conflict for instance-level endpoints you will need to define a package naming convention that gives a way to identify the project that the package belongs to. This generally involves using the project @@ -82,36 +82,13 @@ a user from reusing an existing name within a given scope. Otherwise, naming should follow the package manager's naming conventions and include a validation in the `package.md` model for that package type. -## File uploads - -File uploads should be handled by GitLab Workhorse using object accelerated uploads. What this means is that -the workhorse proxy that checks all incoming requests to GitLab will intercept the upload request, -upload the file, and forward a request to the main GitLab codebase only containing the metadata -and file location rather than the file itself. An overview of this process can be found in the -[development documentation](uploads.md#direct-upload). - -In terms of code, this means a route will need to be added to the -[GitLab Workhorse project](https://gitlab.com/gitlab-org/gitlab-workhorse) for each level of remote being added -(instance, group, project). [This merge request](https://gitlab.com/gitlab-org/gitlab-workhorse/-/merge_requests/412/diffs) -demonstrates adding an instance-level endpoint for Conan to workhorse. You can also see the Maven project level endpoint -implemented in the same file. - -Once the route has been added, you will need to add an additional `/authorize` version of the upload endpoint to your API file. -[Here is an example](https://gitlab.com/gitlab-org/gitlab/blob/398fef1ca26ae2b2c3dc89750f6b20455a1e5507/ee/lib/api/maven_packages.rb#L164) -of the additional endpoint added for Maven. The `/authorize` endpoint verifies and authorizes the request from workhorse, -then the normal upload endpoint is implemented below, consuming the metadata that workhorse provides in order to -create the package record. Workhorse provides a variety of file metadata such as type, size, and different checksum formats. - -For testing purposes, you may want to [enable object storage](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/object_storage.md) -in your local development environment. - -## Services and finders +### Services and finders Logic for performing tasks such as creating package or package file records or finding packages should not live within the API file, but should live in services and finders. Existing services and finders should be used or extended when possible to keep the common package logic grouped as much as possible. -## Configuration +### Configuration GitLab has a `packages` section in its configuration file (`gitlab.rb`). It applies to all package systems supported by GitLab. Usually you don't need @@ -119,7 +96,96 @@ to add anything there. Packages can be configured to use object storage, therefore your code must support it. -## Database and handling metadata +## MVC Approach + +The way new package systems are integrated in GitLab is using an [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc). Therefore, the first iteration should support the bare minimum user actions: + +- Authentication +- Uploading a package +- Pulling a package +- Required actions + +Required actions are all the additional requests that GitLab will need to handle so the corresponding package manager CLI can work properly. It could be a search feature or an endpoint providing meta information about a package. For example: + +- For NuGet, the search request was implemented during the first MVC iteration, to support Visual Studio. +- For NPM, there is a metadata endpoint used by `npm` to get the tarball url. + +For the first MVC iteration, it's recommended to stay at the project level of the [remote hierarchy](#remote-hierarchy). Other levels can be tackled with [future Merge Requests](#future-work). + +There are usually 2 phases for the MVC: + +- [Analysis](#analysis) +- [Implementation](#implementation) + +### Keep iterations small + +When implementing a new package manager, it is tempting to create one large merge request containing all of the +necessary endpoints and services necessary to support basic usage. Instead, put the +API endpoints behind a [feature flag](feature_flags/development.md) and +submit each endpoint or behavior (download, upload, etc) in a different merge request to shorten the review +process. + +### Analysis + +During this phase, the idea is to collect as much information as possible about the API used by the package system. Here some aspects that can be useful to include: + +- **Authentication**: What authentication mechanisms are available (OAuth, Basic + Authorization, other). Keep in mind that GitLab users will often want to use their + [Personal Access Tokens](../user/profile/personal_access_tokens.md). + Although not needed for the MVC first iteration, the [CI job tokens](../user/project/new_ci_build_permissions_model.md#job-token) + have to be supported at some point in the future. +- **Requests**: Which requests are needed to have a working MVC. Ideally, produce + a list of all the requests needed for the MVC (including required actions). Further + investigation could provide an example for each request with the request and the response bodies. +- **Upload**: Carefully analyse how the upload process works. This will probably be the most + complex request to implement. A detailed analysis is desired here as uploads can be + encoded in different ways (body or multipart) and can even be in a totally different + format (for example, a JSON structure where the package file is a Base64 value of + a particular field). These different encodings lead to slightly different implementations + on GitLab and GitLab Workhorse. For more detailed information, review [file uploads](#file-uploads). +- **Endpoints**: Suggest a list of endpoint URLs that will be implemented in GitLab. +- **Split work**: Suggest a list of changes to do to incrementally build the MVC. + This will give a good idea of how much work there is to be done. Here is an example + list that would need to be adapted on a case by case basis: + 1. Empty file structure (API file, base service for this package) + 1. Authentication system for "logging in" to the package manager + 1. Identify metadata and create applicable tables + 1. Workhorse route for [object storage direct upload](uploads.md#direct-upload) + 1. Endpoints required for upload/publish + 1. Endpoints required for install/download + 1. Endpoints required for required actions + +The analysis usually takes a full milestone to complete, though it's not impossible to start the implementation in the same milestone. + +In particular, the upload request can have some [requirements in the GitLab Workhorse project](#file-uploads). This project has a different release cycle than the rails backend. It's **strongly** recommended that you open an issue there as soon as the upload request analysis is done. This way GitLab Worhorse is already ready when the upload request is implemented on the rails backend. + +### Implementation + +The implementation of the different Merge Requests will vary between different package system integrations. Contributors should take into account some important aspects of the implementation phase. + +#### Authentication + +The MVC must support [Personal Access Tokens](../user/profile/personal_access_tokens.md) right from the start. We currently support two options for these tokens: OAuth and Basic Access. + +OAuth authentication is already supported. You can see an example in the [npm API](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/api/npm_packages.rb). + +[Basic Access authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) +support is done by overriding a specific function in the API helpers, like +[this example in the Conan API](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/api/conan_packages.rb). +For this authentication mechanism, keep in mind that some clients can send an unauthenticated +request first, wait for the 401 Unauthorized response with the [`WWW-Authenticate`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate) +field, then send an updated (authenticated) request. This case is more involved as +GitLab needs to handle the 401 Unauthorized response. The [Nuget API](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/api/nuget_packages.rb) +supports this case. + +#### Authorization + +There are project and group level permissions for `read_package`, `create_package`, and `destroy_package`. Each +endpoint should +[authorize the requesting user](https://gitlab.com/gitlab-org/gitlab/blob/398fef1ca26ae2b2c3dc89750f6b20455a1e5507/ee/lib/api/conan_packages.rb) +against the project or group before continuing. + +#### Database and handling metadata The current database model allows you to store a name and a version for each package. Every time you upload a new package, you can either create a new record of `Package` @@ -137,44 +203,41 @@ delegate from the package model. Note that the existing package UI only displays information within the `packages_packages` and `packages_package_files` tables. If the data stored in the metadata tables need to be displayed, a ~frontend change will be required. -## Authorization +#### File uploads -There are project and group level permissions for `read_package`, `create_package`, and `destroy_package`. Each -endpoint should -[authorize the requesting user](https://gitlab.com/gitlab-org/gitlab/blob/398fef1ca26ae2b2c3dc89750f6b20455a1e5507/ee/lib/api/conan_packages.rb#L84) -against the project or group before continuing. - -## Keep iterations small - -When implementing a new package manager, it is easy to end up creating one large merge request containing all of the -necessary endpoints and services necessary to support basic usage. If this is the case, consider putting the -API endpoints behind a [feature flag](feature_flags/development.md) and -submitting each endpoint or behavior (download, upload, etc) in different merge requests to shorten the review -process. +File uploads should be handled by GitLab Workhorse using object accelerated uploads. What this means is that +the workhorse proxy that checks all incoming requests to GitLab will intercept the upload request, +upload the file, and forward a request to the main GitLab codebase only containing the metadata +and file location rather than the file itself. An overview of this process can be found in the +[development documentation](uploads.md#direct-upload). -### Potential MRs for any given package system +In terms of code, this means a route will need to be added to the +[GitLab Workhorse project](https://gitlab.com/gitlab-org/gitlab-workhorse) for each upload endpoint being added +(instance, group, project). [This merge request](https://gitlab.com/gitlab-org/gitlab-workhorse/-/merge_requests/412/diffs) +demonstrates adding an instance-level endpoint for Conan to workhorse. You can also see the Maven project level endpoint +implemented in the same file. -#### MVC MRs +Once the route has been added, you will need to add an additional `/authorize` version of the upload endpoint to your API file. +[Here is an example](https://gitlab.com/gitlab-org/gitlab/blob/398fef1ca26ae2b2c3dc89750f6b20455a1e5507/ee/lib/api/maven_packages.rb#L164) +of the additional endpoint added for Maven. The `/authorize` endpoint verifies and authorizes the request from workhorse, +then the normal upload endpoint is implemented below, consuming the metadata that workhorse provides in order to +create the package record. Workhorse provides a variety of file metadata such as type, size, and different checksum formats. -These changes represent all that is needed to deliver a minimally usable package management system. +For testing purposes, you may want to [enable object storage](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/object_storage.md) +in your local development environment. -1. Empty file structure (API file, base service for this package) -1. Authentication system for 'logging in' to the package manager -1. Identify metadata and create applicable tables -1. Workhorse route for [object storage direct upload](uploads.md#direct-upload) -1. Endpoints required for upload/publish -1. Endpoints required for install/download -1. Endpoints required for remove/delete +### Future Work -#### Possible post-MVC MRs +While working on the MVC, contributors will probably find features that are not mandatory for the MVC but can provide a better user experience. It's generally a good idea to keep an eye on those and open issues. -These updates are not essential to be able to publish and consume packages, but may be desired as the system is -released for general use. +Here are some examples 1. Endpoints required for search 1. Front end updates to display additional package information and metadata 1. Limits on file sizes 1. Tracking for metrics +1. Read more metadata fields from the package to make it available to the front end. For example, it's usual to be able to tag a package. Those tags can be read and saved by backend and then displayed on the packages UI. +1. Endpoints for the upper levels of the [remote hierarchy](#remote-hierarchy). This step might need to create a [naming convention](#naming-conventions) ## Exceptions diff --git a/doc/user/application_security/configuration/index.md b/doc/user/application_security/configuration/index.md index 29137c9b50c..da36d7ff95c 100644 --- a/doc/user/application_security/configuration/index.md +++ b/doc/user/application_security/configuration/index.md @@ -11,7 +11,7 @@ type: reference, howto The security configuration page displays the configuration state of each of the security features and can be accessed through a project's sidebar nav. -![Screenshot of security configuration page](../img/security_configuration_page_v12_6.png) +![Screenshot of security configuration page](../img/security_configuration_page_v12_9.png) The page uses the project's latest default branch [CI pipeline](../../../ci/pipelines.md) to determine the configuration state of each feature. If a job with the expected security report artifact exists in the pipeline, diff --git a/doc/user/application_security/img/security_configuration_page_v12_6.png b/doc/user/application_security/img/security_configuration_page_v12_6.png Binary files differdeleted file mode 100644 index d838b648c1f..00000000000 --- a/doc/user/application_security/img/security_configuration_page_v12_6.png +++ /dev/null diff --git a/doc/user/application_security/img/security_configuration_page_v12_9.png b/doc/user/application_security/img/security_configuration_page_v12_9.png Binary files differnew file mode 100644 index 00000000000..a81d82e03c3 --- /dev/null +++ b/doc/user/application_security/img/security_configuration_page_v12_9.png diff --git a/doc/user/packages/workflows/monorepo.md b/doc/user/packages/workflows/monorepo.md index 0c7fb4a1a20..5acd4fd0735 100644 --- a/doc/user/packages/workflows/monorepo.md +++ b/doc/user/packages/workflows/monorepo.md @@ -38,14 +38,14 @@ If you follow the instructions you can publish `MyProject` by running `npm publish` from the root directory. Publishing `Foo` is almost exactly the same, you simply have to follow the steps -while in the `Foo` directory. `Foo` will need it's own `package.json` file, -which can be added manually or using `npm init`. And it will need it's own +while in the `Foo` directory. `Foo` will need its own `package.json` file, +which can be added manually or using `npm init`. And it will need its own configuration settings. Since you are publishing to the same place, if you used `npm config set` to set the registry for the parent project, then no additional setup is necessary. If you used a `.npmrc` file, you will need an additional `.npmrc` file in the `Foo` directory (be sure to add `.npmrc` files to the `.gitignore` file or use environment variables in place of your access -tokens to preven them from being exposed). It can be identical to the +tokens to prevent them from being exposed). It can be identical to the one you used in `MyProject`. You can now run `npm publish` from the `Foo` directory and you will be able to publish `Foo` separately from `MyProject` diff --git a/doc/user/project/clusters/serverless/aws.md b/doc/user/project/clusters/serverless/aws.md index 95c0b7f1436..30ed82e45b6 100644 --- a/doc/user/project/clusters/serverless/aws.md +++ b/doc/user/project/clusters/serverless/aws.md @@ -4,13 +4,17 @@ GitLab allows users to easily deploy AWS Lambda functions and create rich server GitLab supports deployment of functions to AWS Lambda using a combination of: -- [Serverless Framework with AWS](https://serverless.com/framework/docs/providers/aws/) +- [Serverless Framework with AWS](#serverless-framework) +- [AWS' Serverless Application Model (SAM)](#aws-serverless-application-model) - GitLab CI/CD +## Serverless Framework + +The [Serverless Framework can deploy to AWS](https://serverless.com/framework/docs/providers/aws/). + We have prepared an example with a step-by-step guide to create a simple function and deploy it on AWS. -Additionally, in the [How To section](#how-to), you can read about different use cases, -like: +Additionally, in the [How To section](#how-to), you can read about different use cases like: - Running a function locally. - Working with secrets. @@ -18,27 +22,27 @@ like: Alternatively, you can quickly [create a new project with a template](../../../../gitlab-basics/create-project.md#project-templates). The [`Serverless Framework/JS` template](https://gitlab.com/gitlab-org/project-templates/serverless-framework/) already includes all parts described below. -## Example +### Example In the following example, you will: 1. Create a basic AWS Lambda Node.js function. 1. Link the function to an API Gateway `GET` endpoint. -### Steps +#### Steps The example consists of the following steps: -1. Creating a Lambda handler function -1. Creating a `serverless.yml` file -1. Crafting the `.gitlab-ci.yml` file -1. Setting up your AWS credentials with your GitLab account -1. Deploying your function -1. Testing the deployed function +1. Creating a Lambda handler function. +1. Creating a `serverless.yml` file. +1. Crafting the `.gitlab-ci.yml` file. +1. Setting up your AWS credentials with your GitLab account. +1. Deploying your function. +1. Testing the deployed function. Lets take it step by step. -### Creating a Lambda handler function +#### Creating a Lambda handler function Your Lambda function will be the primary handler of requests. In this case we will create a very simple Node.js `hello` function: @@ -67,7 +71,7 @@ In our case, `module.exports.hello` defines the `hello` handler that will be ref You can learn more about the AWS Lambda Node.js function handler and all its various options here: <https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html> -### Creating a `serverless.yml` file +#### Creating a `serverless.yml` file In the root of your project, create a `serverless.yml` file that will contain configuration specifics for the Serverless Framework. @@ -94,7 +98,7 @@ The `events` declaration will create a AWS API Gateway `GET` endpoint to receive You can read more about the available properties and additional configuration possibilities of the Serverless Framework here: <https://serverless.com/framework/docs/providers/aws/guide/serverless.yml/> -### Crafting the `.gitlab-ci.yml` file +#### Crafting the `.gitlab-ci.yml` file In a `.gitlab-ci.yml` file in the root of your project, place the following code: @@ -122,7 +126,7 @@ This example code does the following: - Deploys the serverless function to your AWS account using the AWS credentials defined above. -### Setting up your AWS credentials with your GitLab account +#### Setting up your AWS credentials with your GitLab account In order to interact with your AWS account, the GitLab CI/CD pipelines require both `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` to be defined in your GitLab settings under **Settings > CI/CD > Variables**. For more information please see: <https://docs.gitlab.com/ee/ci/variables/README.html#via-the-ui> @@ -130,7 +134,7 @@ For more information please see: <https://docs.gitlab.com/ee/ci/variables/README NOTE: **Note:** The AWS credentials you provide must include IAM policies that provision correct access control to AWS Lambda, API Gateway, CloudFormation, and IAM resources. -### Deploying your function +#### Deploying your function `git push` the changes to your GitLab repository and the GitLab build pipeline will automatically deploy your function. @@ -142,7 +146,7 @@ endpoints: GET - https://u768nzby1j.execute-api.us-east-1.amazonaws.com/production/hello ``` -### Manually testing your function +#### Manually testing your function Running the following `curl` command should trigger your function. @@ -165,7 +169,7 @@ Hooray! You now have a AWS Lambda function deployed via GitLab CI. Nice work! -## How To +### How To In this section, we show you how to build on the basic example to: @@ -173,7 +177,7 @@ In this section, we show you how to build on the basic example to: - Set up secret variables. - Set up CORS. -### Running function locally +#### Running function locally The `serverless-offline` plugin allows to run your code locally. To run your code locally: @@ -204,7 +208,7 @@ It should output: } ``` -### Secret variables +#### Secret variables Secrets are injected into your functions using environment variables. @@ -225,7 +229,7 @@ NOTE: **Note:** Anyone with access to the AWS environment may be able to see the values of those variables persisted in the lambda definition. -### Setting up CORS +#### Setting up CORS If you want to set up a web page that makes calls to your function, like we have done in the [template](https://gitlab.com/gitlab-org/project-templates/serverless-framework/), you need to deal with the Cross-Origin Resource Sharing (CORS). @@ -269,13 +273,13 @@ module.exports.hello = async event => { For more information, see the [Your CORS and API Gateway survival guide](https://serverless.com/blog/cors-api-gateway-survival-guide/) blog post written by the Serverless Framework team. -### Writing automated tests +#### Writing automated tests The [Serverless Framework](https://gitlab.com/gitlab-org/project-templates/serverless-framework/) example project shows how to use Jest, Axios, and `serverless-offline` plugin to do automated testing of both local and deployed serverless function. -## Examples and template +### Examples and template The example code is available: @@ -285,3 +289,225 @@ The example code is available: You can also use a [template](../../../../gitlab-basics/create-project.md#project-templates) (based on the version with tests and secret variables) from within the GitLab UI (see the `Serverless Framework/JS` template). + +## AWS Serverless Application Model + +AWS Serverless Application Model is an open source framework for building serverless +applications. It makes it easier to build and deploy serverless applications. For more +details, please take a look at AWS documentation on [AWS Serverless Application Model](https://docs.aws.amazon.com/serverless-application-model/). + +### Deploying AWS Lambda function using AWS SAM and GitLab CI/CD + +GitLab allows developers to build and deploy serverless applications using the combination of: + +- [AWS Serverless Application Model (AWS SAM)](https://aws.amazon.com/serverless/sam/). +- GitLab CI/CD. + +### Example + +In the following example, you will: + +- Install SAM CLI. +- Create a sample SAM application including a Lambda function and API Gateway. +- Build and deploy the application to your AWS account using GitLab CI/CD. + +### Steps + +The example consists of the following steps: + +1. Installing SAM CLI. +1. Creating an AWS SAM application using SAM CLI. +1. Crafting the `.gitlab-ci.yml` file. +1. Setting up your AWS credentials with your GitLab account. +1. Deploying your application. +1. Testing the deployed function. + +### Installing SAM CLI + +AWS SAM provides a CLI called AWS SAM CLI to make it easier to create and manage +applications. + +Some steps in this documentation use SAM CLI. Follow the instructions for +[installing SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) +to install and configure SAM CLI. + +If you use [AWS Cloud9](https://aws.amazon.com/cloud9/) as your integrated development +environment (IDE), the following are installed for you: + +- [AWS Command Line Interface](https://docs.aws.amazon.com/en_pv/cli/latest/userguide/cli-chap-install.html) +- [SAM CLI](https://docs.aws.amazon.com/en_pv/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) +- [Docker](https://docs.docker.com/install/) and necessary Docker images. + +### Creating an AWS SAM application using SAM CLI + +To create a new AWS SAM application: + +1. Create a new GitLab project. +1. `git clone` the project into your local environment. +1. Change to the newly cloned project and create a new SAM app using the following command: + + ```shell + sam init -r python3.8 -n gitlabpoc --app-template "hello-world" + ``` + +1. `git push` the application back to the GitLab project. + +This creates a SAM app named `gitlabpoc` using the default configuration, a single +Python 3.8 function invoked by an [Amazon API Gateway](https://aws.amazon.com/api-gateway/) +endpoint. To see additional runtimes supported by SAM and options for `sam init`, run: + +```shell +sam init -h +``` + +### Setting up your AWS credentials with your GitLab account + +In order to interact with your AWS account, the GitLab CI/CD pipelines require both +`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` to be set in the project's CI/CD +variables. + +To set these: + +1. Navigate to the project's **{settings}** **Settings > CI / CD**. +1. Expand the **Variables** section and create entires for `AWS_ACCESS_KEY_ID` and + `AWS_SECRET_ACCESS_KEY`. +1. Mask the credentials so they do not show in logs using the **Masked** toggle. + +NOTE: **Note:** +The AWS credentials you provide must include IAM policies that provision correct access +control to AWS Lambda, API Gateway, CloudFormation, and IAM resources. + +### Crafting the `.gitlab-ci.yml` file + +In a [`.gitlab-ci.yml`](../../../../ci/yaml/README.md) file in the root of your project, +add the following and replace <S3_bucket_name> with the name of the S3 bucket where you +want to store your package: + +```yaml +image: python:latest + +stages: + + - deploy + +production: + + stage: deploy + + before_script: + + - pip3 install awscli --upgrade + + - pip3 install aws-sam-cli --upgrade + + script: + + - sam build + + - sam package --output-template-file packaged.yaml --s3-bucket <S3_bucket_name> + + - sam deploy --template-file packaged.yaml --stack-name gitlabpoc --s3-bucket <S3_bucket_name> --capabilities CAPABILITY_IAM --region us-east-1 + + environment: production + ``` + +Let’s examine the config file more closely: + +- `image` specifies the Docker image to use for this build. This is the latest Python + image since the sample application is written in Python. +- AWS CLI and AWS SAM CLI are installed in the `before_script` section. +- SAM build, package, and deploy commands are used to build, package, and deploy the + application. + +### Deploying your application + +Push changes to your GitLab repository and the GitLab build pipeline will automatically +deploy your application. If your: + +- Build and deploy are successful, [test your deployed application](#testing-the-deployed-application). +- Build fails, look at the build log to see why the build failed. Some common reasons + the build might fail are: + + - Incompatible versions of software. For example, Python runtime version might be + different from the Python on the build machine. Address this by installing the + required versions of the software. + - You may not be able to access your AWS account from GitLab. Check the environment + variables you set up with AWS credentials. + - You may not have permission to deploy a serverless application. Make sure you + provide all required permissions to deploy a serverless application. + +### Testing the deployed application + +To test the application you deployed, please go to the build log and follow the following steps: + +1. Click on “Show complete raw” on the upper right-hand corner: + + ![sam-complete-raw](img/sam-complete-raw.png) + +1. Look for HelloWorldApi – API Gateway endpoint similar to shown below: + + ![sam-api-endpoint](img/sam-api-endpoint.png) + +1. Use curl to test the API. For example: + + ```shell + curl https://py4rg7qtlg.execute-api.us-east-1.amazonaws.com/Prod/hello/ + ``` + +Output should be: + +```json +{"message": "hello world"} +``` + +### Testing Locally + +AWS SAM provides functionality to test your applications locally. You must have AWS SAM +CLI installed locally for you to test locally. + +First, test the function. + +SAM provides a default event in `events/event.json` that includes a message body of: + +```json +{\“message\”: \“hello world\”} +``` + +If you pass that event into the `HelloWorldFunction`, it should respond with the same +body. + +Invoke the function by running: + +```shell +sam local invoke HelloWorldFunction -e events/event.json +``` + +Output should be: + +```json +{"message": "hello world"} +``` + +After you confirm that Lambda function is working as expected, test the API Gateway +using following steps. + +Start the API locally by running: + +```shell +sam local start-api +``` + +SAM again launches a Docker container, this time with a mocked Amazon API Gateway +listening on `localhost:3000`. + +Call the `hello` API by running: + +```shell +curl http://127.0.0.1:3000/hello +``` + +Output again should be: + +```json +{"message": "hello world"} +``` diff --git a/doc/user/project/clusters/serverless/img/sam-api-endpoint.png b/doc/user/project/clusters/serverless/img/sam-api-endpoint.png Binary files differnew file mode 100644 index 00000000000..695d975387f --- /dev/null +++ b/doc/user/project/clusters/serverless/img/sam-api-endpoint.png diff --git a/doc/user/project/clusters/serverless/img/sam-complete-raw.png b/doc/user/project/clusters/serverless/img/sam-complete-raw.png Binary files differnew file mode 100644 index 00000000000..1098c1bb93f --- /dev/null +++ b/doc/user/project/clusters/serverless/img/sam-complete-raw.png diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index 43ff5d9895a..8631c8a649d 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -155,10 +155,17 @@ Multiple metrics can be displayed on the same chart if the fields **Name**, **Ty #### Query Variables -GitLab supports a limited set of [CI variables](../../../ci/variables/README.md) in the Prometheus query. This is particularly useful for identifying a specific environment, for example with `CI_ENVIRONMENT_SLUG`. The supported variables are: +GitLab supports a limited set of [CI variables](../../../ci/variables/README.md) in the Prometheus query. This is particularly useful for identifying a specific environment, for example with `ci_environment_slug`. The supported variables are: -- CI_ENVIRONMENT_SLUG -- KUBE_NAMESPACE +- `ci_environment_slug` +- `kube_namespace` +- `ci_project_name` +- `ci_project_namespace` +- `ci_project_path` +- `ci_environment_name` + +NOTE: **Note:** +Variables for Prometheus queries must be lowercase. There are 2 methods to specify a variable in a query or dashboard: diff --git a/lib/gitlab/prometheus/query_variables.rb b/lib/gitlab/prometheus/query_variables.rb index ba2d33ee1c1..4d48c4a3af7 100644 --- a/lib/gitlab/prometheus/query_variables.rb +++ b/lib/gitlab/prometheus/query_variables.rb @@ -7,7 +7,11 @@ module Gitlab { ci_environment_slug: environment.slug, kube_namespace: environment.deployment_namespace || '', - environment_filter: %{container_name!="POD",environment="#{environment.slug}"} + environment_filter: %{container_name!="POD",environment="#{environment.slug}"}, + ci_project_name: environment.project.name, + ci_project_namespace: environment.project.namespace.name, + ci_project_path: environment.project.full_path, + ci_environment_name: environment.name } end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3c9520fc58f..babf073fd4a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2026,10 +2026,10 @@ msgstr "" msgid "Analyze a review version of your web application." msgstr "" -msgid "Analyze your dependencies for known vulnerabilities" +msgid "Analyze your dependencies for known vulnerabilities." msgstr "" -msgid "Analyze your source code for known vulnerabilities" +msgid "Analyze your source code for known vulnerabilities." msgstr "" msgid "Ancestors" @@ -2509,6 +2509,9 @@ msgstr "" msgid "Authenticating" msgstr "" +msgid "Authentication Failure" +msgstr "" + msgid "Authentication Log" msgstr "" @@ -3460,7 +3463,7 @@ msgstr "" msgid "Check your .gitlab-ci.yml" msgstr "" -msgid "Check your Docker images for known vulnerabilities" +msgid "Check your Docker images for known vulnerabilities." msgstr "" msgid "Checking %{text} availability…" @@ -5169,6 +5172,9 @@ msgstr "" msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled." msgstr "" +msgid "Connected" +msgstr "" + msgid "Connecting" msgstr "" @@ -6373,6 +6379,9 @@ msgstr "" msgid "Deleted in this version" msgstr "" +msgid "Deleting" +msgstr "" + msgid "Deleting the license failed." msgstr "" @@ -16986,9 +16995,6 @@ msgstr "" msgid "Scoped issue boards" msgstr "" -msgid "Scoped label" -msgstr "" - msgid "Scopes" msgstr "" @@ -17085,7 +17091,7 @@ msgstr "" msgid "Search users or groups" msgstr "" -msgid "Search your project dependencies for their licenses and apply policies" +msgid "Search your project dependencies for their licenses and apply policies." msgstr "" msgid "Search your projects" @@ -17311,7 +17317,7 @@ msgstr "" msgid "SecurityConfiguration|Feature" msgstr "" -msgid "SecurityConfiguration|Feature documentation" +msgid "SecurityConfiguration|Feature documentation for %{featureName}" msgstr "" msgid "SecurityConfiguration|Not yet configured" @@ -18720,6 +18726,9 @@ msgstr "" msgid "Status:" msgstr "" +msgid "Status: %{title}" +msgstr "" + msgid "Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments." msgstr "" @@ -21101,6 +21110,9 @@ msgstr "" msgid "Unmarks this %{noun} as Work In Progress." msgstr "" +msgid "Unreachable" +msgstr "" + msgid "Unresolve" msgstr "" diff --git a/package.json b/package.json index 3eb5bf5c9a6..77d5f154091 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,8 @@ "@babel/plugin-syntax-import-meta": "^7.8.3", "@babel/preset-env": "^7.8.4", "@gitlab/at.js": "^1.5.5", - "@gitlab/svgs": "^1.105.0", - "@gitlab/ui": "^9.21.1", + "@gitlab/svgs": "^1.110.0", + "@gitlab/ui": "^9.23.0", "@gitlab/visual-review-tools": "1.5.1", "@sentry/browser": "^5.10.2", "@sourcegraph/code-host-integration": "0.0.30", @@ -90,7 +90,7 @@ "jquery-ujs": "1.2.2", "jquery.caret": "^0.3.1", "jquery.waitforimages": "^2.2.0", - "js-cookie": "^2.1.3", + "js-cookie": "^2.2.1", "jszip": "^3.1.3", "jszip-utils": "^0.0.2", "katex": "^0.10.0", diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index db6bb639e79..0ac8e7c5fc8 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -519,7 +519,7 @@ describe 'Issue Boards', :js do page.within(find('.board:nth-child(2)')) do expect(page).to have_selector('.board-card', count: 8) expect(find('.board-card', match: :first)).to have_content(bug.title) - click_button(bug.title) + click_link(bug.title) wait_for_requests end @@ -536,7 +536,7 @@ describe 'Issue Boards', :js do it 'removes label filter by clicking label button on issue' do page.within(find('.board:nth-child(2)')) do page.within(find('.board-card', match: :first)) do - click_button(bug.title) + click_link(bug.title) end wait_for_requests diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index e54c11f657d..f30689240c5 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -305,7 +305,7 @@ describe 'Issue Boards', :js do end # 'Development' label does not show since the card is in a 'Development' list label - expect(card).to have_selector('.badge', count: 2) + expect(card).to have_selector('.gl-label', count: 2) expect(card).to have_content(bug.title) end @@ -335,7 +335,7 @@ describe 'Issue Boards', :js do end # 'Development' label does not show since the card is in a 'Development' list label - expect(card).to have_selector('.badge', count: 3) + expect(card).to have_selector('.gl-label', count: 3) expect(card).to have_content(bug.title) expect(card).to have_content(regression.title) end diff --git a/spec/models/merge_request/pipelines_spec.rb b/spec/finders/ci/pipelines_for_merge_request_finder_spec.rb index 0afbcc60ed6..c49ac487519 100644 --- a/spec/models/merge_request/pipelines_spec.rb +++ b/spec/finders/ci/pipelines_for_merge_request_finder_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe MergeRequest::Pipelines do +describe Ci::PipelinesForMergeRequestFinder do describe '#all' do let(:merge_request) { create(:merge_request) } let(:project) { merge_request.source_project } diff --git a/spec/frontend/blob/components/__snapshots__/blob_edit_content_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_edit_content_spec.js.snap new file mode 100644 index 00000000000..0409b118222 --- /dev/null +++ b/spec/frontend/blob/components/__snapshots__/blob_edit_content_spec.js.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Blob Header Editing rendering matches the snapshot 1`] = ` +<div + class="file-content code" +> + <pre + data-editor-loading="" + id="editor" + > + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + </pre> +</div> +`; diff --git a/spec/frontend/blob/components/blob_edit_content_spec.js b/spec/frontend/blob/components/blob_edit_content_spec.js new file mode 100644 index 00000000000..eff53fe7ce9 --- /dev/null +++ b/spec/frontend/blob/components/blob_edit_content_spec.js @@ -0,0 +1,81 @@ +import { shallowMount } from '@vue/test-utils'; +import BlobEditContent from '~/blob/components/blob_edit_content.vue'; +import { initEditorLite } from '~/blob/utils'; +import { nextTick } from 'vue'; + +jest.mock('~/blob/utils', () => ({ + initEditorLite: jest.fn(), +})); + +describe('Blob Header Editing', () => { + let wrapper; + const value = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'; + const fileName = 'lorem.txt'; + + function createComponent() { + wrapper = shallowMount(BlobEditContent, { + propsData: { + value, + fileName, + }, + }); + } + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('rendering', () => { + it('matches the snapshot', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('renders content', () => { + expect(wrapper.text()).toContain(value); + }); + }); + + describe('functionality', () => { + it('initialises Editor Lite', () => { + const el = wrapper.find({ ref: 'editor' }).element; + expect(initEditorLite).toHaveBeenCalledWith({ + el, + blobPath: fileName, + blobContent: value, + }); + }); + + it('reacts to the changes in fileName', () => { + wrapper.vm.editor = { + updateModelLanguage: jest.fn(), + }; + + const newFileName = 'ipsum.txt'; + + wrapper.setProps({ + fileName: newFileName, + }); + + return nextTick().then(() => { + expect(wrapper.vm.editor.updateModelLanguage).toHaveBeenCalledWith(newFileName); + }); + }); + + it('emits input event when the blob content is changed', () => { + const editorEl = wrapper.find({ ref: 'editor' }); + wrapper.vm.editor = { + getValue: jest.fn().mockReturnValue(value), + }; + + editorEl.trigger('focusout'); + + return nextTick().then(() => { + expect(wrapper.emitted().input[0]).toEqual([value]); + }); + }); + }); +}); diff --git a/spec/frontend/blob/utils_spec.js b/spec/frontend/blob/utils_spec.js new file mode 100644 index 00000000000..39a73aae444 --- /dev/null +++ b/spec/frontend/blob/utils_spec.js @@ -0,0 +1,95 @@ +import Editor from '~/editor/editor_lite'; +import * as utils from '~/blob/utils'; + +const mockCreateMonacoInstance = jest.fn(); +jest.mock('~/editor/editor_lite', () => { + return jest.fn().mockImplementation(() => { + return { createInstance: mockCreateMonacoInstance }; + }); +}); + +const mockCreateAceInstance = jest.fn(); +global.ace = { + edit: mockCreateAceInstance, +}; + +describe('Blob utilities', () => { + beforeEach(() => { + Editor.mockClear(); + }); + + describe('initEditorLite', () => { + let editorEl; + const blobPath = 'foo.txt'; + const blobContent = 'Foo bar'; + + beforeEach(() => { + setFixtures('<div id="editor"></div>'); + editorEl = document.getElementById('editor'); + }); + + describe('Monaco editor', () => { + let origProp; + + beforeEach(() => { + origProp = window.gon; + window.gon = { + features: { + monacoSnippets: true, + }, + }; + }); + + afterEach(() => { + window.gon = origProp; + }); + + it('initializes the Editor Lite', () => { + utils.initEditorLite({ el: editorEl }); + expect(Editor).toHaveBeenCalled(); + }); + + it('creates the instance with the passed parameters', () => { + utils.initEditorLite({ el: editorEl }); + expect(mockCreateMonacoInstance.mock.calls[0]).toEqual([ + { + el: editorEl, + blobPath: undefined, + blobContent: undefined, + }, + ]); + + utils.initEditorLite({ el: editorEl, blobPath, blobContent }); + expect(mockCreateMonacoInstance.mock.calls[1]).toEqual([ + { + el: editorEl, + blobPath, + blobContent, + }, + ]); + }); + }); + describe('ACE editor', () => { + let origProp; + + beforeEach(() => { + origProp = window.gon; + window.gon = { + features: { + monacoSnippets: false, + }, + }; + }); + + afterEach(() => { + window.gon = origProp; + }); + + it('does not initialize the Editor Lite', () => { + utils.initEditorLite({ el: editorEl }); + expect(Editor).not.toHaveBeenCalled(); + expect(mockCreateAceInstance).toHaveBeenCalledWith(editorEl); + }); + }); + }); +}); diff --git a/spec/frontend/boards/components/issue_card_inner_scoped_label_spec.js b/spec/frontend/boards/components/issue_card_inner_scoped_label_spec.js deleted file mode 100644 index 53e670e76da..00000000000 --- a/spec/frontend/boards/components/issue_card_inner_scoped_label_spec.js +++ /dev/null @@ -1,40 +0,0 @@ -import { GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import IssueCardInnerScopedLabel from '~/boards/components/issue_card_inner_scoped_label.vue'; - -describe('IssueCardInnerScopedLabel Component', () => { - let wrapper; - - beforeEach(() => { - wrapper = shallowMount(IssueCardInnerScopedLabel, { - propsData: { - label: { title: 'Foo::Bar', description: 'Some Random Description' }, - labelStyle: { background: 'white', color: 'black' }, - scopedLabelsDocumentationLink: '/docs-link', - }, - }); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('should render label title', () => { - expect(wrapper.find('.color-label').text()).toBe('Foo::Bar'); - }); - - it('should render question mark symbol', () => { - expect(wrapper.find('.fa-question-circle').exists()).toBe(true); - }); - - it('should render label style provided', () => { - const label = wrapper.find('.color-label'); - - expect(label.attributes('style')).toContain('background: white;'); - expect(label.attributes('style')).toContain('color: black;'); - }); - - it('should render the docs link', () => { - expect(wrapper.find(GlLink).attributes('href')).toBe('/docs-link'); - }); -}); diff --git a/spec/frontend/boards/issue_card_spec.js b/spec/frontend/boards/issue_card_spec.js index 1fd2b417aba..f78e4dad2c0 100644 --- a/spec/frontend/boards/issue_card_spec.js +++ b/spec/frontend/boards/issue_card_spec.js @@ -8,6 +8,7 @@ import '~/boards/models/list'; import IssueCardInner from '~/boards/components/issue_card_inner.vue'; import { listObj } from '../../javascripts/boards/mock_data'; import store from '~/boards/stores'; +import { GlLabel } from '@gitlab/ui'; describe('Issue card component', () => { const user = new ListAssignee({ @@ -20,7 +21,7 @@ describe('Issue card component', () => { const label1 = new ListLabel({ id: 3, title: 'testing 123', - color: 'blue', + color: '#000CFF', text_color: 'white', description: 'test', }); @@ -50,6 +51,9 @@ describe('Issue card component', () => { rootPath: '/', }, store, + stubs: { + GlLabel: true, + }, }); }); @@ -290,25 +294,11 @@ describe('Issue card component', () => { }); it('does not render list label but renders all other labels', () => { - expect(wrapper.findAll('.badge').length).toBe(1); - }); - - it('renders label', () => { - const nodes = wrapper.findAll('.badge').wrappers.map(label => label.attributes('title')); - - expect(nodes.includes(label1.description)).toBe(true); - }); - - it('sets label description as title', () => { - expect(wrapper.find('.badge').attributes('title')).toContain(label1.description); - }); - - it('sets background color of button', () => { - const nodes = wrapper - .findAll('.badge') - .wrappers.map(label => label.element.style.backgroundColor); - - expect(nodes.includes(label1.color)).toBe(true); + expect(wrapper.findAll(GlLabel).length).toBe(1); + const label = wrapper.find(GlLabel); + expect(label.props('title')).toEqual(label1.title); + expect(label.props('description')).toEqual(label1.description); + expect(label.props('backgroundColor')).toEqual(label1.color); }); it('does not render label if label does not have an ID', done => { @@ -321,7 +311,7 @@ describe('Issue card component', () => { wrapper.vm .$nextTick() .then(() => { - expect(wrapper.findAll('.badge').length).toBe(1); + expect(wrapper.findAll(GlLabel).length).toBe(1); expect(wrapper.text()).not.toContain('closed'); done(); }) diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js index 825bc7813a5..85c86b2c0a9 100644 --- a/spec/frontend/clusters_list/components/clusters_spec.js +++ b/spec/frontend/clusters_list/components/clusters_spec.js @@ -1,7 +1,8 @@ +import Vuex from 'vuex'; import { createLocalVue, mount } from '@vue/test-utils'; import { GlTable, GlLoadingIcon } from '@gitlab/ui'; import Clusters from '~/clusters_list/components/clusters.vue'; -import Vuex from 'vuex'; +import mockData from '../mock_data'; const localVue = createLocalVue(); localVue.use(Vuex); @@ -11,9 +12,10 @@ describe('Clusters', () => { const findTable = () => wrapper.find(GlTable); const findLoader = () => wrapper.find(GlLoadingIcon); + const findStatuses = () => findTable().findAll('.js-status'); const mountComponent = _state => { - const state = { clusters: [], endpoint: 'some/endpoint', ..._state }; + const state = { clusters: mockData, endpoint: 'some/endpoint', ..._state }; const store = new Vuex.Store({ state, }); @@ -52,4 +54,25 @@ describe('Clusters', () => { expect(findTable().classes()).toContain('b-table-stacked-md'); }); }); + + describe('cluster status', () => { + it.each` + statusName | className | lineNumber + ${'disabled'} | ${'disabled'} | ${0} + ${'unreachable'} | ${'bg-danger'} | ${1} + ${'authentication_failure'} | ${'bg-warning'} | ${2} + ${'deleting'} | ${null} | ${3} + ${'connected'} | ${'bg-success'} | ${4} + `('renders a status for each cluster', ({ statusName, className, lineNumber }) => { + const statuses = findStatuses(); + const status = statuses.at(lineNumber); + if (statusName !== 'deleting') { + const statusIndicator = status.find('.cluster-status-indicator'); + expect(statusIndicator.exists()).toBe(true); + expect(statusIndicator.classes()).toContain(className); + } else { + expect(status.find(GlLoadingIcon).exists()).toBe(true); + } + }); + }); }); diff --git a/spec/frontend/clusters_list/mock_data.js b/spec/frontend/clusters_list/mock_data.js new file mode 100644 index 00000000000..0a49c2e9f43 --- /dev/null +++ b/spec/frontend/clusters_list/mock_data.js @@ -0,0 +1,37 @@ +export default [ + { + name: 'My Cluster 1', + environmentScope: '*', + size: '3', + clusterType: 'group_type', + status: 'disabled', + }, + { + name: 'My Cluster 2', + environmentScope: 'development', + size: '12', + clusterType: 'project_type', + status: 'unreachable', + }, + { + name: 'My Cluster 3', + environmentScope: 'development', + size: '12', + clusterType: 'project_type', + status: 'authentication_failure', + }, + { + name: 'My Cluster 4', + environmentScope: 'production', + size: '12', + clusterType: 'project_type', + status: 'deleting', + }, + { + name: 'My Cluster 5', + environmentScope: 'development', + size: '12', + clusterType: 'project_type', + status: 'connected', + }, +]; diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap new file mode 100644 index 00000000000..b1bbe2a9710 --- /dev/null +++ b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Snippet Blob Edit component rendering matches the snapshot 1`] = ` +<div + class="form-group file-editor" +> + <label> + File + </label> + + <div + class="file-holder snippet" + > + <blob-header-edit-stub + value="lorem.txt" + /> + + <blob-content-edit-stub + filename="lorem.txt" + value="Lorem ipsum dolor sit amet, consectetur adipiscing elit." + /> + </div> +</div> +`; diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js new file mode 100644 index 00000000000..42b49c50c75 --- /dev/null +++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js @@ -0,0 +1,40 @@ +import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue'; +import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue'; +import BlobContentEdit from '~/blob/components/blob_edit_content.vue'; +import { shallowMount } from '@vue/test-utils'; + +jest.mock('~/blob/utils', () => jest.fn()); + +describe('Snippet Blob Edit component', () => { + let wrapper; + const content = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'; + const fileName = 'lorem.txt'; + + function createComponent() { + wrapper = shallowMount(SnippetBlobEdit, { + propsData: { + content, + fileName, + }, + }); + } + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('rendering', () => { + it('matches the snapshot', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('renders required components', () => { + expect(wrapper.contains(BlobHeaderEdit)).toBe(true); + expect(wrapper.contains(BlobContentEdit)).toBe(true); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js index 54ad96073c8..06355c0dd65 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js @@ -1,31 +1,26 @@ import { mount } from '@vue/test-utils'; -import { hexToRgb } from '~/lib/utils/color_utils'; import DropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue'; -import DropdownValueScopedLabel from '~/vue_shared/components/sidebar/labels_select/dropdown_value_scoped_label.vue'; +import { GlLabel } from '@gitlab/ui'; import { mockConfig, mockLabels, } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data'; -const labelStyles = { - textColor: '#FFFFFF', - color: '#BADA55', -}; const createComponent = ( labels = mockLabels, labelFilterBasePath = mockConfig.labelFilterBasePath, -) => { - labels.forEach(label => Object.assign(label, labelStyles)); - - return mount(DropdownValueComponent, { +) => + mount(DropdownValueComponent, { propsData: { labels, labelFilterBasePath, enableScopedLabels: true, }, + stubs: { + GlLabel: true, + }, }); -}; describe('DropdownValueComponent', () => { let vm; @@ -56,24 +51,17 @@ describe('DropdownValueComponent', () => { describe('methods', () => { describe('labelFilterUrl', () => { it('returns URL string starting with labelFilterBasePath and encoded label.title', () => { - expect(vm.find(DropdownValueScopedLabel).props('labelFilterUrl')).toBe( - '/gitlab-org/my-project/issues?label_name[]=Foo%3A%3ABar', + expect(vm.find(GlLabel).props('target')).toBe( + '/gitlab-org/my-project/issues?label_name[]=Foo%20Label', ); }); }); - describe('labelStyle', () => { - it('returns object with `color` & `backgroundColor` properties from label.textColor & label.color', () => { - expect(vm.find(DropdownValueScopedLabel).props('labelStyle')).toEqual({ - color: labelStyles.textColor, - backgroundColor: labelStyles.color, - }); - }); - }); - describe('showScopedLabels', () => { it('returns true if the label is scoped label', () => { - expect(vm.findAll(DropdownValueScopedLabel).length).toEqual(1); + const labels = vm.findAll(GlLabel); + expect(labels.length).toEqual(2); + expect(labels.at(1).props('scoped')).toBe(true); }); }); }); @@ -95,33 +83,10 @@ describe('DropdownValueComponent', () => { vmEmptyLabels.destroy(); }); - it('renders label element with filter URL', () => { - expect(vm.find('a').attributes('href')).toBe( - '/gitlab-org/my-project/issues?label_name[]=Foo%20Label', - ); - }); - - it('renders label element and styles based on label details', () => { - const labelEl = vm.find('a span.badge.color-label'); + it('renders DropdownValueComponent element', () => { + const labelEl = vm.find(GlLabel); expect(labelEl.exists()).toBe(true); - expect(labelEl.attributes('style')).toContain( - `background-color: rgb(${hexToRgb(labelStyles.color).join(', ')});`, - ); - expect(labelEl.text().trim()).toBe(mockLabels[0].title); - }); - - describe('label is of scoped-label type', () => { - it('renders a scoped-label-wrapper span to incorporate 2 anchors', () => { - expect(vm.find('span.scoped-label-wrapper').exists()).toBe(true); - }); - - it('renders anchor tag containing question icon', () => { - const anchor = vm.find('.scoped-label-wrapper a.scoped-label'); - - expect(anchor.exists()).toBe(true); - expect(anchor.find('i.fa-question-circle').exists()).toBe(true); - }); }); }); }); diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index 72367377929..2b0eee8b95d 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -32,7 +32,7 @@ describe('Board card', () => { const label1 = new ListLabel({ id: 3, title: 'testing 123', - color: 'blue', + color: '#000cff', text_color: 'white', description: 'test', }); @@ -155,12 +155,6 @@ describe('Board card', () => { expect(boardsStore.detail.issue).toEqual({}); }); - it('does not set detail issue if button is clicked', () => { - triggerEvent('mouseup', vm.$el.querySelector('button')); - - expect(boardsStore.detail.issue).toEqual({}); - }); - it('does not set detail issue if img is clicked', done => { vm.issue.assignees = [ new ListAssignee({ diff --git a/spec/lib/gitlab/prometheus/query_variables_spec.rb b/spec/lib/gitlab/prometheus/query_variables_spec.rb index 849265de513..d8f8a2b7e7c 100644 --- a/spec/lib/gitlab/prometheus/query_variables_spec.rb +++ b/spec/lib/gitlab/prometheus/query_variables_spec.rb @@ -11,6 +11,10 @@ describe Gitlab::Prometheus::QueryVariables do subject { described_class.call(environment) } it { is_expected.to include(ci_environment_slug: slug) } + it { is_expected.to include(ci_project_name: project.name) } + it { is_expected.to include(ci_project_namespace: project.namespace.name) } + it { is_expected.to include(ci_project_path: project.full_path) } + it { is_expected.to include(ci_environment_name: environment.name) } it do is_expected.to include(environment_filter: diff --git a/yarn.lock b/yarn.lock index 7ef43bf902f..772951f7b98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -796,15 +796,15 @@ dependencies: vue-eslint-parser "^7.0.0" -"@gitlab/svgs@^1.105.0": - version "1.105.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.105.0.tgz#9686f8696594a5f22de11af2b81fdcceb715f4f2" - integrity sha512-2wzZXe2b7DnGyL7FTbPq0dSpk+gjkq4SBTNtMrqdwX2qaM+XJB50XaMm17kdY5V1bBkMgbc7JJ2vgbLxhS/CkQ== +"@gitlab/svgs@^1.110.0": + version "1.110.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.110.0.tgz#3c4f5f0e78fcf616ec63a265754158b84ed80af8" + integrity sha512-bLVUW9Hj6j7zTdeoQELO3Bls5xDKr6AoSEU8gZbEZKLK9PV81hxRl/lJPJUo1qt4E7eJGapCTlH73tTIL4OZ3A== -"@gitlab/ui@^9.21.1": - version "9.21.1" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-9.21.1.tgz#76da9b86de959c2757a0c0a9389970f8d5afcc47" - integrity sha512-nJi2lFYq3WFXDNlH5vAg1Mb3Tf/PKnaVIm5W07I+hIWj/GALnwZHO3WHJuhwWIUTZOtLz7egIr4Wyh3EqBk+cg== +"@gitlab/ui@^9.23.0": + version "9.23.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-9.23.0.tgz#0ad0232c529d1f8a386c8e86159e273111a55686" + integrity sha512-1VOob5tNPB3zjLHeTuMbQBMG3q6LF36iCq6XqH5eeYzpAI42zj/WhY5T47RKrfvlkflWRSUPTarGo97pQqIKzg== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0" @@ -6872,10 +6872,10 @@ js-beautify@^1.6.12, js-beautify@^1.8.8: mkdirp "~0.5.1" nopt "~4.0.1" -js-cookie@^2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.1.3.tgz#48071625217ac9ecfab8c343a13d42ec09ff0526" - integrity sha1-SAcWJSF6yez6uMNDoT1C7An/BSY= +js-cookie@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" + integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" |