diff options
144 files changed, 1997 insertions, 1266 deletions
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index b67d586ce64..3d65b7eb71f 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -102,7 +102,6 @@ Layout/LineLength: - 'app/controllers/projects/pipelines_controller.rb' - 'app/controllers/projects/prometheus/metrics_controller.rb' - 'app/controllers/projects/raw_controller.rb' - - 'app/controllers/projects/services_controller.rb' - 'app/controllers/projects/settings/ci_cd_controller.rb' - 'app/controllers/projects/settings/operations_controller.rb' - 'app/controllers/projects/settings/repository_controller.rb' @@ -4203,7 +4202,6 @@ Layout/LineLength: - 'spec/controllers/projects/serverless/functions_controller_spec.rb' - 'spec/controllers/projects/service_desk_controller_spec.rb' - 'spec/controllers/projects/service_ping_controller_spec.rb' - - 'spec/controllers/projects/services_controller_spec.rb' - 'spec/controllers/projects/settings/ci_cd_controller_spec.rb' - 'spec/controllers/projects/settings/operations_controller_spec.rb' - 'spec/controllers/projects/settings/repository_controller_spec.rb' diff --git a/.rubocop_todo/rails/lexically_scoped_action_filter.yml b/.rubocop_todo/rails/lexically_scoped_action_filter.yml index 84b85f76c68..9edc8f7ce58 100644 --- a/.rubocop_todo/rails/lexically_scoped_action_filter.yml +++ b/.rubocop_todo/rails/lexically_scoped_action_filter.yml @@ -36,7 +36,6 @@ Rails/LexicallyScopedActionFilter: - 'app/controllers/projects/project_members_controller.rb' - 'app/controllers/projects/prometheus/alerts_controller.rb' - 'app/controllers/projects/releases_controller.rb' - - 'app/controllers/projects/service_hook_logs_controller.rb' - 'app/controllers/projects/snippets_controller.rb' - 'app/controllers/projects/tags_controller.rb' - 'app/controllers/projects/todos_controller.rb' diff --git a/.rubocop_todo/rspec/any_instance_of.yml b/.rubocop_todo/rspec/any_instance_of.yml index 32bf03b18ad..67b931140eb 100644 --- a/.rubocop_todo/rspec/any_instance_of.yml +++ b/.rubocop_todo/rspec/any_instance_of.yml @@ -152,8 +152,8 @@ RSpec/AnyInstanceOf: - spec/controllers/projects/labels_controller_spec.rb - spec/controllers/projects/merge_requests_controller_spec.rb - spec/controllers/projects/pipelines_controller_spec.rb - - spec/controllers/projects/service_hook_logs_controller_spec.rb - - spec/controllers/projects/services_controller_spec.rb + - spec/controllers/projects/settings/integration_hook_logs_controller_spec.rb + - spec/controllers/projects/settings/integrations_controller_spec.rb - spec/controllers/projects/tags_controller_spec.rb - spec/controllers/registrations/experience_levels_controller_spec.rb - spec/controllers/registrations_controller_spec.rb @@ -177,7 +177,6 @@ RSpec/AnyInstanceOf: - spec/features/projects/jobs_spec.rb - spec/features/projects/navbar_spec.rb - spec/features/projects/pages_spec.rb - - spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb - spec/features/projects/settings/service_desk_setting_spec.rb - spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb - spec/features/snippets/embedded_snippet_spec.rb diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index 8734fca753f..efe0d41ce7d 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -3855,7 +3855,6 @@ RSpec/ContextWording: - 'spec/views/projects/hooks/edit.html.haml_spec.rb' - 'spec/views/projects/hooks/index.html.haml_spec.rb' - 'spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb' - - 'spec/views/projects/services/edit.html.haml_spec.rb' - 'spec/views/projects/settings/operations/show.html.haml_spec.rb' - 'spec/views/projects/tags/index.html.haml_spec.rb' - 'spec/views/projects/tree/show.html.haml_spec.rb' diff --git a/.rubocop_todo/style/class_and_module_children.yml b/.rubocop_todo/style/class_and_module_children.yml index 9d89acfb394..5a4a58f2f6c 100644 --- a/.rubocop_todo/style/class_and_module_children.yml +++ b/.rubocop_todo/style/class_and_module_children.yml @@ -220,9 +220,7 @@ Style/ClassAndModuleChildren: - 'app/controllers/projects/runner_projects_controller.rb' - 'app/controllers/projects/runners_controller.rb' - 'app/controllers/projects/service_desk_controller.rb' - - 'app/controllers/projects/service_hook_logs_controller.rb' - 'app/controllers/projects/service_ping_controller.rb' - - 'app/controllers/projects/services_controller.rb' - 'app/controllers/projects/snippets/application_controller.rb' - 'app/controllers/projects/snippets/blobs_controller.rb' - 'app/controllers/projects/snippets_controller.rb' diff --git a/.rubocop_todo/style/empty_method.yml b/.rubocop_todo/style/empty_method.yml index 9bca01015aa..aa3972a0b22 100644 --- a/.rubocop_todo/style/empty_method.yml +++ b/.rubocop_todo/style/empty_method.yml @@ -53,7 +53,7 @@ Style/EmptyMethod: - 'app/controllers/projects/pipeline_schedules_controller.rb' - 'app/controllers/projects/product_analytics_controller.rb' - 'app/controllers/projects/runners_controller.rb' - - 'app/controllers/projects/services_controller.rb' + - 'app/controllers/projects/settings/integrations_controller.rb' - 'app/controllers/projects/settings/packages_and_registries_controller.rb' - 'app/controllers/projects/tags/releases_controller.rb' - 'app/controllers/projects/terraform_controller.rb' diff --git a/.rubocop_todo/style/format_string.yml b/.rubocop_todo/style/format_string.yml index 82ece4dd3bd..a91194bc760 100644 --- a/.rubocop_todo/style/format_string.yml +++ b/.rubocop_todo/style/format_string.yml @@ -41,7 +41,6 @@ Style/FormatString: - 'app/controllers/projects/merge_requests_controller.rb' - 'app/controllers/projects/performance_monitoring/dashboards_controller.rb' - 'app/controllers/projects/pipeline_schedules_controller.rb' - - 'app/controllers/projects/services_controller.rb' - 'app/controllers/projects/settings/ci_cd_controller.rb' - 'app/controllers/projects_controller.rb' - 'app/controllers/search_controller.rb' diff --git a/.rubocop_todo/style/if_unless_modifier.yml b/.rubocop_todo/style/if_unless_modifier.yml index 60e43a74e4d..3a90a63c940 100644 --- a/.rubocop_todo/style/if_unless_modifier.yml +++ b/.rubocop_todo/style/if_unless_modifier.yml @@ -57,7 +57,6 @@ Style/IfUnlessModifier: - 'app/controllers/projects/protected_refs_controller.rb' - 'app/controllers/projects/releases_controller.rb' - 'app/controllers/projects/runners_controller.rb' - - 'app/controllers/projects/services_controller.rb' - 'app/controllers/registrations_controller.rb' - 'app/controllers/repositories/git_http_controller.rb' - 'app/controllers/repositories/lfs_api_controller.rb' diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index 5119d5021da..60c3f3caf66 100644 --- a/app/assets/javascripts/behaviors/markdown/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -1,6 +1,5 @@ import $ from 'jquery'; import syntaxHighlight from '~/syntax_highlight'; -import initUserPopovers from '../../user_popovers'; import highlightCurrentUser from './highlight_current_user'; import { renderKroki } from './render_kroki'; import renderMath from './render_math'; @@ -22,7 +21,6 @@ $.fn.renderGFM = function renderGFM() { renderMermaid(this.find('.js-render-mermaid')); } highlightCurrentUser(this.find('.gfm-project_member').get()); - initUserPopovers(this.find('.js-user-link').get()); const issuablePopoverElements = this.find('.gfm-issue, .gfm-merge_request').get(); if (issuablePopoverElements.length) { diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue index dca89133931..8a997624a36 100644 --- a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue +++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue @@ -2,29 +2,9 @@ import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui'; import csrf from '~/lib/utils/csrf'; import { s__ } from '~/locale'; -import SplitButton from '~/vue_shared/components/split_button.vue'; - -const splitButtonActionItems = [ - { - title: s__('ClusterIntegration|Remove integration and resources'), - description: s__( - 'ClusterIntegration|Deletes all GitLab resources attached to this cluster during removal', - ), - eventName: 'remove-cluster-and-cleanup', - }, - { - title: s__('ClusterIntegration|Remove integration'), - description: s__( - 'ClusterIntegration|Removes cluster from project but keeps associated resources', - ), - eventName: 'remove-cluster', - }, -]; export default { - splitButtonActionItems, components: { - SplitButton, GlModal, GlButton, GlFormInput, @@ -79,6 +59,9 @@ export default { canCleanupResources() { return !this.hasManagementProject; }, + buttonCategory() { + return !this.hasManagementProject ? 'secondary' : 'primary'; + }, }, methods: { handleClickRemoveCluster(cleanup = false) { @@ -99,19 +82,20 @@ export default { </script> <template> - <div class="gl-display-flex gl-justify-content-end"> - <split-button + <div class="gl-display-flex"> + <gl-button v-if="canCleanupResources" - :action-items="$options.splitButtonActionItems" - menu-class="dropdown-menu-large" + data-testid="remove-integration-and-resources-button" + class="gl-mr-3" variant="danger" - @remove-cluster="handleClickRemoveCluster(false)" - @remove-cluster-and-cleanup="handleClickRemoveCluster(true)" - /> + @click="handleClickRemoveCluster(true)" + > + {{ s__('ClusterIntegration|Remove integration and resources') }} + </gl-button> <gl-button - v-else + data-testid="remove-integration-button" + :category="buttonCategory" variant="danger" - data-testid="btnRemove" @click="handleClickRemoveCluster(false)" > {{ s__('ClusterIntegration|Remove integration') }} @@ -163,13 +147,7 @@ export default { <template v-if="confirmCleanup"> <gl-button :disabled="!canSubmit" - variant="warning" - category="primary" - @click="handleSubmit" - >{{ s__('ClusterIntegration|Remove integration') }}</gl-button - > - <gl-button - :disabled="!canSubmit" + data-testid="remove-integration-and-resources-modal-button" variant="danger" category="primary" @click="handleSubmit(true)" @@ -179,6 +157,7 @@ export default { <template v-else> <gl-button :disabled="!canSubmit" + data-testid="remove-integration-modal-button" variant="danger" category="primary" @click="handleSubmit" diff --git a/app/assets/javascripts/content_editor/extensions/sourcemap.js b/app/assets/javascripts/content_editor/extensions/sourcemap.js index 94236e2e70e..61d1d983846 100644 --- a/app/assets/javascripts/content_editor/extensions/sourcemap.js +++ b/app/assets/javascripts/content_editor/extensions/sourcemap.js @@ -13,6 +13,7 @@ import Link from './link'; import ListItem from './list_item'; import OrderedList from './ordered_list'; import Paragraph from './paragraph'; +import Strike from './strike'; export default Extension.create({ addGlobalAttributes() { @@ -33,6 +34,7 @@ export default Extension.create({ ListItem.name, OrderedList.name, Paragraph.name, + Strike.name, ], attributes: { sourceMarkdown: { diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js index d665f24bba1..cd3ac6fcb1f 100644 --- a/app/assets/javascripts/content_editor/services/markdown_serializer.js +++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js @@ -65,6 +65,7 @@ import { italic, link, code, + strike, } from './serialization_helpers'; const defaultSerializerConfig = { @@ -89,12 +90,7 @@ const defaultSerializerConfig = { close: (...args) => `${defaultMarkdownSerializer.marks.code.close(...args)}$`, escape: false, }, - [Strike.name]: { - open: '~~', - close: '~~', - mixable: true, - expelEnclosingWhitespace: true, - }, + [Strike.name]: strike, ...HTMLMarks.reduce( (acc, { name }) => ({ ...acc, diff --git a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js index 899bbcff82f..4cbbfc36151 100644 --- a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js +++ b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js @@ -66,6 +66,10 @@ const factorySpecs = { title: hastNode.properties.title, }), }, + strike: { + type: 'mark', + selector: (hastNode) => ['strike', 's', 'del'].includes(hastNode.tagName), + }, }; export default () => { diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js index 089d30edec7..c11ce08de63 100644 --- a/app/assets/javascripts/content_editor/services/serialization_helpers.js +++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js @@ -364,7 +364,7 @@ export function preserveUnchanged(render) { }; } -const generateBoldTags = (open = true) => { +const generateBoldTags = (wrapTagName = openTag) => { return (_, mark) => { const type = /^(\*\*|__|<strong|<b).*/.exec(mark.attrs.sourceMarkdown)?.[1]; @@ -375,7 +375,7 @@ const generateBoldTags = (open = true) => { // eslint-disable-next-line @gitlab/require-i18n-strings case '<strong': case '<b': - return (open ? openTag : closeTag)(type.substring(1)); + return wrapTagName(type.substring(1)); default: return '**'; } @@ -384,12 +384,12 @@ const generateBoldTags = (open = true) => { export const bold = { open: generateBoldTags(), - close: generateBoldTags(false), + close: generateBoldTags(closeTag), mixable: true, expelEnclosingWhitespace: true, }; -const generateItalicTag = (open = true) => { +const generateItalicTag = (wrapTagName = openTag) => { return (_, mark) => { const type = /^(\*|_|<em|<i).*/.exec(mark.attrs.sourceMarkdown)?.[1]; @@ -400,7 +400,7 @@ const generateItalicTag = (open = true) => { // eslint-disable-next-line @gitlab/require-i18n-strings case '<em': case '<i': - return (open ? openTag : closeTag)(type.substring(1)); + return wrapTagName(type.substring(1)); default: return '_'; } @@ -409,17 +409,17 @@ const generateItalicTag = (open = true) => { export const italic = { open: generateItalicTag(), - close: generateItalicTag(false), + close: generateItalicTag(closeTag), mixable: true, expelEnclosingWhitespace: true, }; -const generateCodeTag = (open = true) => { +const generateCodeTag = (wrapTagName = openTag) => { return (_, mark) => { const type = /^(`|<code).*/.exec(mark.attrs.sourceMarkdown)?.[1]; if (type === '<code') { - return (open ? openTag : closeTag)(type.substring(1)); + return wrapTagName(type.substring(1)); } return '`'; @@ -428,7 +428,7 @@ const generateCodeTag = (open = true) => { export const code = { open: generateCodeTag(), - close: generateCodeTag(false), + close: generateCodeTag(closeTag), mixable: true, expelEnclosingWhitespace: true, }; @@ -480,3 +480,28 @@ export const link = { return `](${state.esc(canonicalSrc || href)}${title ? ` ${state.quote(title)}` : ''})`; }, }; + +const generateStrikeTag = (wrapTagName = openTag) => { + return (_, mark) => { + const type = /^(~~|<del|<strike|<s).*/.exec(mark.attrs.sourceMarkdown)?.[1]; + + switch (type) { + case '~~': + return type; + /* eslint-disable @gitlab/require-i18n-strings */ + case '<del': + case '<strike': + case '<s': + return wrapTagName(type.substring(1)); + default: + return '~~'; + } + }; +}; + +export const strike = { + open: generateStrikeTag(), + close: generateStrikeTag(closeTag), + mixable: true, + expelEnclosingWhitespace: true, +}; diff --git a/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue b/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue index 3158ae9b126..ccd22085470 100644 --- a/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue +++ b/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue @@ -22,7 +22,7 @@ export default { type: Boolean, required: true, }, - editProjectServicePath: { + editIntegrationPath: { type: String, required: true, }, @@ -79,7 +79,7 @@ export default { <gl-button variant="success" category="primary" :disabled="!formIsValid" @click="submit"> {{ saveButtonText }} </gl-button> - <gl-button class="float-right" :href="editProjectServicePath">{{ __('Cancel') }}</gl-button> + <gl-button class="float-right" :href="editIntegrationPath">{{ __('Cancel') }}</gl-button> <delete-custom-metric-modal v-if="metricPersisted" :delete-metric-url="customMetricsPath" diff --git a/app/assets/javascripts/custom_metrics/index.js b/app/assets/javascripts/custom_metrics/index.js index 4c279daf5f0..bf572217f5e 100644 --- a/app/assets/javascripts/custom_metrics/index.js +++ b/app/assets/javascripts/custom_metrics/index.js @@ -13,7 +13,7 @@ export default () => { const domEl = document.querySelector(this.$options.el); const { customMetricsPath, - editProjectServicePath, + editIntegrationPath, validateQueryPath, title, query, @@ -30,7 +30,7 @@ export default () => { props: { customMetricsPath, metricPersisted, - editProjectServicePath, + editIntegrationPath, validateQueryPath, formData: { title, diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue index 42f4ea8eb58..fc69dca73a7 100644 --- a/app/assets/javascripts/diffs/components/commit_item.vue +++ b/app/assets/javascripts/diffs/components/commit_item.vue @@ -7,8 +7,6 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import initUserPopovers from '../../user_popovers'; - /** * CommitItem * @@ -82,11 +80,6 @@ export default { return this.commit.description_html.replace(/^
/, ''); }, }, - created() { - this.$nextTick(() => { - initUserPopovers(this.$el.querySelectorAll('.js-user-link')); - }); - }, safeHtmlConfig: { ADD_TAGS: ['gl-emoji'], }, diff --git a/app/assets/javascripts/lib/gfm/index.js b/app/assets/javascripts/lib/gfm/index.js index 4e704eb69b2..537a5867096 100644 --- a/app/assets/javascripts/lib/gfm/index.js +++ b/app/assets/javascripts/lib/gfm/index.js @@ -1,10 +1,15 @@ import { unified } from 'unified'; import remarkParse from 'remark-parse'; +import remarkGfm from 'remark-gfm'; import remarkRehype from 'remark-rehype'; import rehypeRaw from 'rehype-raw'; const createParser = () => { - return unified().use(remarkParse).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeRaw); + return unified() + .use(remarkParse) + .use(remarkGfm) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeRaw); }; const compilerFactory = (renderer) => diff --git a/app/assets/javascripts/lib/utils/users_cache.js b/app/assets/javascripts/lib/utils/users_cache.js index bd000bb26fe..670acbbabd7 100644 --- a/app/assets/javascripts/lib/utils/users_cache.js +++ b/app/assets/javascripts/lib/utils/users_cache.js @@ -29,8 +29,11 @@ class UsersCache extends Cache { } return getUser(userId).then(({ data }) => { - this.internalStorage[userId] = data; - return data; + this.internalStorage[userId] = { + ...this.get(userId), + ...data, + }; + return this.internalStorage[userId]; }); // missing catch is intentional, error handling depends on use case } diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue index 14d628e455c..4f99e7a2aef 100644 --- a/app/assets/javascripts/members/components/table/members_table.vue +++ b/app/assets/javascripts/members/components/table/members_table.vue @@ -4,7 +4,6 @@ import { mapState } from 'vuex'; import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue'; import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils'; import { mergeUrlParams } from '~/lib/utils/url_utility'; -import initUserPopovers from '~/user_popovers'; import UserDate from '~/vue_shared/components/user_date.vue'; import { FIELD_KEY_ACTIONS, @@ -85,9 +84,6 @@ export default { return this.tabQueryParamValue === TAB_QUERY_PARAM_VALUES.invite; }, }, - mounted() { - initUserPopovers(this.$el.querySelectorAll('.js-user-link')); - }, methods: { hasActionButtons(member) { return ( diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 7d8d23335e0..148a73100ee 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -3,7 +3,6 @@ import { mapGetters, mapActions } from 'vuex'; import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user'; import createFlash from '~/flash'; import { __ } from '~/locale'; -import initUserPopovers from '~/user_popovers'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import OrderedLayout from '~/vue_shared/components/ordered_layout.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -169,7 +168,6 @@ export default { updated() { this.$nextTick(() => { highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member')); - initUserPopovers(this.$el.querySelectorAll('.js-user-link')); }); }, beforeDestroy() { diff --git a/app/assets/javascripts/pages/projects/services/edit/index.js b/app/assets/javascripts/pages/projects/settings/integrations/edit/index.js index 64df0d07d74..64df0d07d74 100644 --- a/app/assets/javascripts/pages/projects/services/edit/index.js +++ b/app/assets/javascripts/pages/projects/settings/integrations/edit/index.js diff --git a/app/assets/javascripts/pages/projects/settings/integrations/show/index.js b/app/assets/javascripts/pages/projects/settings/integrations/index/index.js index 53068f72d3f..53068f72d3f 100644 --- a/app/assets/javascripts/pages/projects/settings/integrations/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/integrations/index/index.js diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue index e35fccf2d7e..05cb2ebb769 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue @@ -36,12 +36,12 @@ export default { }; </script> <template> - <div data-testid="pipeline-mini-graph" class="gl-display-inline gl-vertical-align-middle gl-my-1"> + <div data-testid="pipeline-mini-graph" class="gl-display-inline gl-vertical-align-middle"> <div v-for="stage in stages" :key="stage.name" :class="stagesClass" - class="stage-container dropdown" + class="dropdown gl-display-inline-block gl-mr-2 gl-my-2 gl-vertical-align-middle stage-container" > <pipeline-stage :stage="stage" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue index 53e21d4ce8b..008b5780ecd 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue @@ -21,6 +21,9 @@ import eventHub from '../../event_hub'; import JobItem from './job_item.vue'; export default { + i18n: { + stage: __('Stage:'), + }, components: { CiIcon, GlLoadingIcon, @@ -48,20 +51,26 @@ export default { }, data() { return { + isDropdownOpen: false, isLoading: false, dropdownContent: [], + stageName: '', }; }, watch: { updateDropdown() { - if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) { + if (this.updateDropdown && this.isDropdownOpen && !this.isLoading) { this.fetchJobs(); } }, }, methods: { + onHideDropdown() { + this.isDropdownOpen = false; + }, onShowDropdown() { eventHub.$emit('clickedDropdown'); + this.isDropdownOpen = true; this.isLoading = true; this.fetchJobs(); }, @@ -70,6 +79,7 @@ export default { .get(this.stage.dropdown_path) .then(({ data }) => { this.dropdownContent = data.latest_statuses; + this.stageName = data.name; this.isLoading = false; }) .catch(() => { @@ -81,9 +91,6 @@ export default { }); }); }, - isDropdownOpen() { - return this.$el.classList.contains('show'); - }, pipelineActionRequestComplete() { // close the dropdown in MR widget this.$refs.dropdown.hide(); @@ -112,15 +119,17 @@ export default { } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */" :toggle-class="['gl-rounded-full!']" menu-class="mini-pipeline-graph-dropdown-menu" + @hide="onHideDropdown" @show="onShowDropdown" > <template #button-content> <ci-icon is-interactive css-classes="gl-rounded-full" + :is-active="isDropdownOpen" :size="24" :status="stage.status" - class="gl-align-items-center gl-display-inline-flex" + class="gl-align-items-center gl-display-inline-flex gl-z-index-1" /> </template> <gl-loading-icon v-if="isLoading" size="sm" /> @@ -129,6 +138,12 @@ export default { class="js-builds-dropdown-list scrollable-menu" data-testid="mini-pipeline-graph-dropdown-menu-list" > + <div + class="gl-align-items-center gl-border-b gl-display-flex gl-font-weight-bold gl-justify-content-center gl-pb-3" + > + <span class="gl-mr-1">{{ $options.i18n.stage }}</span> + <span data-testid="pipeline-stage-dropdown-menu-title">{{ stageName }}</span> + </div> <li v-for="job in dropdownContent" :key="job.id"> <job-item :dropdown-length="dropdownContent.length" diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js index 438ae2bc1bc..a3615eab26f 100644 --- a/app/assets/javascripts/user_popovers.js +++ b/app/assets/javascripts/user_popovers.js @@ -1,6 +1,8 @@ import Vue from 'vue'; +import { debounce } from 'lodash'; import UsersCache from './lib/utils/users_cache'; import UserPopover from './vue_shared/components/user_popover/user_popover.vue'; +import { USER_POPOVER_DELAY } from './vue_shared/components/user_popover/constants'; const removeTitle = (el) => { // Removing titles so its not showing tooltips also @@ -59,87 +61,78 @@ const populateUserInfo = (user) => { ); }; -const initializedPopovers = new Map(); -let domObservedForChanges = false; +function createPopover(el, user) { + removeTitle(el); + const preloadedUserInfo = getPreloadedUserInfo(el.dataset); -const addPopoversToModifiedTree = new MutationObserver(() => { - const userLinks = document?.querySelectorAll('.js-user-link, .gfm-project_member'); + Object.assign(user, preloadedUserInfo); - if (userLinks) { - addPopovers(userLinks); /* eslint-disable-line no-use-before-define */ + if (preloadedUserInfo.userId) { + populateUserInfo(user); } -}); + const UserPopoverComponent = Vue.extend(UserPopover); + return new UserPopoverComponent({ + propsData: { + target: el, + user, + show: true, + placement: el.dataset.placement || 'top', + }, + }); +} -function observeBody() { - if (!domObservedForChanges) { - addPopoversToModifiedTree.observe(document.body, { - subtree: true, - childList: true, - }); +function launchPopover(el, mountPopover) { + if (el.user) return; - domObservedForChanges = true; - } + const emptyUser = { + location: null, + bio: null, + workInformation: null, + status: null, + isFollowed: false, + loaded: false, + }; + el.user = emptyUser; + el.addEventListener( + 'mouseleave', + ({ target }) => { + target.removeAttribute('aria-describedby'); + }, + { once: true }, + ); + const popoverInstance = createPopover(el, emptyUser); + + const { userId } = el.dataset; + + popoverInstance.$on('follow', () => { + UsersCache.updateById(userId, { is_followed: true }); + el.user.isFollowed = true; + }); + + popoverInstance.$on('unfollow', () => { + UsersCache.updateById(userId, { is_followed: false }); + el.user.isFollowed = false; + }); + + mountPopover(popoverInstance); } -export default function addPopovers(elements = document.querySelectorAll('.js-user-link')) { - const userLinks = Array.from(elements); - const UserPopoverComponent = Vue.extend(UserPopover); +const userLinkSelector = 'a.js-user-link, a.gfm-project_member'; - observeBody(); +const getUserLinkNode = (node) => node.closest(userLinkSelector); - return userLinks - .filter(({ dataset }) => dataset.user || dataset.userId) - .map((el) => { - if (initializedPopovers.has(el)) { - return initializedPopovers.get(el); - } +const lazyLaunchPopover = debounce((mountPopover, event) => { + const userLink = getUserLinkNode(event.target); + if (userLink) { + launchPopover(userLink, mountPopover); + } +}, USER_POPOVER_DELAY); - const user = { - location: null, - bio: null, - workInformation: null, - status: null, - isFollowed: false, - loaded: false, - }; - const renderedPopover = new UserPopoverComponent({ - propsData: { - target: el, - user, - placement: el.dataset.placement || 'top', - }, - }); - - const { userId } = el.dataset; - - renderedPopover.$on('follow', () => { - UsersCache.updateById(userId, { is_followed: true }); - user.isFollowed = true; - }); - - renderedPopover.$on('unfollow', () => { - UsersCache.updateById(userId, { is_followed: false }); - user.isFollowed = false; - }); - - initializedPopovers.set(el, renderedPopover); - - renderedPopover.$mount(); - - el.addEventListener('mouseenter', ({ target }) => { - removeTitle(target); - const preloadedUserInfo = getPreloadedUserInfo(target.dataset); - - Object.assign(user, preloadedUserInfo); - - if (preloadedUserInfo.userId) { - populateUserInfo(user); - } - }); - el.addEventListener('mouseleave', ({ target }) => { - target.removeAttribute('aria-describedby'); - }); - - return renderedPopover; - }); +let hasAddedLazyPopovers = false; + +export default function addPopovers(mountPopover = (instance) => instance.$mount()) { + if (!hasAddedLazyPopovers) { + document.addEventListener('mouseover', (event) => lazyLaunchPopover(mountPopover, event)); + hasAddedLazyPopovers = true; + } } diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue index c93f620995f..afa5402c28c 100644 --- a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue +++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue @@ -18,7 +18,6 @@ import { toggleContainerClasses } from '~/lib/utils/dom_utils'; import { visitUrl, joinPaths } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; import Tracking from '~/tracking'; -import initUserPopovers from '~/user_popovers'; import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import MetricImagesTab from '~/vue_shared/components/metric_images/metric_images_tab.vue'; @@ -175,7 +174,6 @@ export default { updated() { this.$nextTick(() => { highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member')); - initUserPopovers(this.$el.querySelectorAll('.js-user-link')); }); }, methods: { diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index 9bccc49e894..c70f028a876 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -50,6 +50,11 @@ export default { required: false, default: false, }, + isActive: { + type: Boolean, + required: false, + default: false, + }, isInteractive: { type: Boolean, required: false, @@ -74,8 +79,9 @@ export default { </script> <template> <span - :class="[wrapperStyleClasses, { interactive: isInteractive }]" + :class="[wrapperStyleClasses, { interactive: isInteractive, active: isActive }]" :style="{ height: `${size}px`, width: `${size}px` }" + data-testid="ci-icon-wrapper" > <gl-icon :name="icon" :size="size" :class="cssClasses" :aria-label="status.icon" /> </span> diff --git a/app/assets/javascripts/vue_shared/components/user_popover/constants.js b/app/assets/javascripts/vue_shared/components/user_popover/constants.js new file mode 100644 index 00000000000..1d49aefd297 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/user_popover/constants.js @@ -0,0 +1 @@ +export const USER_POPOVER_DELAY = 200; diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue index ec7a7cd72ae..2eec65457c7 100644 --- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue +++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue @@ -14,12 +14,14 @@ import { glEmojiTag } from '~/emoji'; import createFlash from '~/flash'; import { followUser, unfollowUser } from '~/rest_api'; import UserAvatarImage from '../user_avatar/user_avatar_image.vue'; +import { USER_POPOVER_DELAY } from './constants'; const MAX_SKELETON_LINES = 4; export default { name: 'UserPopover', maxSkeletonLines: MAX_SKELETON_LINES, + USER_POPOVER_DELAY, components: { GlIcon, GlLink, @@ -48,6 +50,11 @@ export default { required: false, default: 'top', }, + show: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -133,8 +140,15 @@ export default { </script> <template> - <!-- 200ms delay so not every mouseover triggers Popover --> - <gl-popover :target="target" :delay="200" :placement="placement" boundary="viewport"> + <!-- delay so not every mouseover triggers Popover --> + <gl-popover + :show="show" + :target="target" + :delay="$options.USER_POPOVER_DELAY" + :placement="placement" + boundary="viewport" + triggers="hover focus manual" + > <div class="gl-p-3 gl-line-height-normal gl-display-flex" data-testid="user-popover"> <div class="gl-p-2 flex-shrink-1 gl-display-flex gl-flex-direction-column align-items-center gl-w-70p" diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index ca0240b6a65..1a6a461dad8 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -1,94 +1,55 @@ -.ci-status-icon-success, -.ci-status-icon-passed { - svg { - fill: $green-500; +@mixin icon-styles($primary-color, $svg-color) { + svg, + .gl-icon { + fill: $primary-color; } &.interactive { &:hover { - background: $green-500; + background: $primary-color; - svg { - --svg-status-bg: #{$green-100}; - box-shadow: 0 0 0 1px $green-500; + .gl-icon { + --svg-status-bg: #{$svg-color}; + box-shadow: 0 0 0 1px $primary-color; } } - } -} -.ci-status-icon-error, -.ci-status-icon-failed { - svg { - fill: $red-500; - } + &.active { + background: $primary-color; - &.interactive { - &:hover { - background: $red-500; - - svg { - --svg-status-bg: #{$red-100}; - box-shadow: 0 0 0 1px $red-500; + .gl-icon { + box-shadow: 0 0 0 1px $primary-color; } } } } +.ci-status-icon-success, +.ci-status-icon-passed { + @include icon-styles($green-500, $green-100); +} + +.ci-status-icon-error, +.ci-status-icon-failed { + @include icon-styles($red-500, $red-100); +} + .ci-status-icon-pending, .ci-status-icon-waiting-for-resource, .ci-status-icon-failed-with-warnings, .ci-status-icon-success-with-warnings { - svg { - fill: $orange-500; - } - - &.interactive { - &:hover { - background: $orange-500; - - svg { - --svg-status-bg: #{$orange-100}; - box-shadow: 0 0 0 1px $orange-500; - } - } - } + @include icon-styles($orange-500, $orange-100); } .ci-status-icon-running { - svg { - fill: $blue-500; - } - - &.interactive { - &:hover { - background: $blue-500; - - svg { - --svg-status-bg: #{$blue-100}; - box-shadow: 0 0 0 1px $blue-500; - } - } - } + @include icon-styles($blue-500, $blue-100); } .ci-status-icon-canceled, .ci-status-icon-disabled, .ci-status-icon-scheduled, .ci-status-icon-manual { - svg { - fill: $gray-900; - } - - &.interactive { - &:hover { - background: $gray-900; - - svg { - --svg-status-bg: #{$gray-100}; - box-shadow: 0 0 0 1px $gray-900; - } - } - } + @include icon-styles($gray-900, $gray-100); } .ci-status-icon-notification, @@ -96,20 +57,7 @@ .ci-status-icon-created, .ci-status-icon-skipped, .ci-status-icon-notfound { - svg { - fill: $gray-500; - } - - &.interactive { - &:hover { - background: $gray-500; - - svg { - --svg-status-bg: #{$gray-100}; - box-shadow: 0 0 0 1px $gray-500; - } - } - } + @include icon-styles($gray-500, $gray-100); } .icon-link { diff --git a/app/assets/stylesheets/page_bundles/pipelines.scss b/app/assets/stylesheets/page_bundles/pipelines.scss index a225a0f0061..4946bbbebe5 100644 --- a/app/assets/stylesheets/page_bundles/pipelines.scss +++ b/app/assets/stylesheets/page_bundles/pipelines.scss @@ -74,11 +74,8 @@ .stage-cell { .stage-container { - align-items: center; - display: inline-flex; - - + .stage-container { - margin-left: 4px; + &:last-child { + margin-right: 0; } // Hack to show a button tooltip inline @@ -94,10 +91,11 @@ &:not(:last-child) { &::after { content: ''; - width: 4px; + border-bottom: 2px solid $gray-200; position: absolute; right: -4px; - border-bottom: 2px solid $gray-200; + top: 11px; + width: 4px; } } } diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb index c4f4913a620..a4091ebdf4b 100644 --- a/app/controllers/projects/mattermosts_controller.rb +++ b/app/controllers/projects/mattermosts_controller.rb @@ -20,7 +20,7 @@ class Projects::MattermostsController < Projects::ApplicationController if result flash[:notice] = 'This service is now configured' - redirect_to edit_project_integration_path(@project, integration) + redirect_to edit_project_settings_integration_path(@project, integration) else flash[:alert] = message || 'Failed to configure service' redirect_to new_project_mattermost_path(@project) diff --git a/app/controllers/projects/prometheus/metrics_controller.rb b/app/controllers/projects/prometheus/metrics_controller.rb index f79fbd663a0..db5471ea322 100644 --- a/app/controllers/projects/prometheus/metrics_controller.rb +++ b/app/controllers/projects/prometheus/metrics_controller.rb @@ -67,7 +67,7 @@ module Projects ) if @metric.persisted? - redirect_to edit_project_integration_path(project, ::Integrations::Prometheus), + redirect_to edit_project_settings_integration_path(project, ::Integrations::Prometheus), notice: _('Metric was successfully added.') else render 'new' @@ -78,7 +78,7 @@ module Projects @metric = prometheus_metric if @metric.update(metrics_params) - redirect_to edit_project_integration_path(project, ::Integrations::Prometheus), + redirect_to edit_project_settings_integration_path(project, ::Integrations::Prometheus), notice: _('Metric was successfully updated.') else render 'edit' @@ -94,7 +94,7 @@ module Projects respond_to do |format| format.html do - redirect_to edit_project_integration_path(project, ::Integrations::Prometheus), status: :see_other + redirect_to edit_project_settings_integration_path(project, ::Integrations::Prometheus), status: :see_other end format.json do head :ok diff --git a/app/controllers/projects/service_hook_logs_controller.rb b/app/controllers/projects/service_hook_logs_controller.rb deleted file mode 100644 index 7b037c60321..00000000000 --- a/app/controllers/projects/service_hook_logs_controller.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -class Projects::ServiceHookLogsController < Projects::HookLogsController - extend Gitlab::Utils::Override - - before_action :integration, only: [:show, :retry] - - def retry - execute_hook - redirect_to edit_project_integration_path(@project, @integration) - end - - private - - def integration - @integration ||= @project.find_or_initialize_integration(params[:integration_id]) - end - - override :hook - def hook - @hook ||= integration.service_hook || not_found - end -end diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb deleted file mode 100644 index 8f83e34411b..00000000000 --- a/app/controllers/projects/services_controller.rb +++ /dev/null @@ -1,122 +0,0 @@ -# frozen_string_literal: true - -class Projects::ServicesController < Projects::ApplicationController - include Integrations::Params - include InternalRedirect - - # Authorize - before_action :authorize_admin_project! - before_action :ensure_service_enabled - before_action :integration - before_action :default_integration, only: [:edit, :update] - before_action :web_hook_logs, only: [:edit, :update] - - respond_to :html - - layout "project_settings" - - feature_category :integrations - urgency :low, [:test] - - def edit - end - - def update - attributes = integration_params[:integration] - - if use_inherited_settings?(attributes) - integration.inherit_from_id = default_integration.id - - if saved = integration.save(context: :manual_change) - BulkUpdateIntegrationService.new(default_integration, [integration]).execute - end - else - attributes[:inherit_from_id] = nil - integration.attributes = attributes - saved = integration.save(context: :manual_change) - end - - respond_to do |format| - format.html do - if saved - redirect_to redirect_path, notice: success_message - else - render 'edit' - end - end - - format.json do - status = saved ? :ok : :unprocessable_entity - - render json: serialize_as_json, status: status - end - end - end - - def test - if integration.testable? - render json: service_test_response, status: :ok - else - render json: {}, status: :not_found - end - end - - private - - def redirect_path - safe_redirect_path(params[:redirect_to]).presence || edit_project_integration_path(project, integration) - end - - def service_test_response - unless integration.update(integration_params[:integration]) - return { error: true, message: _('Validations failed.'), service_response: integration.errors.full_messages.join(','), test_failed: false } - end - - result = ::Integrations::Test::ProjectService.new(integration, current_user, params[:event]).execute - - unless result[:success] - return { error: true, message: s_('Integrations|Connection failed. Please check your settings.'), service_response: result[:message].to_s, test_failed: true } - end - - result[:data].presence || {} - rescue *Gitlab::HTTP::HTTP_ERRORS => e - { error: true, message: s_('Integrations|Connection failed. Please check your settings.'), service_response: e.message, test_failed: true } - end - - def success_message - if integration.active? - s_('Integrations|%{integration} settings saved and active.') % { integration: integration.title } - else - s_('Integrations|%{integration} settings saved, but not active.') % { integration: integration.title } - end - end - - def integration - @integration ||= project.find_or_initialize_integration(params[:id]) - end - alias_method :service, :integration - - def default_integration - @default_integration ||= Integration.default_integration(integration.type, project) - end - - def web_hook_logs - return unless integration.service_hook.present? - - @web_hook_logs ||= integration.service_hook.web_hook_logs.recent.page(params[:page]) - end - - def ensure_service_enabled - render_404 unless service - end - - def serialize_as_json - integration - .as_json(only: integration.json_fields) - .merge(errors: integration.errors.as_json) - end - - def use_inherited_settings?(attributes) - default_integration && attributes[:inherit_from_id] == default_integration.id.to_s - end -end diff --git a/app/controllers/projects/settings/integration_hook_logs_controller.rb b/app/controllers/projects/settings/integration_hook_logs_controller.rb new file mode 100644 index 00000000000..b3b5a292d42 --- /dev/null +++ b/app/controllers/projects/settings/integration_hook_logs_controller.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Projects + module Settings + class IntegrationHookLogsController < Projects::HookLogsController + extend Gitlab::Utils::Override + + before_action :integration, only: [:show, :retry] + + def retry + execute_hook + redirect_to edit_project_settings_integration_path(@project, @integration) + end + + private + + def integration + @integration ||= @project.find_or_initialize_integration(params[:integration_id]) + end + + override :hook + def hook + @hook ||= integration.service_hook || not_found + end + end + end +end diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb index c9d92d1aee9..3365da65de8 100644 --- a/app/controllers/projects/settings/integrations_controller.rb +++ b/app/controllers/projects/settings/integrations_controller.rb @@ -3,14 +3,142 @@ module Projects module Settings class IntegrationsController < Projects::ApplicationController + include ::Integrations::Params + include ::InternalRedirect + before_action :authorize_admin_project! + before_action :ensure_integration_enabled, only: [:edit, :update, :test] + before_action :integration, only: [:edit, :update, :test] + before_action :default_integration, only: [:edit, :update] + before_action :web_hook_logs, only: [:edit, :update] + + respond_to :html + layout "project_settings" feature_category :integrations + urgency :low, [:test] - def show + def index @integrations = @project.find_or_initialize_integrations end + + def edit + end + + def update + attributes = integration_params[:integration] + + if use_inherited_settings?(attributes) + integration.inherit_from_id = default_integration.id + + if saved = integration.save(context: :manual_change) + BulkUpdateIntegrationService.new(default_integration, [integration]).execute + end + else + attributes[:inherit_from_id] = nil + integration.attributes = attributes + saved = integration.save(context: :manual_change) + end + + respond_to do |format| + format.html do + if saved + redirect_to redirect_path, notice: success_message + else + render 'edit' + end + end + + format.json do + status = saved ? :ok : :unprocessable_entity + + render json: serialize_as_json, status: status + end + end + end + + def test + if integration.testable? + render json: integration_test_response, status: :ok + else + render json: {}, status: :not_found + end + end + + private + + def redirect_path + safe_redirect_path(params[:redirect_to]).presence || + edit_project_settings_integration_path(project, integration) + end + + def integration_test_response + unless integration.update(integration_params[:integration]) + return { + error: true, + message: _('Validations failed.'), + service_response: integration.errors.full_messages.join(','), + test_failed: false + } + end + + result = ::Integrations::Test::ProjectService.new(integration, current_user, params[:event]).execute + + unless result[:success] + return { + error: true, + message: s_('Integrations|Connection failed. Please check your settings.'), + service_response: result[:message].to_s, + test_failed: true + } + end + + result[:data].presence || {} + rescue *Gitlab::HTTP::HTTP_ERRORS => e + { + error: true, + message: s_('Integrations|Connection failed. Please check your settings.'), + service_response: e.message, + test_failed: true + } + end + + def success_message + if integration.active? + format(s_('Integrations|%{integration} settings saved and active.'), integration: integration.title) + else + format(s_('Integrations|%{integration} settings saved, but not active.'), integration: integration.title) + end + end + + def integration + @integration ||= project.find_or_initialize_integration(params[:id]) + end + + def default_integration + @default_integration ||= Integration.default_integration(integration.type, project) + end + + def web_hook_logs + return unless integration.service_hook.present? + + @web_hook_logs ||= integration.service_hook.web_hook_logs.recent.page(params[:page]) + end + + def ensure_integration_enabled + render_404 unless integration + end + + def serialize_as_json + integration + .as_json(only: integration.json_fields) + .merge(errors: integration.errors.as_json) + end + + def use_inherited_settings?(attributes) + default_integration && attributes[:inherit_from_id] == default_integration.id.to_s + end end end end diff --git a/app/helpers/custom_metrics_helper.rb b/app/helpers/custom_metrics_helper.rb index 5442120008a..8a9d94bd2a1 100644 --- a/app/helpers/custom_metrics_helper.rb +++ b/app/helpers/custom_metrics_helper.rb @@ -5,7 +5,7 @@ module CustomMetricsHelper { 'custom-metrics-path' => url_for([project, metric]), 'metric-persisted' => metric.persisted?.to_s, - 'edit-project-service-path' => edit_project_integration_path(project, ::Integrations::Prometheus), + 'edit-integration-path' => edit_project_settings_integration_path(project, ::Integrations::Prometheus), 'validate-query-path' => validate_query_project_prometheus_metrics_path(project), 'title' => metric.title.to_s, 'query' => metric.query.to_s, diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index 3b60bda8605..119eaa88bd1 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -59,7 +59,7 @@ module EnvironmentsHelper return {} unless project { - 'settings_path' => edit_project_integration_path(project, 'prometheus'), + 'settings_path' => edit_project_settings_integration_path(project, 'prometheus'), 'clusters_path' => project_clusters_path(project), 'dashboards_endpoint' => project_performance_monitoring_dashboards_path(project, format: :json), 'default_branch' => project.default_branch, diff --git a/app/helpers/integrations_helper.rb b/app/helpers/integrations_helper.rb index 862938ac961..44e0307fd81 100644 --- a/app/helpers/integrations_helper.rb +++ b/app/helpers/integrations_helper.rb @@ -58,7 +58,7 @@ module IntegrationsHelper def scoped_integration_path(integration, project: nil, group: nil) if project.present? - project_integration_path(project, integration) + project_settings_integration_path(project, integration) elsif group.present? group_settings_integration_path(group, integration) else @@ -68,7 +68,7 @@ module IntegrationsHelper def scoped_edit_integration_path(integration, project: nil, group: nil) if project.present? - edit_project_integration_path(project, integration) + edit_project_settings_integration_path(project, integration) elsif group.present? edit_group_settings_integration_path(group, integration) else @@ -82,7 +82,7 @@ module IntegrationsHelper def scoped_test_integration_path(integration, project: nil, group: nil) if project.present? - test_project_integration_path(project, integration) + test_project_settings_integration_path(project, integration) elsif group.present? test_group_settings_integration_path(group, integration) else diff --git a/app/presenters/service_hook_presenter.rb b/app/presenters/service_hook_presenter.rb index f2a06358918..b34679c85cf 100644 --- a/app/presenters/service_hook_presenter.rb +++ b/app/presenters/service_hook_presenter.rb @@ -4,10 +4,10 @@ class ServiceHookPresenter < Gitlab::View::Presenter::Delegated presents ::ServiceHook, as: :service_hook def logs_details_path(log) - project_integration_hook_log_path(integration.project, integration, log) + project_settings_integration_hook_log_path(integration.project, integration, log) end def logs_retry_path(log) - retry_project_integration_hook_log_path(integration.project, integration, log) + retry_project_settings_integration_hook_log_path(integration.project, integration, log) end end diff --git a/app/views/clusters/clusters/_advanced_settings.html.haml b/app/views/clusters/clusters/_advanced_settings.html.haml index 59c8fe04b09..8eba398fd13 100644 --- a/app/views/clusters/clusters/_advanced_settings.html.haml +++ b/app/views/clusters/clusters/_advanced_settings.html.haml @@ -35,7 +35,7 @@ = s_("ClusterIntegration|This is necessary if your integration has become out of sync. The cache is repopulated during the next CI job that requires namespace and service accounts.") - else = s_("ClusterIntegration|This is necessary to clear existing environment-namespace associations from clusters previously managed by GitLab.") - = link_to(s_('ClusterIntegration|Clear cluster cache'), clusterable.clear_cluster_cache_path(@cluster), method: :delete, class: 'btn gl-button btn-info') + = link_to(s_('ClusterIntegration|Clear cluster cache'), clusterable.clear_cluster_cache_path(@cluster), method: :delete, class: 'btn gl-button btn-confirm') .sub-section.form-group %h4.text-danger diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml index 2c46bf1e074..0b45869bdf9 100644 --- a/app/views/profiles/chat_names/_chat_name.html.haml +++ b/app/views/profiles/chat_names/_chat_name.html.haml @@ -10,7 +10,7 @@ %td %strong - if can?(current_user, :admin_project, project) - = link_to integration.title, edit_project_integration_path(project, integration) + = link_to integration.title, edit_project_settings_integration_path(project, integration) - else = integration.title %td diff --git a/app/views/projects/import/jira/show.html.haml b/app/views/projects/import/jira/show.html.haml index 1feae7baa02..2605ebc544f 100644 --- a/app/views/projects/import/jira/show.html.haml +++ b/app/views/projects/import/jira/show.html.haml @@ -1,6 +1,6 @@ .js-jira-import-root{ data: { project_path: @project.full_path, issues_path: project_issues_path(@project), - jira_integration_path: edit_project_integration_path(@project, :jira), + jira_integration_path: edit_project_settings_integration_path(@project, :jira), is_jira_configured: @project.jira_integration&.configured?.to_s, in_progress_illustration: image_path('illustrations/export-import.svg'), project_id: @project.id, diff --git a/app/views/projects/mattermosts/_no_teams.html.haml b/app/views/projects/mattermosts/_no_teams.html.haml index 1f008496a34..5886c0565b1 100644 --- a/app/views/projects/mattermosts/_no_teams.html.haml +++ b/app/views/projects/mattermosts/_no_teams.html.haml @@ -9,4 +9,4 @@ and try again. %hr .clearfix - = link_to 'Go back', edit_project_integration_path(@project, @integration), class: 'gl-button btn btn-lg float-right' + = link_to 'Go back', edit_project_settings_integration_path(@project, @integration), class: 'gl-button btn btn-lg float-right' diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml index d52d980c364..e6f7833d16a 100644 --- a/app/views/projects/mattermosts/_team_selection.html.haml +++ b/app/views/projects/mattermosts/_team_selection.html.haml @@ -42,5 +42,5 @@ %hr .clearfix .float-right - = link_to _('Cancel'), edit_project_integration_path(@project, @integration), class: 'gl-button btn btn-lg' + = link_to _('Cancel'), edit_project_settings_integration_path(@project, @integration), class: 'gl-button btn btn-lg' = f.submit 'Install', class: 'gl-button btn btn-success btn-lg' diff --git a/app/views/projects/prometheus/metrics/edit.html.haml b/app/views/projects/prometheus/metrics/edit.html.haml index 146bf6b6853..212d625d292 100644 --- a/app/views/projects/prometheus/metrics/edit.html.haml +++ b/app/views/projects/prometheus/metrics/edit.html.haml @@ -1,6 +1,6 @@ - add_to_breadcrumbs _("Settings"), edit_project_path(@project) - add_to_breadcrumbs _("Integrations"), project_settings_integrations_path(@project) -- add_to_breadcrumbs "Prometheus", edit_project_integration_path(@project, ::Integrations::Prometheus) +- add_to_breadcrumbs "Prometheus", edit_project_settings_integration_path(@project, ::Integrations::Prometheus) - breadcrumb_title s_('Metrics|Edit metric') - page_title @metric.title, s_('Metrics|Edit metric') = render 'form', project: @project, metric: @metric diff --git a/app/views/projects/prometheus/metrics/new.html.haml b/app/views/projects/prometheus/metrics/new.html.haml index ad8463d1804..c04e5f385d9 100644 --- a/app/views/projects/prometheus/metrics/new.html.haml +++ b/app/views/projects/prometheus/metrics/new.html.haml @@ -1,6 +1,6 @@ - add_to_breadcrumbs _("Settings"), edit_project_path(@project) - add_to_breadcrumbs _("Integrations"), project_settings_integrations_path(@project) -- add_to_breadcrumbs "Prometheus", edit_project_integration_path(@project, ::Integrations::Prometheus) +- add_to_breadcrumbs "Prometheus", edit_project_settings_integration_path(@project, ::Integrations::Prometheus) - breadcrumb_title s_('Metrics|New metric') - page_title s_('Metrics|New metric') = render 'form', project: @project, metric: @metric diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/settings/integrations/_form.html.haml index 9d74f99bb19..9d74f99bb19 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/settings/integrations/_form.html.haml diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/settings/integrations/edit.html.haml index a250daafdbb..a250daafdbb 100644 --- a/app/views/projects/services/edit.html.haml +++ b/app/views/projects/settings/integrations/edit.html.haml diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/index.html.haml index 84635941436..84635941436 100644 --- a/app/views/projects/settings/integrations/show.html.haml +++ b/app/views/projects/settings/integrations/index.html.haml diff --git a/app/views/shared/empty_states/_wikis.html.haml b/app/views/shared/empty_states/_wikis.html.haml index c3120774826..552b100d5dd 100644 --- a/app/views/shared/empty_states/_wikis.html.haml +++ b/app/views/shared/empty_states/_wikis.html.haml @@ -13,7 +13,7 @@ = create_link - if show_enable_confluence_integration?(@wiki.container) = link_to s_('WikiEmpty|Enable the Confluence Wiki integration'), - edit_project_integration_path(@project, :confluence), + edit_project_settings_integration_path(@project, :confluence), class: 'btn gl-button', title: s_('WikiEmpty|Enable the Confluence Wiki integration') - elsif @project && can?(current_user, :read_issue, @project) diff --git a/config/application.rb b/config/application.rb index 8a40aaa8bd6..1dd6fc04b16 100644 --- a/config/application.rb +++ b/config/application.rb @@ -99,7 +99,6 @@ module Gitlab #{config.root}/app/models/badges #{config.root}/app/models/hooks #{config.root}/app/models/members - #{config.root}/app/models/project_services #{config.root}/app/graphql/resolvers/concerns #{config.root}/app/graphql/mutations/concerns #{config.root}/app/graphql/types/concerns]) diff --git a/config/metrics/counts_all/20210216181301_jira_imports_total_imported_issues_count.yml b/config/metrics/counts_all/20210216181301_jira_imports_total_imported_issues_count.yml index 0b68f3d5698..312bbc1dff6 100644 --- a/config/metrics/counts_all/20210216181301_jira_imports_total_imported_issues_count.yml +++ b/config/metrics/counts_all/20210216181301_jira_imports_total_imported_issues_count.yml @@ -1,6 +1,7 @@ --- data_category: optional key_path: counts.jira_imports_total_imported_issues_count +instrumentation_class: JiraImportsTotalImportedIssuesCountMetric description: Count of total issues imported via the Jira Importer product_section: dev product_stage: ecosystem diff --git a/config/routes/project.rb b/config/routes/project.rb index 6b2a9700686..c7f0c726371 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -130,7 +130,17 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end - resource :integrations, only: [:show] + resources :integrations, constraints: { id: %r{[^/]+} }, only: [:index, :edit, :update] do + member do + put :test + end + + resources :hook_logs, only: [:show], controller: :integration_hook_logs do + member do + post :retry + end + end + end resource :repository, only: [:show], controller: :repository do # TODO: Removed this "create_deploy_token" route after change was made in app/helpers/ci_variables_helper.rb:14 @@ -209,12 +219,14 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end - resources :integrations, controller: :services, constraints: { id: %r{[^/]+} }, only: [:edit, :update] do + # Legacy routes for `/-/integrations` which are now in `/-/settings/integrations`. + # Can be removed in 15.2, see https://gitlab.com/gitlab-org/gitlab/-/issues/334846 + resources :integrations, controller: 'settings/integrations', constraints: { id: %r{[^/]+} }, only: [:edit, :update] do member do put :test end - resources :hook_logs, only: [:show], controller: :service_hook_logs do + resources :hook_logs, only: [:show], controller: 'settings/integration_hook_logs' do member do post :retry end diff --git a/data/removals/15_0/15-0-runner_api_new_stale_status_breaking_change.yml b/data/removals/15_0/15-0-runner_api_new_stale_status_breaking_change.yml index 905bc3befce..182c492f17b 100644 --- a/data/removals/15_0/15-0-runner_api_new_stale_status_breaking_change.yml +++ b/data/removals/15_0/15-0-runner_api_new_stale_status_breaking_change.yml @@ -5,12 +5,12 @@ removal_date: "2022-05-22" breaking_change: true body: | # Do not modify this line, instead modify the lines below. - A breaking change was made to the Runner [API](https://docs.gitlab.com/ee/api/runners.html#runners-api) endpoints - in 15.0. + The Runner [API](https://docs.gitlab.com/ee/api/runners.html#runners-api) endpoints have changed in 15.0. - Instead of the GitLab Runner API endpoints returning `offline` and `not_connected` for runners that have not - contacted the GitLab instance in the past three months, the API endpoints now return the `stale` value, - which was introduced in 14.6. + If a runner has not contacted the GitLab instance in more than three months, the API returns `stale` instead of `offline` or `not_connected`. + The `stale` status was introduced in 14.6. + + The `not_connected` status is no longer valid. It was replaced with `never_contacted`. Available statuses are `online`, `offline`, `stale`, and `never_contacted`. stage: Verify tiers: [Core, Premium, Ultimate] issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347303 diff --git a/db/post_migrate/20220524202158_drop_index_on_deployments_on_created_at_cluster_id_and_project_id.rb b/db/post_migrate/20220524202158_drop_index_on_deployments_on_created_at_cluster_id_and_project_id.rb new file mode 100644 index 00000000000..91b1e5f8ce3 --- /dev/null +++ b/db/post_migrate/20220524202158_drop_index_on_deployments_on_created_at_cluster_id_and_project_id.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class DropIndexOnDeploymentsOnCreatedAtClusterIdAndProjectId < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + INDEX_NAME = 'tp_index_created_at_cluster_id_project_id_on_deployments' + + def up + remove_concurrent_index_by_name :deployments, INDEX_NAME + end + + def down + # no-op + # + # There's no need to re-add this index as it's purpose was temporary, served only + # for a specific CR query which is now closed, and should not be re-opened. + end +end diff --git a/db/schema_migrations/20220524202158 b/db/schema_migrations/20220524202158 new file mode 100644 index 00000000000..3df81f0c5c2 --- /dev/null +++ b/db/schema_migrations/20220524202158 @@ -0,0 +1 @@ +21f37004086f6d7f606791dd7caeb7c5ca701b009689932eb9ea4eb653e3e0dc
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 3c725c9f44c..61bb3a60ef3 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -29930,8 +29930,6 @@ CREATE INDEX tmp_index_projects_on_id_and_runners_token ON projects USING btree CREATE INDEX tmp_index_projects_on_id_and_runners_token_encrypted ON projects USING btree (id, runners_token_encrypted) WHERE (runners_token_encrypted IS NOT NULL); -CREATE INDEX tp_index_created_at_cluster_id_project_id_on_deployments ON deployments USING btree (created_at, cluster_id, project_id) WHERE ((cluster_id IS NOT NULL) AND (created_at > '2022-04-03 00:00:00'::timestamp without time zone)); - CREATE UNIQUE INDEX uniq_pkgs_deb_grp_architectures_on_distribution_id_and_name ON packages_debian_group_architectures USING btree (distribution_id, name); CREATE UNIQUE INDEX uniq_pkgs_deb_grp_components_on_distribution_id_and_name ON packages_debian_group_components USING btree (distribution_id, name); diff --git a/doc/ci/jobs/index.md b/doc/ci/jobs/index.md index 42827dc2d48..9406ae81486 100644 --- a/doc/ci/jobs/index.md +++ b/doc/ci/jobs/index.md @@ -266,7 +266,7 @@ In this example: When running manual jobs you can supply additional job specific variables. You can do this from the job page of the manual job you want to run with -additional variables. To access this page, click on the **name** of the manual job in +additional variables. To access this page, select the **name** of the manual job in the pipeline view, *not* the play (**{play}**) button. This is useful when you want to alter the execution of a job that uses @@ -337,7 +337,7 @@ In the example above: - `my_first_section`: The name given to the section. - `\r\e[0K`: Prevents the section markers from displaying in the rendered (colored) job log, but they are displayed in the raw job log. To see them, in the top right - of the job log, click **{doc-text}** (**Show complete raw**). + of the job log, select **{doc-text}** (**Show complete raw**). - `\r`: carriage return. - `\e[0K`: clear line ANSI escape code. diff --git a/doc/ci/quick_start/index.md b/doc/ci/quick_start/index.md index e35a042a320..0369824c92e 100644 --- a/doc/ci/quick_start/index.md +++ b/doc/ci/quick_start/index.md @@ -79,7 +79,7 @@ To create a `.gitlab-ci.yml` file: 1. On the left sidebar, select **Project information > Details**. 1. Above the file list, select the branch you want to commit to, - click the plus icon, then select **New file**: + select the plus icon, then select **New file**: ![New file](img/new_file_v13_6.png) @@ -115,7 +115,7 @@ To create a `.gitlab-ci.yml` file: [predefined variables](../variables/predefined_variables.md) that populate when the job runs. -1. Click **Commit changes**. +1. Select **Commit changes**. The pipeline starts when the commit is committed. @@ -172,11 +172,11 @@ To view your pipeline: ![Three stages](img/three_stages_v13_6.png) -- To view a visual representation of your pipeline, click the pipeline ID. +- To view a visual representation of your pipeline, select the pipeline ID. ![Pipeline graph](img/pipeline_graph_v13_6.png) -- To view details of a job, click the job name, for example, `deploy-prod`. +- To view details of a job, select the job name, for example, `deploy-prod`. ![Job details](img/job_details_v13_6.png) diff --git a/doc/development/cached_queries.md b/doc/development/cached_queries.md index 8c69981b27a..bfa518ff7bf 100644 --- a/doc/development/cached_queries.md +++ b/doc/development/cached_queries.md @@ -93,7 +93,7 @@ below the query. You can see multiple duplicate cached queries in this modal win ![Performance Bar Cached Queries Modal](img/performance_bar_cached_queries.png) -Click **...** to expand the actual stack trace: +Select **...** to expand the actual stack trace: ```ruby [ diff --git a/doc/development/database_review.md b/doc/development/database_review.md index fd0e2e17623..ceb8c9e5906 100644 --- a/doc/development/database_review.md +++ b/doc/development/database_review.md @@ -25,7 +25,7 @@ A database review is required for: generally up to the author of a merge request to decide whether or not complex queries are being introduced and if they require a database review. -- Changes in Service Data metrics that use `count`, `distinct_count` and `estimate_batch_distinct_count`. +- Changes in Service Data metrics that use `count`, `distinct_count`, `estimate_batch_distinct_count` and `sum`. These metrics could have complex queries over large tables. See the [Product Intelligence Guide](https://about.gitlab.com/handbook/product/product-intelligence-guide/) for implementation details. diff --git a/doc/development/documentation/versions.md b/doc/development/documentation/versions.md index b084bd2fec4..81b9108364e 100644 --- a/doc/development/documentation/versions.md +++ b/doc/development/documentation/versions.md @@ -25,7 +25,7 @@ To view versions that are not available on `docs.gitlab.com`: ## Documenting version-specific features When a feature is added or updated, you can include its version information -either as a **Version history** bullet or as an inline text reference. +either as a **Version history** list item or as an inline text reference. You do not need to add version information on the pages in the `/development` directory. diff --git a/doc/development/fe_guide/frontend_faq.md b/doc/development/fe_guide/frontend_faq.md index c39d2373705..9c892cf89e2 100644 --- a/doc/development/fe_guide/frontend_faq.md +++ b/doc/development/fe_guide/frontend_faq.md @@ -187,8 +187,8 @@ Be sure to add these polyfills to `app/assets/javascripts/commons/polyfills.js`. To see what polyfills are being used: 1. Navigate to your merge request. -1. In the secondary menu below the title of the merge request, click **Pipelines**, then - click the pipeline you want to view, to display the jobs in that pipeline. +1. In the secondary menu below the title of the merge request, select **Pipelines**, then + select the pipeline you want to view, to display the jobs in that pipeline. 1. Select the [`compile-production-assets`](https://gitlab.com/gitlab-org/gitlab/-/jobs/641770154) job. 1. In the right-hand sidebar, scroll to **Job Artifacts**, and select **Browse**. 1. Select the **webpack-report** folder to open it, and select **index.html**. diff --git a/doc/development/integrations/jenkins.md b/doc/development/integrations/jenkins.md index 8a3f64f0a0d..f430fc380b1 100644 --- a/doc/development/integrations/jenkins.md +++ b/doc/development/integrations/jenkins.md @@ -36,8 +36,8 @@ GitLab does not allow requests to localhost or the local network by default. Whe Jenkins uses the GitLab API and needs an access token. 1. Sign in to your GitLab instance. -1. Click on your profile picture, then click **Settings**. -1. Click **Access Tokens**. +1. Select your profile picture, then select **Settings**. +1. Select **Access Tokens**. 1. Create a new Access Token with the **API** scope enabled. Note the value of the token. ## Configure Jenkins diff --git a/doc/development/integrations/jira_connect.md b/doc/development/integrations/jira_connect.md index 26ef67c937c..32e6634c70e 100644 --- a/doc/development/integrations/jira_connect.md +++ b/doc/development/integrations/jira_connect.md @@ -37,13 +37,13 @@ To install the app in Jira: Marketplace: 1. In Jira, navigate to **Jira settings > Apps > Manage apps**. - 1. Scroll to the bottom of the **Manage apps** page and click **Settings**. - 1. Select **Enable development mode** and click **Apply**. + 1. Scroll to the bottom of the **Manage apps** page and select **Settings**. + 1. Select **Enable development mode** and select **Apply**. 1. Install the app: 1. In Jira, navigate to **Jira settings > Apps > Manage apps**. - 1. Click **Upload app**. + 1. Select **Upload app**. 1. In the **From this URL** field, provide a link to the app descriptor. The host and port must point to your GitLab instance. For example: @@ -52,10 +52,10 @@ To install the app in Jira: https://xxxx.gitpod.io/-/jira_connect/app_descriptor.json ``` - 1. Click **Upload**. + 1. Select **Upload**. If the install was successful, you should see the **GitLab.com for Jira Cloud** app under **Manage apps**. - You can also click **Getting Started** to open the configuration page rendered from your GitLab instance. + You can also select **Getting Started** to open the configuration page rendered from your GitLab instance. _Note that any changes to the app descriptor requires you to uninstall then reinstall the app._ diff --git a/doc/development/service_ping/implement.md b/doc/development/service_ping/implement.md index ad7ea3b9e07..049ca11b3a1 100644 --- a/doc/development/service_ping/implement.md +++ b/doc/development/service_ping/implement.md @@ -102,29 +102,13 @@ Examples using `usage_data.rb` have been [deprecated](usage_data.md). We recomme #### Sum batch operation -There is no support for `sum` for database metrics. - Sum the values of a given ActiveRecord_Relation on given column and handles errors. Handles the `ActiveRecord::StatementInvalid` error Method: ```ruby -sum(relation, column, batch_size: nil, start: nil, finish: nil) -``` - -Arguments: - -- `relation`: the ActiveRecord_Relation to perform the operation -- `column`: the column to sum on -- `batch_size`: if none set it uses default value 1000 from `Gitlab::Database::BatchCounter` -- `start`: custom start of the batch counting to avoid complex min calculations -- `end`: custom end of the batch counting to avoid complex min calculations - -Examples: - -```ruby -sum(JiraImportState.finished, :imported_issues_count) +add_metric('JiraImportsTotalImportedIssuesCountMetric') ``` #### Grouping and batch operations diff --git a/doc/development/service_ping/metrics_instrumentation.md b/doc/development/service_ping/metrics_instrumentation.md index e718d972fba..a503c0704e5 100644 --- a/doc/development/service_ping/metrics_instrumentation.md +++ b/doc/development/service_ping/metrics_instrumentation.md @@ -35,7 +35,7 @@ We have built a domain-specific language (DSL) to define the metrics instrumenta ## Database metrics -- `operation`: Operations for the given `relation`, one of `count`, `distinct_count`. +- `operation`: Operations for the given `relation`, one of `count`, `distinct_count`, `sum`. - `relation`: `ActiveRecord::Relation` for the objects we want to perform the `operation`. - `start`: Specifies the start value of the batch counting, by default is `relation.minimum(:id)`. - `finish`: Specifies the end value of the batch counting, by default is `relation.maximum(:id)`. @@ -104,6 +104,26 @@ module Gitlab end ``` +### Sum Example + +```ruby +# frozen_string_literal: true + +module Gitlab + module Usage + module Metrics + module Instrumentations + class JiraImportsTotalImportedIssuesCountMetric < DatabaseMetric + operation :sum, column: :imported_issues_count + + relation { JiraImportState.finished } + end + end + end + end +end +``` + ## Redis metrics [Example of a merge request that adds a `Redis` metric](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66582). @@ -228,7 +248,7 @@ end There is support for: -- `count`, `distinct_count`, `estimate_batch_distinct_count` for [database metrics](#database-metrics). +- `count`, `distinct_count`, `estimate_batch_distinct_count`, `sum` for [database metrics](#database-metrics). - [Redis metrics](#redis-metrics). - [Redis HLL metrics](#redis-hyperloglog-metrics). - [Generic metrics](#generic-metrics), which are metrics based on settings or configurations. @@ -246,7 +266,7 @@ To create a stub instrumentation for a Service Ping metric, you can use a dedica The generator takes the class name as an argument and the following options: - `--type=TYPE` Required. Indicates the metric type. It must be one of: `database`, `generic`, `redis`. -- `--operation` Required for `database` type. It must be one of: `count`, `distinct_count`, `estimate_batch_distinct_count`. +- `--operation` Required for `database` type. It must be one of: `count`, `distinct_count`, `estimate_batch_distinct_count`, `sum`. - `--ee` Indicates if the metric is for EE. ```shell diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md index 1351d5b5b85..d7b8d8a7be2 100644 --- a/doc/development/testing_guide/review_apps.md +++ b/doc/development/testing_guide/review_apps.md @@ -94,8 +94,8 @@ the GitLab handbook information for the [shared 1Password account](https://about 1. Make sure you [have access to the cluster](#get-access-to-the-gcp-review-apps-cluster) and the `container.pods.exec` permission first. 1. [Filter Workloads by your Review App slug](https://console.cloud.google.com/kubernetes/workload?project=gitlab-review-apps). For example, `review-qa-raise-e-12chm0`. 1. Find and open the `toolbox` Deployment. For example, `review-qa-raise-e-12chm0-toolbox`. -1. Click on the Pod in the "Managed pods" section. For example, `review-qa-raise-e-12chm0-toolbox-d5455cc8-2lsvz`. -1. Click on the `KUBECTL` dropdown, then `Exec` -> `toolbox`. +1. Select the Pod in the "Managed pods" section. For example, `review-qa-raise-e-12chm0-toolbox-d5455cc8-2lsvz`. +1. Select the `KUBECTL` dropdown, then `Exec` -> `toolbox`. 1. Replace `-c toolbox -- ls` with `-it -- gitlab-rails console` from the default command or - Run `kubectl exec --namespace review-qa-raise-e-12chm0 review-qa-raise-e-12chm0-toolbox-d5455cc8-2lsvz -it -- gitlab-rails console` and @@ -107,8 +107,8 @@ the GitLab handbook information for the [shared 1Password account](https://about 1. Make sure you [have access to the cluster](#get-access-to-the-gcp-review-apps-cluster) and the `container.pods.getLogs` permission first. 1. [Filter Workloads by your Review App slug](https://console.cloud.google.com/kubernetes/workload?project=gitlab-review-apps). For example, `review-qa-raise-e-12chm0`. 1. Find and open the `migrations` Deployment. For example, `review-qa-raise-e-12chm0-migrations.1`. -1. Click on the Pod in the "Managed pods" section. For example, `review-qa-raise-e-12chm0-migrations.1-nqwtx`. -1. Click on the `Container logs` link. +1. Select the Pod in the "Managed pods" section. For example, `review-qa-raise-e-12chm0-migrations.1-nqwtx`. +1. Select `Container logs`. Alternatively, you could use the [Logs Explorer](https://console.cloud.google.com/logs/query;query=?project=gitlab-review-apps) which provides more utility to search logs. An example query for a pod name is as follows: diff --git a/doc/development/windows.md b/doc/development/windows.md index fb095b68939..3eed9c057ab 100644 --- a/doc/development/windows.md +++ b/doc/development/windows.md @@ -65,21 +65,21 @@ Build a Google Cloud image with the above shared runners repository by doing the 1. In a web browser, go to the [Google Cloud Platform console](https://console.cloud.google.com/compute/images). 1. Filter images by the name you used when creating image, `windows` is likely all you need to filter by. -1. Click the image's name. -1. Click the **CREATE INSTANCE** link. +1. Select the image's name. +1. Select **CREATE INSTANCE**. 1. Important: Change name to what you'd like as you can't change it later. 1. Optional: Change Region to be closest to you as well as any other option you'd like. -1. Click **Create** at the bottom of the page. -1. Click the name of your newly created VM Instance (optionally you can filter to find it). -1. Click **Set Windows password**. +1. Select **Create** at the bottom of the page. +1. Select the name of your newly created VM Instance (optionally you can filter to find it). +1. Select **Set Windows password**. 1. Optional: Set a username or use default. -1. Click **Next**. +1. Select **Next**. 1. Copy and save the password as it is not shown again. -1. Click **RDP** down arrow. -1. Click **Download the RDP file**. +1. Select **RDP** down arrow. +1. Select **Download the RDP file**. 1. Open the downloaded RDP file with the Windows remote desktop app (<https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/remote-desktop-clients>). -1. Click **Continue** to accept the certificate. -1. Enter the password and click **Next**. +1. Select **Continue** to accept the certificate. +1. Enter the password and select **Next**. You should now be connected into a Windows machine with a command prompt. diff --git a/doc/install/aws/manual_install_aws.md b/doc/install/aws/manual_install_aws.md index eb43cf7c723..0ec210856a0 100644 --- a/doc/install/aws/manual_install_aws.md +++ b/doc/install/aws/manual_install_aws.md @@ -265,7 +265,7 @@ On the EC2 dashboard, look for Load Balancer in the left navigation bar: 1. Select **Configure Security Settings** and set the following: 1. Select an SSL/TLS certificate from ACM or upload a certificate to IAM. 1. Under **Select a Cipher**, pick a predefined security policy from the dropdown. You can see a breakdown of [Predefined SSL Security Policies for Classic Load Balancers](https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-policy-table.html) in the AWS docs. Check the GitLab codebase for a list of [supported SSL ciphers and protocols](https://gitlab.com/gitlab-org/gitlab/-/blob/9ee7ad433269b37251e0dd5b5e00a0f00d8126b4/lib/support/nginx/gitlab-ssl#L97-99). -1. Click **Configure Health Check** and set up a health check for your EC2 instances. +1. Select **Configure Health Check** and set up a health check for your EC2 instances. 1. For **Ping Protocol**, select HTTP. 1. For **Ping Port**, enter 80. 1. For **Ping Path** - we recommend that you [use the Readiness check endpoint](../../administration/load_balancer.md#readiness-check). You'll need to add [the VPC IP Address Range (CIDR)](https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-groups.html#elb-vpc-nacl) to the [IP Allowlist](../../administration/monitoring/ip_whitelist.md) for the [Health Check endpoints](../../user/admin_area/monitoring/health_check.md) @@ -282,7 +282,7 @@ you might have. On the Route 53 dashboard, select **Hosted zones** in the left navigation bar: -1. Select an existing hosted zone or, if you do not already have one for your domain, click **Create Hosted Zone**, enter your domain name, and click **Create**. +1. Select an existing hosted zone or, if you do not already have one for your domain, select **Create Hosted Zone**, enter your domain name, and select **Create**. 1. Select **Create Record Set** and provide the following values: 1. **Name:** Use the domain name (the default value) or enter a subdomain. 1. **Type:** Select **A - IPv4 address**. @@ -377,7 +377,7 @@ persistence and is used to store session data, temporary cache information, and 1. Navigate to the ElastiCache dashboard from your AWS console. 1. Go to **Subnet Groups** in the left menu, and create a new subnet group (we'll name ours `gitlab-redis-group`). - Make sure to select our VPC and its [private subnets](#subnets). + Make sure to select our VPC and its [private subnets](#subnets). 1. Select **Create** when ready. ![ElastiCache subnet](img/ec_subnet.png) diff --git a/doc/integration/arkose.md b/doc/integration/arkose.md index 2db36f88834..0135785dc11 100644 --- a/doc/integration/arkose.md +++ b/doc/integration/arkose.md @@ -56,7 +56,7 @@ To enable Arkose Protect: ``` 1. Optional. To prevent high risk sessions from signing, enable the `arkose_labs_prevent_login` feature flag. Run the following command in the Rails console: - + ```ruby Feature.enable(:arkose_labs_prevent_login) ``` @@ -73,5 +73,5 @@ test suite doesn't fail. This bypass is done in the `UserVerificationService` cl ## Feedback Job -To help Arkose improve their protection service, we created a daily background job to send them the list of blocked users by us. +To help Arkose improve their protection service, we created a daily background job to send them the list of blocked users by us. This job is performed by the `Arkose::BlockedUsersReportWorker` class. diff --git a/doc/update/removals.md b/doc/update/removals.md index b63cc345c03..5d0a31996d5 100644 --- a/doc/update/removals.md +++ b/doc/update/removals.md @@ -38,12 +38,12 @@ as a [breaking change](https://docs.gitlab.com/ee/development/contributing/#brea Before updating GitLab, review the details carefully to determine if you need to make any changes to your code, settings, or workflow. -A breaking change was made to the Runner [API](https://docs.gitlab.com/ee/api/runners.html#runners-api) endpoints -in 15.0. +The Runner [API](https://docs.gitlab.com/ee/api/runners.html#runners-api) endpoints have changed in 15.0. -Instead of the GitLab Runner API endpoints returning `offline` and `not_connected` for runners that have not -contacted the GitLab instance in the past three months, the API endpoints now return the `stale` value, -which was introduced in 14.6. +If a runner has not contacted the GitLab instance in more than three months, the API returns `stale` instead of `offline` or `not_connected`. +The `stale` status was introduced in 14.6. + +The `not_connected` status is no longer valid. It was replaced with `never_contacted`. Available statuses are `online`, `offline`, `stale`, and `never_contacted`. ### Audit events for repository push events diff --git a/doc/user/packages/package_registry/index.md b/doc/user/packages/package_registry/index.md index 86f6ee225c9..d68c7218ca5 100644 --- a/doc/user/packages/package_registry/index.md +++ b/doc/user/packages/package_registry/index.md @@ -110,7 +110,7 @@ You can also remove the Package Registry for your project specifically: 1. In your project, go to **Settings > General**. 1. Expand the **Visibility, project features, permissions** section and disable the **Packages** feature. -1. Click **Save changes**. +1. Select **Save changes**. The **Packages & Registries > Package Registry** entry is removed from the sidebar. diff --git a/doc/user/project/integrations/webhook_events.md b/doc/user/project/integrations/webhook_events.md index 2bf6b4bbe01..465a51def7c 100644 --- a/doc/user/project/integrations/webhook_events.md +++ b/doc/user/project/integrations/webhook_events.md @@ -1126,6 +1126,15 @@ Payload example: "email": "user@gitlab.com" } }, + "source_pipeline":{ + "project":{ + "id": 41, + "web_url": "https://gitlab.example.com/gitlab-org/upstream-project", + "path_with_namespace": "gitlab-org/upstream-project", + }, + "pipeline_id": 30, + "job_id": 3401 + }, "builds":[ { "id": 380, diff --git a/jest.config.base.js b/jest.config.base.js index fb2f1084bea..b8df8684f6d 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -111,6 +111,7 @@ module.exports = (path, options = {}) => { 'remark-.*', 'hast*', 'unist.*', + 'markdown-table', 'mdast-util-.*', 'micromark.*', 'vfile.*', diff --git a/lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb b/lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb new file mode 100644 index 00000000000..2d5231b0541 --- /dev/null +++ b/lib/bulk_imports/projects/pipelines/design_bundle_pipeline.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module BulkImports + module Projects + module Pipelines + class DesignBundlePipeline + include Pipeline + + file_extraction_pipeline! + relation_name BulkImports::FileTransfer::ProjectConfig::DESIGN_BUNDLE_RELATION + + def extract(_context) + download_service.execute + decompression_service.execute + extraction_service.execute + + bundle_path = File.join(tmpdir, "#{self.class.relation}.bundle") + + BulkImports::Pipeline::ExtractedData.new(data: bundle_path) + end + + def load(_context, bundle_path) + Gitlab::Utils.check_path_traversal!(bundle_path) + Gitlab::Utils.check_allowed_absolute_path!(bundle_path, [Dir.tmpdir]) + + return unless portable.lfs_enabled? + return unless File.exist?(bundle_path) + return if File.directory?(bundle_path) + return if File.lstat(bundle_path).symlink? + + portable.design_repository.create_from_bundle(bundle_path) + end + + def after_run(_) + FileUtils.remove_entry(tmpdir) if Dir.exist?(tmpdir) + end + + private + + def download_service + BulkImports::FileDownloadService.new( + configuration: context.configuration, + relative_url: context.entity.relation_download_url_path(self.class.relation), + tmpdir: tmpdir, + filename: targz_filename + ) + end + + def decompression_service + BulkImports::FileDecompressionService.new(tmpdir: tmpdir, filename: targz_filename) + end + + def extraction_service + BulkImports::ArchiveExtractionService.new(tmpdir: tmpdir, filename: tar_filename) + end + + def tar_filename + "#{self.class.relation}.tar" + end + + def targz_filename + "#{tar_filename}.gz" + end + + def tmpdir + @tmpdir ||= Dir.mktmpdir('bulk_imports') + end + end + end + end +end diff --git a/lib/bulk_imports/projects/stage.rb b/lib/bulk_imports/projects/stage.rb index 229df9c410d..82b1d9b427c 100644 --- a/lib/bulk_imports/projects/stage.rb +++ b/lib/bulk_imports/projects/stage.rb @@ -95,6 +95,10 @@ module BulkImports pipeline: BulkImports::Common::Pipelines::LfsObjectsPipeline, stage: 5 }, + design: { + pipeline: BulkImports::Projects::Pipelines::DesignBundlePipeline, + stage: 5 + }, auto_devops: { pipeline: BulkImports::Projects::Pipelines::AutoDevopsPipeline, stage: 5 diff --git a/lib/generators/gitlab/usage_metric_generator.rb b/lib/generators/gitlab/usage_metric_generator.rb index 0656dfbc312..0eef6ceb539 100644 --- a/lib/generators/gitlab/usage_metric_generator.rb +++ b/lib/generators/gitlab/usage_metric_generator.rb @@ -15,7 +15,7 @@ module Gitlab redis: 'Redis' }.freeze - ALLOWED_OPERATIONS = %w(count distinct_count estimate_batch_distinct_count).freeze + ALLOWED_OPERATIONS = %w(count distinct_count estimate_batch_distinct_count sum).freeze source_root File.expand_path('usage_metric/templates', __dir__) diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb index 385f1e57705..c13bb1d6a9a 100644 --- a/lib/gitlab/data_builder/pipeline.rb +++ b/lib/gitlab/data_builder/pipeline.rb @@ -12,7 +12,7 @@ module Gitlab def initialize(pipeline) @pipeline = pipeline - super( + attrs = { object_kind: 'pipeline', object_attributes: hook_attrs(pipeline), merge_request: pipeline.merge_request && merge_request_attrs(pipeline.merge_request), @@ -23,7 +23,13 @@ module Gitlab preload_builds(pipeline, :latest_builds) pipeline.latest_builds.map(&method(:build_hook_attrs)) end - ) + } + + if pipeline.source_pipeline.present? + attrs[:source_pipeline] = source_pipeline_attrs(pipeline.source_pipeline) + end + + super(attrs) end def with_retried_builds @@ -72,6 +78,20 @@ module Gitlab } end + def source_pipeline_attrs(source_pipeline) + project = source_pipeline.source_project + + { + project: { + id: project.id, + web_url: project.web_url, + path_with_namespace: project.full_path + }, + job_id: source_pipeline.source_job_id, + pipeline_id: source_pipeline.source_pipeline_id + } + end + def merge_request_attrs(merge_request) { id: merge_request.id, diff --git a/lib/gitlab/database/migration.rb b/lib/gitlab/database/migration.rb index 038af570dbc..ab8b6988c3d 100644 --- a/lib/gitlab/database/migration.rb +++ b/lib/gitlab/database/migration.rb @@ -37,6 +37,7 @@ module Gitlab class V1_0 < ActiveRecord::Migration[6.1] # rubocop:disable Naming/ClassAndModuleCamelCase include LockRetriesConcern include Gitlab::Database::MigrationHelpers::V2 + include Gitlab::Database::MigrationHelpers::AnnounceDatabase # When running migrations, the `db:migrate` switches connection of # ActiveRecord::Base depending where the migration runs. diff --git a/lib/gitlab/database/migration_helpers/announce_database.rb b/lib/gitlab/database/migration_helpers/announce_database.rb new file mode 100644 index 00000000000..28710aab717 --- /dev/null +++ b/lib/gitlab/database/migration_helpers/announce_database.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Database + module MigrationHelpers + module AnnounceDatabase + extend ActiveSupport::Concern + + def write(text = "") + if text.present? # announce/say + super("#{db_config_name}: #{text}") + else + super(text) + end + end + + def db_config_name + @db_config_name ||= Gitlab::Database.db_config_name(connection) + end + end + end + end +end diff --git a/lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb b/lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb index d8d07fcaf2d..b8d1d21a0d2 100644 --- a/lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb +++ b/lib/gitlab/database/migration_helpers/restrict_gitlab_schema.rb @@ -21,7 +21,7 @@ module Gitlab end end - def migrate(direction) + def exec_migration(conn, direction) if unmatched_schemas.any? migration_skipped return @@ -37,8 +37,9 @@ module Gitlab private def migration_skipped - say "Current migration is skipped since it modifies "\ - "'#{self.class.allowed_gitlab_schemas}' which is outside of '#{allowed_schemas_for_connection}'" + say "The migration is skipped since it modifies the schemas: #{self.class.allowed_gitlab_schemas}." + say "This database can only apply migrations in one of the following schemas: " \ + "#{allowed_schemas_for_connection}." end def validator_class diff --git a/lib/gitlab/usage/metrics/instrumentations/database_metric.rb b/lib/gitlab/usage/metrics/instrumentations/database_metric.rb index a000b4509c6..48ff78cfd0f 100644 --- a/lib/gitlab/usage/metrics/instrumentations/database_metric.rb +++ b/lib/gitlab/usage/metrics/instrumentations/database_metric.rb @@ -18,7 +18,7 @@ module Gitlab UnimplementedOperationError = Class.new(StandardError) # rubocop:disable UsageData/InstrumentationSuperclass class << self - IMPLEMENTED_OPERATIONS = %i(count distinct_count estimate_batch_distinct_count).freeze + IMPLEMENTED_OPERATIONS = %i(count distinct_count estimate_batch_distinct_count sum).freeze private_constant :IMPLEMENTED_OPERATIONS diff --git a/lib/gitlab/usage/metrics/instrumentations/jira_imports_total_imported_issues_count_metric.rb b/lib/gitlab/usage/metrics/instrumentations/jira_imports_total_imported_issues_count_metric.rb new file mode 100644 index 00000000000..6ca57864b8a --- /dev/null +++ b/lib/gitlab/usage/metrics/instrumentations/jira_imports_total_imported_issues_count_metric.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module Usage + module Metrics + module Instrumentations + class JiraImportsTotalImportedIssuesCountMetric < DatabaseMetric + operation :sum, column: :imported_issues_count + + relation { JiraImportState.finished } + end + end + end + end +end diff --git a/lib/gitlab/usage/metrics/query.rb b/lib/gitlab/usage/metrics/query.rb index 851aa7a50e8..91ffca4a92d 100644 --- a/lib/gitlab/usage/metrics/query.rb +++ b/lib/gitlab/usage/metrics/query.rb @@ -25,19 +25,19 @@ module Gitlab private def count(relation, column = nil) - raw_sql(relation, column) + raw_count_sql(relation, column) end def distinct_count(relation, column = nil) - raw_sql(relation, column, true) + raw_count_sql(relation, column, true) end def sum(relation, column) - relation.select(relation.all.table[column].sum).to_sql + raw_sum_sql(relation, column) end def estimate_batch_distinct_count(relation, column = nil) - raw_sql(relation, column, true) + raw_count_sql(relation, column, true) end # rubocop: disable CodeReuse/ActiveRecord @@ -62,15 +62,23 @@ module Gitlab # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord - def raw_sql(relation, column, distinct = false) + def raw_count_sql(relation, column, distinct = false) column ||= relation.primary_key - node = node_to_count(relation, column) + node = node_to_operate(relation, column) relation.unscope(:order).select(node.count(distinct)).to_sql end # rubocop: enable CodeReuse/ActiveRecord - def node_to_count(relation, column) + # rubocop: disable CodeReuse/ActiveRecord + def raw_sum_sql(relation, column) + node = node_to_operate(relation, column) + + relation.unscope(:order).select(node.sum).to_sql + end + # rubocop: enable CodeReuse/ActiveRecord + + def node_to_operate(relation, column) if join_relation?(relation) && joined_column?(column) table_name, column_name = column.split(".") Arel::Table.new(table_name)[column_name] diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index c64a6508d34..208d7c327c3 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -407,7 +407,7 @@ module Gitlab { jira_imports_total_imported_count: count(finished_jira_imports), jira_imports_projects_count: distinct_count(finished_jira_imports, :project_id), - jira_imports_total_imported_issues_count: sum(JiraImportState.finished, :imported_issues_count) + jira_imports_total_imported_issues_count: add_metric('JiraImportsTotalImportedIssuesCountMetric') } # rubocop: enable UsageData/LargeTable end diff --git a/lib/sidebars/projects/menus/settings_menu.rb b/lib/sidebars/projects/menus/settings_menu.rb index 2b5b3cdbb22..9dada3a3f73 100644 --- a/lib/sidebars/projects/menus/settings_menu.rb +++ b/lib/sidebars/projects/menus/settings_menu.rb @@ -54,7 +54,7 @@ module Sidebars ::Sidebars::MenuItem.new( title: _('Integrations'), link: project_settings_integrations_path(context.project), - active_routes: { path: %w[integrations#show services#edit] }, + active_routes: { path: %w[integrations#index integrations#edit] }, item_id: :integrations ) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 99567eb0ba4..4e5914bdf0c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8624,9 +8624,6 @@ msgstr "" msgid "ClusterIntegration|Create a Kubernetes cluster" msgstr "" -msgid "ClusterIntegration|Deletes all GitLab resources attached to this cluster during removal" -msgstr "" - msgid "ClusterIntegration|Deploy each environment to its own namespace. Otherwise, environments within a project share a project-wide namespace. Note that anyone who can trigger a deployment of a namespace can read its secrets. If modified, existing environments will use their current namespaces until the cluster cache is cleared." msgstr "" @@ -8786,9 +8783,6 @@ msgstr "" msgid "ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster." msgstr "" -msgid "ClusterIntegration|Removes cluster from project but keeps associated resources" -msgstr "" - msgid "ClusterIntegration|Save changes" msgstr "" @@ -36031,6 +36025,9 @@ msgstr "" msgid "Stage" msgstr "" +msgid "Stage:" +msgstr "" + msgid "Standard" msgstr "" @@ -41563,6 +41560,9 @@ msgstr "" msgid "ValueStreamAnalytics|Total number of deploys to production." msgstr "" +msgid "ValueStreamAnalytics|Value Stream" +msgstr "" + msgid "ValueStreamEvent|Items in stage" msgstr "" diff --git a/package.json b/package.json index 0be616dec1c..6b523241109 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,7 @@ "raphael": "^2.2.7", "raw-loader": "^4.0.2", "rehype-raw": "^6.1.1", + "remark-gfm": "^3.0.1", "remark-parse": "^10.0.1", "remark-rehype": "^10.1.0", "scrollparent": "^2.0.1", diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb index 596cd5c1a20..19a04654114 100644 --- a/spec/controllers/projects/mattermosts_controller_spec.rb +++ b/spec/controllers/projects/mattermosts_controller_spec.rb @@ -62,7 +62,7 @@ RSpec.describe Projects::MattermostsController do subject integration = project.integrations.last - expect(subject).to redirect_to(edit_project_integration_path(project, integration)) + expect(subject).to redirect_to(edit_project_settings_integration_path(project, integration)) end end end diff --git a/spec/controllers/projects/prometheus/metrics_controller_spec.rb b/spec/controllers/projects/prometheus/metrics_controller_spec.rb index 7dfa283195e..cd195b95100 100644 --- a/spec/controllers/projects/prometheus/metrics_controller_spec.rb +++ b/spec/controllers/projects/prometheus/metrics_controller_spec.rb @@ -141,7 +141,7 @@ RSpec.describe Projects::Prometheus::MetricsController do expect(flash[:notice]).to include('Metric was successfully added.') - expect(response).to redirect_to(edit_project_integration_path(project, ::Integrations::Prometheus)) + expect(response).to redirect_to(edit_project_settings_integration_path(project, ::Integrations::Prometheus)) end end @@ -168,7 +168,7 @@ RSpec.describe Projects::Prometheus::MetricsController do expect(metric.reload.title).to eq('new_title') expect(flash[:notice]).to include('Metric was successfully updated.') - expect(response).to redirect_to(edit_project_integration_path(project, ::Integrations::Prometheus)) + expect(response).to redirect_to(edit_project_settings_integration_path(project, ::Integrations::Prometheus)) end end end @@ -180,7 +180,7 @@ RSpec.describe Projects::Prometheus::MetricsController do it 'destroys the metric' do delete :destroy, params: project_params(id: metric.id) - expect(response).to redirect_to(edit_project_integration_path(project, ::Integrations::Prometheus)) + expect(response).to redirect_to(edit_project_settings_integration_path(project, ::Integrations::Prometheus)) expect(PrometheusMetric.find_by(id: metric.id)).to be_nil end end diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb deleted file mode 100644 index 6802ebeb63e..00000000000 --- a/spec/controllers/projects/services_controller_spec.rb +++ /dev/null @@ -1,356 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Projects::ServicesController do - include JiraServiceHelper - include AfterNextHelpers - - let_it_be(:project) { create(:project, :repository) } - let_it_be(:user) { create(:user) } - let_it_be(:jira_integration) { create(:jira_integration, project: project) } - - let(:integration) { jira_integration } - let(:integration_params) { { username: 'username', password: 'password', url: 'http://example.com' } } - - before do - sign_in(user) - project.add_maintainer(user) - end - - it_behaves_like Integrations::Actions do - let(:integration_attributes) { { project: project } } - - let(:routing_params) do - { - namespace_id: project.namespace, - project_id: project, - id: integration.to_param - } - end - end - - describe '#test' do - context 'when the integration is not testable' do - it 'renders 404' do - allow_any_instance_of(Integration).to receive(:testable?).and_return(false) - - put :test, params: project_params - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'when validations fail' do - let(:integration_params) { { active: 'true', url: '' } } - - it 'returns error messages in JSON response' do - put :test, params: project_params(service: integration_params) - - expect(json_response['message']).to eq 'Validations failed.' - expect(json_response['service_response']).to include "Url can't be blank" - expect(response).to be_successful - end - end - - context 'when successful' do - context 'with empty project' do - let_it_be(:project) { create(:project) } - - context 'with chat notification integration' do - let_it_be(:teams_integration) { project.create_microsoft_teams_integration(webhook: 'http://webhook.com') } - - let(:integration) { teams_integration } - - it 'returns success' do - allow_next(::MicrosoftTeams::Notifier).to receive(:ping).and_return(true) - - put :test, params: project_params - - expect(response).to be_successful - end - end - - it 'returns success' do - stub_jira_integration_test - - expect(Gitlab::HTTP).to receive(:get).with('/rest/api/2/serverInfo', any_args).and_call_original - - put :test, params: project_params(service: integration_params) - - expect(response).to be_successful - end - end - - it 'returns success' do - stub_jira_integration_test - - expect(Gitlab::HTTP).to receive(:get).with('/rest/api/2/serverInfo', any_args).and_call_original - - put :test, params: project_params(service: integration_params) - - expect(response).to be_successful - end - - context 'when service is configured for the first time' do - let(:integration_params) do - { - 'active' => '1', - 'push_events' => '1', - 'token' => 'token', - 'project_url' => 'https://buildkite.com/organization/pipeline' - } - end - - before do - allow_any_instance_of(ServiceHook).to receive(:execute).and_return(true) - end - - it 'persist the object' do - do_put - - expect(response).to be_successful - expect(json_response).to be_empty - expect(Integrations::Buildkite.first).to be_present - end - - it 'creates the ServiceHook object' do - do_put - - expect(response).to be_successful - expect(json_response).to be_empty - expect(Integrations::Buildkite.first.service_hook).to be_present - end - - def do_put - put :test, params: project_params(id: 'buildkite', - service: integration_params) - end - end - end - - context 'when unsuccessful' do - it 'returns an error response when the integration test fails' do - stub_request(:get, 'http://example.com/rest/api/2/serverInfo') - .to_return(status: 404) - - put :test, params: project_params(service: integration_params) - - expect(response).to be_successful - expect(json_response).to eq( - 'error' => true, - 'message' => 'Connection failed. Please check your settings.', - 'service_response' => '', - 'test_failed' => true - ) - end - - context 'with the Slack integration' do - let_it_be(:integration) { build(:integrations_slack) } - - it 'returns an error response when the URL is blocked' do - put :test, params: project_params(service: { webhook: 'http://127.0.0.1' }) - - expect(response).to be_successful - expect(json_response).to eq( - 'error' => true, - 'message' => 'Connection failed. Please check your settings.', - 'service_response' => "URL 'http://127.0.0.1' is blocked: Requests to localhost are not allowed", - 'test_failed' => true - ) - end - - it 'returns an error response when a network exception is raised' do - expect_next(Integrations::Slack).to receive(:test).and_raise(Errno::ECONNREFUSED) - - put :test, params: project_params - - expect(response).to be_successful - expect(json_response).to eq( - 'error' => true, - 'message' => 'Connection failed. Please check your settings.', - 'service_response' => 'Connection refused', - 'test_failed' => true - ) - end - end - end - end - - describe 'PUT #update' do - describe 'as HTML' do - let(:integration_params) { { active: true } } - let(:params) { project_params(service: integration_params) } - - let(:message) { 'Jira settings saved and active.' } - let(:redirect_url) { edit_project_integration_path(project, integration) } - - before do - stub_jira_integration_test - - put :update, params: params - end - - shared_examples 'integration update' do - it 'redirects to the correct url with a flash message' do - expect(response).to redirect_to(redirect_url) - expect(flash[:notice]).to eq(message) - end - end - - context 'when param `active` is set to true' do - let(:params) { project_params(service: integration_params, redirect_to: redirect) } - - context 'when redirect_to param is present' do - let(:redirect) { '/redirect_here' } - let(:redirect_url) { redirect } - - it_behaves_like 'integration update' - end - - context 'when redirect_to is an external domain' do - let(:redirect) { 'http://examle.com' } - - it_behaves_like 'integration update' - end - - context 'when redirect_to param is an empty string' do - let(:redirect) { '' } - - it_behaves_like 'integration update' - end - end - - context 'when param `active` is set to false' do - let(:integration_params) { { active: false } } - let(:message) { 'Jira settings saved, but not active.' } - - it_behaves_like 'integration update' - end - - context 'when param `inherit_from_id` is set to empty string' do - let(:integration_params) { { inherit_from_id: '' } } - - it 'sets inherit_from_id to nil' do - expect(integration.reload.inherit_from_id).to eq(nil) - end - end - - context 'when param `inherit_from_id` is set to an instance integration' do - let(:instance_integration) { create(:jira_integration, :instance, url: 'http://instance.com', password: 'instance') } - let(:integration_params) { { inherit_from_id: instance_integration.id, url: 'http://custom.com', password: 'custom' } } - - it 'ignores submitted params and inherits instance settings' do - expect(integration.reload).to have_attributes( - inherit_from_id: instance_integration.id, - url: instance_integration.url, - password: instance_integration.password - ) - end - end - - context 'when param `inherit_from_id` is set to a group integration' do - let_it_be(:group) { create(:group) } - let_it_be(:project) { create(:project, group: group) } - let_it_be(:jira_integration) { create(:jira_integration, project: project) } - - let(:group_integration) { create(:jira_integration, :group, group: group, url: 'http://group.com', password: 'group') } - let(:integration_params) { { inherit_from_id: group_integration.id, url: 'http://custom.com', password: 'custom' } } - - it 'ignores submitted params and inherits group settings' do - expect(integration.reload).to have_attributes( - inherit_from_id: group_integration.id, - url: group_integration.url, - password: group_integration.password - ) - end - end - - context 'when param `inherit_from_id` is set to an unrelated group' do - let_it_be(:group) { create(:group) } - - let(:group_integration) { create(:jira_integration, :group, group: group, url: 'http://group.com', password: 'group') } - let(:integration_params) { { inherit_from_id: group_integration.id, url: 'http://custom.com', password: 'custom' } } - - it 'ignores the param and saves the submitted settings' do - expect(integration.reload).to have_attributes( - inherit_from_id: nil, - url: 'http://custom.com', - password: 'custom' - ) - end - end - end - - describe 'as JSON' do - before do - stub_jira_integration_test - put :update, params: project_params(service: integration_params, format: :json) - end - - context 'when update succeeds' do - let(:integration_params) { { url: 'http://example.com', password: 'password' } } - - it 'returns success response' do - expect(response).to be_successful - expect(json_response).to include( - 'active' => true, - 'errors' => {} - ) - end - end - - context 'when update fails with missing password' do - let(:integration_params) { { url: 'http://example.com' } } - - it 'returns JSON response errors' do - expect(response).not_to be_successful - expect(json_response).to include( - 'active' => true, - 'errors' => { - 'password' => ["can't be blank"] - } - ) - end - end - - context 'when update fails with invalid URL' do - let(:integration_params) { { url: '', password: 'password' } } - - it 'returns JSON response with errors' do - expect(response).to have_gitlab_http_status(:unprocessable_entity) - expect(json_response).to include( - 'active' => true, - 'errors' => { 'url' => ['must be a valid URL', "can't be blank"] } - ) - end - end - end - end - - describe 'GET #edit' do - context 'with Jira service' do - let(:integration_param) { 'jira' } - - before do - get :edit, params: project_params(id: integration_param) - end - - context 'with approved services' do - it 'renders edit page' do - expect(response).to be_successful - end - end - end - end - - private - - def project_params(opts = {}) - opts.reverse_merge( - namespace_id: project.namespace, - project_id: project, - id: integration.to_param - ) - end -end diff --git a/spec/controllers/projects/service_hook_logs_controller_spec.rb b/spec/controllers/projects/settings/integration_hook_logs_controller_spec.rb index be78668aa88..8261461e8aa 100644 --- a/spec/controllers/projects/service_hook_logs_controller_spec.rb +++ b/spec/controllers/projects/settings/integration_hook_logs_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::ServiceHookLogsController do +RSpec.describe Projects::Settings::IntegrationHookLogsController do let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:integration) { create(:drone_ci_integration, project: project) } @@ -44,7 +44,8 @@ RSpec.describe Projects::ServiceHookLogsController do it 'executes the hook and redirects to the service form' do expect_any_instance_of(ServiceHook).to receive(:execute) expect_any_instance_of(described_class).to receive(:set_hook_execution_notice) - expect(subject).to redirect_to(edit_project_integration_path(project, integration)) + + expect(subject).to redirect_to(edit_project_settings_integration_path(project, integration)) end it 'renders a 404 if the hook does not exist' do diff --git a/spec/controllers/projects/settings/integrations_controller_spec.rb b/spec/controllers/projects/settings/integrations_controller_spec.rb index 0652786c787..16f32ee83b4 100644 --- a/spec/controllers/projects/settings/integrations_controller_spec.rb +++ b/spec/controllers/projects/settings/integrations_controller_spec.rb @@ -3,20 +3,388 @@ require 'spec_helper' RSpec.describe Projects::Settings::IntegrationsController do - let(:project) { create(:project, :public) } - let(:user) { create(:user) } + include JiraServiceHelper + include AfterNextHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + let_it_be(:jira_integration) { create(:jira_integration, project: project) } + + let(:integration) { jira_integration } + let(:integration_params) { { username: 'username', password: 'password', url: 'http://example.com' } } before do - project.add_maintainer(user) sign_in(user) + project.add_maintainer(user) + end + + it_behaves_like Integrations::Actions do + let(:integration_attributes) { { project: project } } + + let(:routing_params) do + { + namespace_id: project.namespace, + project_id: project, + id: integration.to_param + } + end end - describe 'GET show' do - it 'renders show with 200 status code' do - get :show, params: { namespace_id: project.namespace, project_id: project } + describe 'GET index' do + it 'renders index with 200 status code' do + get :index, params: { namespace_id: project.namespace, project_id: project } expect(response).to have_gitlab_http_status(:ok) - expect(response).to render_template(:show) + expect(response).to render_template(:index) + end + end + + describe '#test' do + context 'when the integration is not testable' do + it 'renders 404' do + allow_any_instance_of(Integration).to receive(:testable?).and_return(false) + + put :test, params: project_params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when validations fail' do + let(:integration_params) { { active: 'true', url: '' } } + + it 'returns error messages in JSON response' do + put :test, params: project_params(service: integration_params) + + expect(json_response['message']).to eq 'Validations failed.' + expect(json_response['service_response']).to include "Url can't be blank" + expect(response).to be_successful + end + end + + context 'when successful' do + context 'with empty project' do + let_it_be(:project) { create(:project) } + + context 'with chat notification integration' do + let_it_be(:teams_integration) { project.create_microsoft_teams_integration(webhook: 'http://webhook.com') } + + let(:integration) { teams_integration } + + it 'returns success' do + allow_next(::MicrosoftTeams::Notifier).to receive(:ping).and_return(true) + + put :test, params: project_params + + expect(response).to be_successful + end + end + + it 'returns success' do + stub_jira_integration_test + + expect(Gitlab::HTTP).to receive(:get).with('/rest/api/2/serverInfo', any_args).and_call_original + + put :test, params: project_params(service: integration_params) + + expect(response).to be_successful + end + end + + it 'returns success' do + stub_jira_integration_test + + expect(Gitlab::HTTP).to receive(:get).with('/rest/api/2/serverInfo', any_args).and_call_original + + put :test, params: project_params(service: integration_params) + + expect(response).to be_successful + end + + context 'when service is configured for the first time' do + let(:integration_params) do + { + 'active' => '1', + 'push_events' => '1', + 'token' => 'token', + 'project_url' => 'https://buildkite.com/organization/pipeline' + } + end + + before do + allow_next(ServiceHook).to receive(:execute).and_return(true) + end + + it 'persist the object' do + do_put + + expect(response).to be_successful + expect(json_response).to be_empty + expect(Integrations::Buildkite.first).to be_present + end + + it 'creates the ServiceHook object' do + do_put + + expect(response).to be_successful + expect(json_response).to be_empty + expect(Integrations::Buildkite.first.service_hook).to be_present + end + + def do_put + put :test, params: project_params(id: 'buildkite', + service: integration_params) + end + end + end + + context 'when unsuccessful' do + it 'returns an error response when the integration test fails' do + stub_request(:get, 'http://example.com/rest/api/2/serverInfo') + .to_return(status: 404) + + put :test, params: project_params(service: integration_params) + + expect(response).to be_successful + expect(json_response).to eq( + 'error' => true, + 'message' => 'Connection failed. Please check your settings.', + 'service_response' => '', + 'test_failed' => true + ) + end + + context 'with the Slack integration' do + let_it_be(:integration) { build(:integrations_slack) } + + it 'returns an error response when the URL is blocked' do + put :test, params: project_params(service: { webhook: 'http://127.0.0.1' }) + + expect(response).to be_successful + expect(json_response).to eq( + 'error' => true, + 'message' => 'Connection failed. Please check your settings.', + 'service_response' => "URL 'http://127.0.0.1' is blocked: Requests to localhost are not allowed", + 'test_failed' => true + ) + end + + it 'returns an error response when a network exception is raised' do + expect_next(Integrations::Slack).to receive(:test).and_raise(Errno::ECONNREFUSED) + + put :test, params: project_params + + expect(response).to be_successful + expect(json_response).to eq( + 'error' => true, + 'message' => 'Connection failed. Please check your settings.', + 'service_response' => 'Connection refused', + 'test_failed' => true + ) + end + end + end + end + + describe 'PUT #update' do + describe 'as HTML' do + let(:integration_params) { { active: true } } + let(:params) { project_params(service: integration_params) } + + let(:message) { 'Jira settings saved and active.' } + let(:redirect_url) { edit_project_settings_integration_path(project, integration) } + + before do + stub_jira_integration_test + + put :update, params: params + end + + shared_examples 'integration update' do + it 'redirects to the correct url with a flash message' do + expect(response).to redirect_to(redirect_url) + expect(flash[:notice]).to eq(message) + end + end + + context 'when update fails' do + let(:integration_params) { { url: 'https://new.com', password: '' } } + + it 'renders the edit form' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:edit) + expect(integration.reload.url).not_to eq('https://new.com') + end + end + + context 'when param `active` is set to true' do + let(:params) { project_params(service: integration_params, redirect_to: redirect) } + + context 'when redirect_to param is present' do + let(:redirect) { '/redirect_here' } + let(:redirect_url) { redirect } + + it_behaves_like 'integration update' + end + + context 'when redirect_to is an external domain' do + let(:redirect) { 'http://examle.com' } + + it_behaves_like 'integration update' + end + + context 'when redirect_to param is an empty string' do + let(:redirect) { '' } + + it_behaves_like 'integration update' + end + end + + context 'when param `active` is set to false' do + let(:integration_params) { { active: false } } + let(:message) { 'Jira settings saved, but not active.' } + + it_behaves_like 'integration update' + end + + context 'when param `inherit_from_id` is set to empty string' do + let(:integration_params) { { inherit_from_id: '' } } + + it 'sets inherit_from_id to nil' do + expect(integration.reload.inherit_from_id).to eq(nil) + end + end + + context 'when param `inherit_from_id` is set to an instance integration' do + let(:instance_integration) do + create(:jira_integration, :instance, url: 'http://instance.com', password: 'instance') + end + + let(:integration_params) do + { inherit_from_id: instance_integration.id, url: 'http://custom.com', password: 'custom' } + end + + it 'ignores submitted params and inherits instance settings' do + expect(integration.reload).to have_attributes( + inherit_from_id: instance_integration.id, + url: instance_integration.url, + password: instance_integration.password + ) + end + end + + context 'when param `inherit_from_id` is set to a group integration' do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:jira_integration) { create(:jira_integration, project: project) } + + let(:group_integration) do + create(:jira_integration, :group, group: group, url: 'http://group.com', password: 'group') + end + + let(:integration_params) do + { inherit_from_id: group_integration.id, url: 'http://custom.com', password: 'custom' } + end + + it 'ignores submitted params and inherits group settings' do + expect(integration.reload).to have_attributes( + inherit_from_id: group_integration.id, + url: group_integration.url, + password: group_integration.password + ) + end + end + + context 'when param `inherit_from_id` is set to an unrelated group' do + let_it_be(:group) { create(:group) } + + let(:group_integration) do + create(:jira_integration, :group, group: group, url: 'http://group.com', password: 'group') + end + + let(:integration_params) do + { inherit_from_id: group_integration.id, url: 'http://custom.com', password: 'custom' } + end + + it 'ignores the param and saves the submitted settings' do + expect(integration.reload).to have_attributes( + inherit_from_id: nil, + url: 'http://custom.com', + password: 'custom' + ) + end + end + end + + describe 'as JSON' do + before do + stub_jira_integration_test + put :update, params: project_params(service: integration_params, format: :json) + end + + context 'when update succeeds' do + let(:integration_params) { { url: 'http://example.com', password: 'password' } } + + it 'returns success response' do + expect(response).to be_successful + expect(json_response).to include( + 'active' => true, + 'errors' => {} + ) + end + end + + context 'when update fails with missing password' do + let(:integration_params) { { url: 'http://example.com' } } + + it 'returns JSON response errors' do + expect(response).not_to be_successful + expect(json_response).to include( + 'active' => true, + 'errors' => { + 'password' => ["can't be blank"] + } + ) + end + end + + context 'when update fails with invalid URL' do + let(:integration_params) { { url: '', password: 'password' } } + + it 'returns JSON response with errors' do + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to include( + 'active' => true, + 'errors' => { 'url' => ['must be a valid URL', "can't be blank"] } + ) + end + end + end + end + + describe 'GET #edit' do + context 'with Jira service' do + let(:integration_param) { 'jira' } + + before do + get :edit, params: project_params(id: integration_param) + end + + context 'with approved services' do + it 'renders edit page' do + expect(response).to be_successful + end + end end end + + private + + def project_params(opts = {}) + opts.reverse_merge( + namespace_id: project.namespace, + project_id: project, + id: integration.to_param + ) + end end diff --git a/spec/features/groups/clusters/user_spec.rb b/spec/features/groups/clusters/user_spec.rb index 74ea72b238f..6b512323d4d 100644 --- a/spec/features/groups/clusters/user_spec.rb +++ b/spec/features/groups/clusters/user_spec.rb @@ -112,9 +112,9 @@ RSpec.describe 'User Cluster', :js do context 'when user destroys the cluster' do before do click_link 'Advanced Settings' - click_button 'Remove integration and resources' + find('[data-testid="remove-integration-button"]').click fill_in 'confirm_cluster_name_input', with: cluster.name - click_button 'Remove integration' + find('[data-testid="remove-integration-modal-button"]').click end it 'user sees creation form with the successful message' do diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index a8a23ba1c85..5c54b7fda7c 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -66,9 +66,9 @@ RSpec.describe 'Gcp Cluster', :js do context 'when user destroys the cluster' do before do click_link 'Advanced Settings' - click_button 'Remove integration and resources' + find('[data-testid="remove-integration-button"]').click fill_in 'confirm_cluster_name_input', with: cluster.name - click_button 'Remove integration' + find('[data-testid="remove-integration-modal-button"]').click click_link 'Certificate' end diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb index b6bfaa3a9b9..527d038f975 100644 --- a/spec/features/projects/clusters/user_spec.rb +++ b/spec/features/projects/clusters/user_spec.rb @@ -100,9 +100,9 @@ RSpec.describe 'User Cluster', :js do context 'when user destroys the cluster' do before do click_link 'Advanced Settings' - click_button 'Remove integration and resources' + find('[data-testid="remove-integration-button"]').click fill_in 'confirm_cluster_name_input', with: cluster.name - click_button 'Remove integration' + find('[data-testid="remove-integration-modal-button"]').click click_link 'Certificate' end diff --git a/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb b/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb index 2821f35f6a6..e7d4ed58549 100644 --- a/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb +++ b/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb @@ -34,7 +34,7 @@ RSpec.describe 'User activates issue tracker', :js do it 'activates the integration' do expect(page).to have_content("#{tracker} settings saved and active.") - expect(page).to have_current_path(edit_project_integration_path(project, tracker.parameterize(separator: '_')), ignore_query: true) + expect(page).to have_current_path(edit_project_settings_integration_path(project, tracker.parameterize(separator: '_')), ignore_query: true) end it 'shows the link in the menu' do @@ -58,7 +58,7 @@ RSpec.describe 'User activates issue tracker', :js do end expect(page).to have_content("#{tracker} settings saved and active.") - expect(page).to have_current_path(edit_project_integration_path(project, tracker.parameterize(separator: '_')), ignore_query: true) + expect(page).to have_current_path(edit_project_settings_integration_path(project, tracker.parameterize(separator: '_')), ignore_query: true) end end end @@ -73,7 +73,7 @@ RSpec.describe 'User activates issue tracker', :js do it 'saves but does not activate the integration' do expect(page).to have_content("#{tracker} settings saved, but not active.") - expect(page).to have_current_path(edit_project_integration_path(project, tracker.parameterize(separator: '_')), ignore_query: true) + expect(page).to have_current_path(edit_project_settings_integration_path(project, tracker.parameterize(separator: '_')), ignore_query: true) end it 'does not show the external tracker link in the menu' do diff --git a/spec/features/projects/integrations/user_activates_jira_spec.rb b/spec/features/projects/integrations/user_activates_jira_spec.rb index f855d6befe7..b63c7a115bc 100644 --- a/spec/features/projects/integrations/user_activates_jira_spec.rb +++ b/spec/features/projects/integrations/user_activates_jira_spec.rb @@ -20,7 +20,7 @@ RSpec.describe 'User activates Jira', :js do it 'activates the Jira integration' do expect(page).to have_content('Jira settings saved and active.') - expect(page).to have_current_path(edit_project_integration_path(project, :jira), ignore_query: true) + expect(page).to have_current_path(edit_project_settings_integration_path(project, :jira), ignore_query: true) end unless Gitlab.ee? @@ -55,7 +55,7 @@ RSpec.describe 'User activates Jira', :js do click_test_then_save_integration expect(page).to have_content('Jira settings saved and active.') - expect(page).to have_current_path(edit_project_integration_path(project, :jira), ignore_query: true) + expect(page).to have_current_path(edit_project_settings_integration_path(project, :jira), ignore_query: true) end end end @@ -72,7 +72,7 @@ RSpec.describe 'User activates Jira', :js do it 'saves but does not activate the Jira integration' do expect(page).to have_content('Jira settings saved, but not active.') - expect(page).to have_current_path(edit_project_integration_path(project, :jira), ignore_query: true) + expect(page).to have_current_path(edit_project_settings_integration_path(project, :jira), ignore_query: true) end it 'does not show the Jira link in the menu' do diff --git a/spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb b/spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb index ed0877ab0e9..54c9ec0f62e 100644 --- a/spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb +++ b/spec/features/projects/integrations/user_activates_mattermost_slash_command_spec.rb @@ -15,7 +15,7 @@ RSpec.describe 'Set up Mattermost slash commands', :js do let(:mattermost_enabled) { true } describe 'activation' do - let(:edit_path) { edit_project_integration_path(project, :mattermost_slash_commands) } + let(:edit_path) { edit_project_settings_integration_path(project, :mattermost_slash_commands) } include_examples 'user activates the Mattermost Slash Command integration' end diff --git a/spec/features/projects/integrations/user_activates_slack_notifications_spec.rb b/spec/features/projects/integrations/user_activates_slack_notifications_spec.rb index 616469c5df8..e89f6e309ea 100644 --- a/spec/features/projects/integrations/user_activates_slack_notifications_spec.rb +++ b/spec/features/projects/integrations/user_activates_slack_notifications_spec.rb @@ -34,7 +34,7 @@ RSpec.describe 'User activates Slack notifications', :js do pipeline_channel: 6, wiki_page_channel: 7) - visit(edit_project_integration_path(project, integration)) + visit(edit_project_settings_integration_path(project, integration)) end it 'filters events by channel' do diff --git a/spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb b/spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb index 0b4c9620bdf..df8cd84ffdb 100644 --- a/spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb +++ b/spec/features/projects/integrations/user_activates_slack_slash_command_spec.rb @@ -24,7 +24,11 @@ RSpec.describe 'Slack slash commands', :js do click_active_checkbox click_on 'Save' - expect(page).to have_current_path(edit_project_integration_path(project, :slack_slash_commands), ignore_query: true) + expect(page).to have_current_path( + edit_project_settings_integration_path(project, :slack_slash_commands), + ignore_query: true + ) + expect(page).to have_content('Slack slash commands settings saved, but not active.') end @@ -32,7 +36,11 @@ RSpec.describe 'Slack slash commands', :js do fill_in 'Token', with: 'token' click_on 'Save' - expect(page).to have_current_path(edit_project_integration_path(project, :slack_slash_commands), ignore_query: true) + expect(page).to have_current_path( + edit_project_settings_integration_path(project, :slack_slash_commands), + ignore_query: true + ) + expect(page).to have_content('Slack slash commands settings saved and active.') end diff --git a/spec/frontend/blob/csv/csv_viewer_spec.js b/spec/frontend/blob/csv/csv_viewer_spec.js index ff96193a20c..9364f76da5e 100644 --- a/spec/frontend/blob/csv/csv_viewer_spec.js +++ b/spec/frontend/blob/csv/csv_viewer_spec.js @@ -44,7 +44,7 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => { describe('when the CSV contains errors', () => { it('should render alert with correct props', async () => { createComponent({ csv: brokenCsv }); - await nextTick; + await nextTick(); expect(findAlert().props()).toMatchObject({ papaParseErrors: [{ code: 'UndetectableDelimiter' }], @@ -55,14 +55,14 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => { describe('when the CSV contains no errors', () => { it('should not render alert', async () => { createComponent(); - await nextTick; + await nextTick(); expect(findAlert().exists()).toBe(false); }); it('renders the CSV table with the correct attributes', async () => { createComponent(); - await nextTick; + await nextTick(); expect(findCsvTable().attributes()).toMatchObject({ 'empty-text': 'No CSV data to display.', @@ -72,7 +72,7 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => { it('renders the CSV table with the correct content', async () => { createComponent({ mountFunction: mount }); - await nextTick; + await nextTick(); expect(getAllByRole(wrapper.element, 'row', { name: /One/i })).toHaveLength(1); expect(getAllByRole(wrapper.element, 'row', { name: /Two/i })).toHaveLength(1); @@ -93,7 +93,7 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => { skipEmptyLines: true, complete: expect.any(Function), }); - await nextTick; + await nextTick(); expect(wrapper.vm.items).toEqual(validCsv.split(',')); }); }); diff --git a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap index 42d81900911..46ee123a12d 100644 --- a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap +++ b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap @@ -1,167 +1,44 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Remove cluster confirmation modal renders splitbutton with modal included 1`] = ` +exports[`Remove cluster confirmation modal renders buttons with modal included 1`] = ` <div - class="gl-display-flex gl-justify-content-end" + class="gl-display-flex" > - <div - class="dropdown b-dropdown gl-new-dropdown btn-group" - menu-class="dropdown-menu-large" + <button + class="btn gl-mr-3 btn-danger btn-md gl-button" + data-testid="remove-integration-and-resources-button" + type="button" > - <button - class="btn btn-danger btn-md gl-button split-content-button" - type="button" + <!----> + + <!----> + + <span + class="gl-button-text" > - <!----> - - <!----> - - <span - class="gl-new-dropdown-button-text" - > - Remove integration and resources - </span> - - <!----> - </button> - <button - aria-expanded="false" - aria-haspopup="true" - class="btn dropdown-toggle btn-danger btn-md gl-button gl-dropdown-toggle dropdown-toggle-split" - type="button" - > - <span - class="sr-only" - > - Toggle dropdown - </span> - </button> - <ul - class="dropdown-menu dropdown-menu-large" - role="menu" - tabindex="-1" + + Remove integration and resources + + </span> + </button> + + <button + class="btn btn-danger btn-md gl-button btn-danger-secondary" + data-testid="remove-integration-button" + type="button" + > + <!----> + + <!----> + + <span + class="gl-button-text" > - <div - class="gl-new-dropdown-inner" - > - <!----> - - <!----> - - <div - class="gl-new-dropdown-contents" - > - <!----> - - <li - class="gl-new-dropdown-item" - role="presentation" - > - <button - class="dropdown-item" - role="menuitem" - type="button" - > - <svg - aria-hidden="true" - class="gl-icon s16 gl-new-dropdown-item-check-icon gl-mt-3 gl-align-self-start" - data-testid="dropdown-item-checkbox" - role="img" - > - <use - href="#mobile-issue-close" - /> - </svg> - - <!----> - - <!----> - - <div - class="gl-new-dropdown-item-text-wrapper" - > - <p - class="gl-new-dropdown-item-text-primary" - > - <strong> - Remove integration and resources - </strong> - - <div> - Deletes all GitLab resources attached to this cluster during removal - </div> - </p> - - <!----> - </div> - - <!----> - </button> - </li> - - <li - class="gl-new-dropdown-divider" - role="presentation" - > - <hr - aria-orientation="horizontal" - class="dropdown-divider" - role="separator" - /> - </li> - <li - class="gl-new-dropdown-item" - role="presentation" - > - <button - class="dropdown-item" - role="menuitem" - type="button" - > - <svg - aria-hidden="true" - class="gl-icon s16 gl-new-dropdown-item-check-icon gl-visibility-hidden gl-mt-3 gl-align-self-start" - data-testid="dropdown-item-checkbox" - role="img" - > - <use - href="#mobile-issue-close" - /> - </svg> - - <!----> - - <!----> - - <div - class="gl-new-dropdown-item-text-wrapper" - > - <p - class="gl-new-dropdown-item-text-primary" - > - <strong> - Remove integration - </strong> - - <div> - Removes cluster from project but keeps associated resources - </div> - </p> - - <!----> - </div> - - <!----> - </button> - </li> - - <!----> - </div> - - <!----> - </div> - </ul> - </div> + + Remove integration + + </span> + </button> <!----> </div> diff --git a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js index 173fefe6167..53683af893a 100644 --- a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js +++ b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js @@ -3,7 +3,6 @@ import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import { stubComponent } from 'helpers/stub_component'; import RemoveClusterConfirmation from '~/clusters/components/remove_cluster_confirmation.vue'; -import SplitButton from '~/vue_shared/components/split_button.vue'; describe('Remove cluster confirmation modal', () => { let wrapper; @@ -24,14 +23,17 @@ describe('Remove cluster confirmation modal', () => { wrapper = null; }); - it('renders splitbutton with modal included', () => { + it('renders buttons with modal included', () => { createComponent(); expect(wrapper.element).toMatchSnapshot(); }); - describe('split button dropdown', () => { + describe('two buttons', () => { const findModal = () => wrapper.findComponent(GlModal); - const findSplitButton = () => wrapper.findComponent(SplitButton); + const findRemoveIntegrationButton = () => + wrapper.find('[data-testid="remove-integration-button"]'); + const findRemoveIntegrationAndResourcesButton = () => + wrapper.find('[data-testid="remove-integration-and-resources-button"]'); beforeEach(() => { createComponent({ @@ -41,8 +43,8 @@ describe('Remove cluster confirmation modal', () => { jest.spyOn(findModal().vm, 'show').mockReturnValue(); }); - it('opens modal with "cleanup" option', async () => { - findSplitButton().vm.$emit('remove-cluster-and-cleanup'); + it('open modal with "cleanup" option', async () => { + findRemoveIntegrationAndResourcesButton().trigger('click'); await nextTick(); @@ -53,8 +55,8 @@ describe('Remove cluster confirmation modal', () => { ); }); - it('opens modal without "cleanup" option', async () => { - findSplitButton().vm.$emit('remove-cluster'); + it('open modal without "cleanup" option', async () => { + findRemoveIntegrationButton().trigger('click'); await nextTick(); @@ -71,8 +73,8 @@ describe('Remove cluster confirmation modal', () => { }); it('renders regular button instead', () => { - expect(findSplitButton().exists()).toBe(false); - expect(wrapper.find('[data-testid="btnRemove"]').exists()).toBe(true); + expect(findRemoveIntegrationAndResourcesButton().exists()).toBe(false); + expect(findRemoveIntegrationButton().exists()).toBe(true); }); }); }); diff --git a/spec/frontend/content_editor/remark_markdown_processing_spec.js b/spec/frontend/content_editor/remark_markdown_processing_spec.js index eeabcf0fe4c..06f109f8603 100644 --- a/spec/frontend/content_editor/remark_markdown_processing_spec.js +++ b/spec/frontend/content_editor/remark_markdown_processing_spec.js @@ -13,6 +13,7 @@ import ListItem from '~/content_editor/extensions/list_item'; import OrderedList from '~/content_editor/extensions/ordered_list'; import Paragraph from '~/content_editor/extensions/paragraph'; import Sourcemap from '~/content_editor/extensions/sourcemap'; +import Strike from '~/content_editor/extensions/strike'; import remarkMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer'; import markdownSerializer from '~/content_editor/services/markdown_serializer'; @@ -34,6 +35,7 @@ const tiptapEditor = createTestEditor({ ListItem, OrderedList, Sourcemap, + Strike, ], }); @@ -54,6 +56,7 @@ const { link, listItem, orderedList, + strike, }, } = createDocBuilder({ tiptapEditor, @@ -72,6 +75,7 @@ const { listItem: { nodeType: ListItem.name }, orderedList: { nodeType: OrderedList.name }, paragraph: { nodeType: Paragraph.name }, + strike: { nodeType: Strike.name }, }, }); @@ -566,10 +570,50 @@ const fn = () => 'GitLab'; ), ), }, + { + markdown: '~~Strikedthrough text~~', + expectedDoc: doc( + paragraph( + sourceAttrs('0:23', '~~Strikedthrough text~~'), + strike(sourceAttrs('0:23', '~~Strikedthrough text~~'), 'Strikedthrough text'), + ), + ), + }, + { + markdown: '<del>Strikedthrough text</del>', + expectedDoc: doc( + paragraph( + sourceAttrs('0:30', '<del>Strikedthrough text</del>'), + strike(sourceAttrs('0:30', '<del>Strikedthrough text</del>'), 'Strikedthrough text'), + ), + ), + }, + { + markdown: '<strike>Strikedthrough text</strike>', + expectedDoc: doc( + paragraph( + sourceAttrs('0:36', '<strike>Strikedthrough text</strike>'), + strike( + sourceAttrs('0:36', '<strike>Strikedthrough text</strike>'), + 'Strikedthrough text', + ), + ), + ), + }, + { + markdown: '<s>Strikedthrough text</s>', + expectedDoc: doc( + paragraph( + sourceAttrs('0:26', '<s>Strikedthrough text</s>'), + strike(sourceAttrs('0:26', '<s>Strikedthrough text</s>'), 'Strikedthrough text'), + ), + ), + }, ])('processes %s correctly', async ({ markdown, expectedDoc }) => { const trimmed = markdown.trim(); const document = await deserialize(trimmed); + expect(expectedDoc).not.toBeFalsy(); expect(document.toJSON()).toEqual(expectedDoc.toJSON()); expect(serialize(document)).toEqual(trimmed); }); diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js index 25b7483f234..10625d481ce 100644 --- a/spec/frontend/content_editor/services/markdown_serializer_spec.js +++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js @@ -1176,6 +1176,10 @@ Oranges are orange [^1] ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'} ${'code'} | ${'`code`'} | ${'`code modified`'} ${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'} + ${'strike'} | ${'~~striked~~'} | ${'~~striked modified~~'} + ${'strike'} | ${'<del>striked</del>'} | ${'<del>striked modified</del>'} + ${'strike'} | ${'<strike>striked</strike>'} | ${'<strike>striked modified</strike>'} + ${'strike'} | ${'<s>striked</s>'} | ${'<s>striked modified</s>'} `( 'preserves original $mark syntax when sourceMarkdown is available', async ({ content, modifiedContent }) => { diff --git a/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js b/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js index 384d6699150..af56b94f90b 100644 --- a/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js +++ b/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js @@ -18,7 +18,7 @@ describe('CustomMetricsForm', () => { wrapper = shallowMount(CustomMetricsForm, { propsData: { customMetricsPath: '', - editProjectServicePath: '', + editIntegrationPath: '', metricPersisted, validateQueryPath: '', formData, diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js index eee17e118a0..e52c5abbc7b 100644 --- a/spec/frontend/diffs/components/commit_item_spec.js +++ b/spec/frontend/diffs/components/commit_item_spec.js @@ -6,8 +6,6 @@ import Component from '~/diffs/components/commit_item.vue'; import { getTimeago } from '~/lib/utils/datetime_utility'; import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; -jest.mock('~/user_popovers'); - const TEST_AUTHOR_NAME = 'test'; const TEST_AUTHOR_EMAIL = 'test+test@gitlab.com'; const TEST_AUTHOR_GRAVATAR = `${TEST_HOST}/avatar/test?s=40`; diff --git a/spec/frontend/fixtures/services.rb b/spec/frontend/fixtures/integrations.rb index f0bb8fb962f..1bafb0bfe78 100644 --- a/spec/frontend/fixtures/services.rb +++ b/spec/frontend/fixtures/integrations.rb @@ -2,11 +2,11 @@ require 'spec_helper' -RSpec.describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do +RSpec.describe Projects::Settings::IntegrationsController, '(JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') } + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'integrations-project') } let!(:service) { create(:custom_issue_tracker_integration, project: project) } let(:user) { project.first_owner } @@ -20,7 +20,7 @@ RSpec.describe Projects::ServicesController, '(JavaScript fixtures)', type: :con remove_repository(project) end - it 'services/edit_service.html' do + it 'settings/integrations/edit.html' do get :edit, params: { namespace_id: namespace, project_id: project, diff --git a/spec/frontend/fixtures/prometheus_service.rb b/spec/frontend/fixtures/prometheus_integration.rb index aed73dc1096..883dbb929a2 100644 --- a/spec/frontend/fixtures/prometheus_service.rb +++ b/spec/frontend/fixtures/prometheus_integration.rb @@ -2,11 +2,11 @@ require 'spec_helper' -RSpec.describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do +RSpec.describe Projects::Settings::IntegrationsController, '(JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} - let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') } + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'integrations-project') } let!(:integration) { create(:prometheus_integration, project: project) } let(:user) { project.first_owner } @@ -20,7 +20,7 @@ RSpec.describe Projects::ServicesController, '(JavaScript fixtures)', type: :con remove_repository(project) end - it 'services/prometheus/prometheus_service.html' do + it 'integrations/prometheus/prometheus_integration.html' do get :edit, params: { namespace_id: namespace, project_id: project, diff --git a/spec/frontend/lib/utils/users_cache_spec.js b/spec/frontend/lib/utils/users_cache_spec.js index d35ba20f570..5a55874b5fa 100644 --- a/spec/frontend/lib/utils/users_cache_spec.js +++ b/spec/frontend/lib/utils/users_cache_spec.js @@ -154,8 +154,8 @@ describe('UsersCache', () => { }; const user = await UsersCache.retrieveById(dummyUserId); - expect(user).toBe(dummyUser); - expect(UsersCache.internalStorage[dummyUserId]).toBe(dummyUser); + expect(user).toEqual(dummyUser); + expect(UsersCache.internalStorage[dummyUserId]).toEqual(dummyUser); }); it('returns undefined if Ajax call fails and cache is empty', async () => { @@ -180,6 +180,29 @@ describe('UsersCache', () => { const user = await UsersCache.retrieveById(dummyUserId); expect(user).toBe(dummyUser); }); + + it('does not clobber existing cached values', async () => { + UsersCache.internalStorage[dummyUserId] = { + status: dummyUserStatus, + }; + + apiSpy = (id) => { + expect(id).toBe(dummyUserId); + + return Promise.resolve({ + data: dummyUser, + }); + }; + + const user = await UsersCache.retrieveById(dummyUserId); + const expectedUser = { + status: dummyUserStatus, + ...dummyUser, + }; + + expect(user).toEqual(expectedUser); + expect(UsersCache.internalStorage[dummyUserId]).toEqual(expectedUser); + }); }); describe('retrieveStatusById', () => { diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js index 298a01e4f4d..c02f483dac9 100644 --- a/spec/frontend/members/components/table/members_table_spec.js +++ b/spec/frontend/members/components/table/members_table_spec.js @@ -21,7 +21,6 @@ import { BADGE_LABELS_PENDING_OWNER_APPROVAL, TAB_QUERY_PARAM_VALUES, } from '~/members/constants'; -import * as initUserPopovers from '~/user_popovers'; import { member as memberMock, directMember, @@ -257,14 +256,6 @@ describe('MembersTable', () => { }); }); - it('initializes user popovers when mounted', () => { - const initUserPopoversMock = jest.spyOn(initUserPopovers, 'default'); - - createComponent(); - - expect(initUserPopoversMock).toHaveBeenCalled(); - }); - it('adds QA selector to table', () => { createComponent(); diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap index a93035cc53a..a9f37f90561 100644 --- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap +++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap @@ -151,7 +151,7 @@ exports[`Dashboard template matches the default snapshot 1`] = ` emptynodatasvgpath="/images/illustrations/monitoring/no_data.svg" emptyunabletoconnectsvgpath="/images/illustrations/monitoring/unable_to_connect.svg" selectedstate="gettingStarted" - settingspath="/monitoring/monitor-project/-/integrations/prometheus/edit" + settingspath="/monitoring/monitor-project/-/settings/integrations/prometheus/edit" /> </div> `; diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js index ba5d4d27e55..df41aad2981 100644 --- a/spec/frontend/notes/components/comment_form_spec.js +++ b/spec/frontend/notes/components/comment_form_spec.js @@ -487,8 +487,8 @@ describe('issue_comment_form component', () => { await findCloseReopenButton().trigger('click'); - await nextTick; - await nextTick; + await nextTick(); + await nextTick(); expect(createFlash).toHaveBeenCalledWith({ message: `Something went wrong while closing the ${type}. Please try again later.`, @@ -523,8 +523,8 @@ describe('issue_comment_form component', () => { await findCloseReopenButton().trigger('click'); - await nextTick; - await nextTick; + await nextTick(); + await nextTick(); expect(createFlash).toHaveBeenCalledWith({ message: `Something went wrong while reopening the ${type}. Please try again later.`, diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js index 413ee815906..e018523472b 100644 --- a/spec/frontend/notes/components/notes_app_spec.js +++ b/spec/frontend/notes/components/notes_app_spec.js @@ -19,8 +19,6 @@ import '~/behaviors/markdown/render_gfm'; import OrderedLayout from '~/vue_shared/components/ordered_layout.vue'; import * as mockData from '../mock_data'; -jest.mock('~/user_popovers', () => jest.fn()); - setTestTimeout(1000); const TYPE_COMMENT_FORM = 'comment-form'; diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js index ae5404f2d13..d5b4b3c22d8 100644 --- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js +++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js @@ -190,7 +190,7 @@ describe('Interval Pattern Input Component', () => { findCustomInput().setValue(newValue); - await nextTick; + await nextTick(); expect(findSelectedRadioKey()).toBe(customKey); }); diff --git a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js index 6d0e99ff63e..b574b217180 100644 --- a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js +++ b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js @@ -55,6 +55,8 @@ describe('Pipelines stage component', () => { const findDropdownToggle = () => wrapper.find('button.dropdown-toggle'); const findDropdownMenu = () => wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]'); + const findDropdownMenuTitle = () => + wrapper.find('[data-testid="pipeline-stage-dropdown-menu-title"]'); const findMergeTrainWarning = () => wrapper.find('[data-testid="warning-message-merge-trains"]'); const openStageDropdown = () => { @@ -97,6 +99,7 @@ describe('Pipelines stage component', () => { it('should render the received data and emit `clickedDropdown` event', async () => { expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name); + expect(findDropdownMenuTitle().text()).toContain(stageReply.name); expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown'); }); diff --git a/spec/frontend/prometheus_metrics/custom_metrics_spec.js b/spec/frontend/prometheus_metrics/custom_metrics_spec.js index 473327bf5e1..fc906194059 100644 --- a/spec/frontend/prometheus_metrics/custom_metrics_spec.js +++ b/spec/frontend/prometheus_metrics/custom_metrics_spec.js @@ -6,9 +6,9 @@ import CustomMetrics from '~/prometheus_metrics/custom_metrics'; import { metrics1 as metrics } from './mock_data'; describe('PrometheusMetrics', () => { - const FIXTURE = 'services/prometheus/prometheus_service.html'; + const FIXTURE = 'integrations/prometheus/prometheus_integration.html'; const customMetricsEndpoint = - 'http://test.host/frontend-fixtures/services-project/prometheus/metrics'; + 'http://test.host/frontend-fixtures/integrations-project/prometheus/metrics'; let mock; beforeEach(() => { diff --git a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js index 1151c0b3769..0df2aad5882 100644 --- a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js +++ b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js @@ -7,7 +7,7 @@ import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics'; import { metrics2 as metrics, missingVarMetrics } from './mock_data'; describe('PrometheusMetrics', () => { - const FIXTURE = 'services/prometheus/prometheus_service.html'; + const FIXTURE = 'integrations/prometheus/prometheus_integration.html'; beforeEach(() => { loadHTMLFixture(FIXTURE); diff --git a/spec/frontend/repository/components/new_directory_modal_spec.js b/spec/frontend/repository/components/new_directory_modal_spec.js index fe7f024e3ea..e1c50d63851 100644 --- a/spec/frontend/repository/components/new_directory_modal_spec.js +++ b/spec/frontend/repository/components/new_directory_modal_spec.js @@ -67,7 +67,7 @@ describe('NewDirectoryModal', () => { await findBranchName().vm.$emit('input', branchName); await findCommitMessage().vm.$emit('input', commitMessage); await findMrToggle().vm.$emit('change', createNewMr); - await nextTick; + await nextTick(); }; const submitForm = async () => { diff --git a/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js index c870bbecd76..724fba62479 100644 --- a/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js +++ b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js @@ -72,7 +72,7 @@ describe('boards sidebar remove issue', () => { createComponent({ canUpdate: true, slots }); findEditButton().vm.$emit('click'); - await nextTick; + await nextTick(); expect(findCollapsed().isVisible()).toBe(false); expect(findExpanded().isVisible()).toBe(true); diff --git a/spec/frontend/user_popovers_spec.js b/spec/frontend/user_popovers_spec.js index fa598716645..2c3db36d7e6 100644 --- a/spec/frontend/user_popovers_spec.js +++ b/spec/frontend/user_popovers_spec.js @@ -26,12 +26,13 @@ describe('User Popovers', () => { return link; }; + const findPopovers = () => { + return Array.from(document.querySelectorAll('[data-testid="user-popover"]')); + }; const dummyUser = { name: 'root', username: 'root', is_followed: false }; const dummyUserStatus = { message: 'active' }; - let popovers; - const triggerEvent = (eventName, el) => { const event = new MouseEvent(eventName, { bubbles: true, @@ -54,56 +55,73 @@ describe('User Popovers', () => { .mockImplementation((userId) => userStatusCacheSpy(userId)); jest.spyOn(UsersCache, 'updateById'); - popovers = initUserPopovers(document.querySelectorAll(selector)); + initUserPopovers((popoverInstance) => { + const mountingRoot = document.createElement('div'); + document.body.appendChild(mountingRoot); + popoverInstance.$mount(mountingRoot); + }); }); afterEach(() => { resetHTMLFixture(); }); - it('initializes a popover for each user link with a user id', () => { - const linksWithUsers = findFixtureLinks(); + describe('shows a placeholder popover on hover', () => { + let linksWithUsers; + beforeEach(() => { + linksWithUsers = findFixtureLinks(); + linksWithUsers.forEach((el) => { + triggerEvent('mouseover', el); + }); + }); - expect(linksWithUsers.length).toBe(popovers.length); - }); + it('for initial links', () => { + expect(findPopovers().length).toBe(linksWithUsers.length); + }); - it('adds popovers to user links added to the DOM tree after the initial call', async () => { - document.body.appendChild(createUserLink()); - document.body.appendChild(createUserLink()); + it('for elements added after initial load', async () => { + const addedLinks = [createUserLink(), createUserLink()]; + addedLinks.forEach((link) => { + document.body.appendChild(link); + }); - const linksWithUsers = findFixtureLinks(); + jest.runOnlyPendingTimers(); - expect(linksWithUsers.length).toBe(popovers.length + 2); + addedLinks.forEach((link) => { + triggerEvent('mouseover', link); + }); + + expect(findPopovers().length).toBe(linksWithUsers.length + addedLinks.length); + }); }); - it('does not initialize the user popovers twice for the same element', () => { - const newPopovers = initUserPopovers(document.querySelectorAll(selector)); - const samePopovers = popovers.every((popover, index) => newPopovers[index] === popover); + it('does not initialize the user popovers twice for the same element', async () => { + const [firstUserLink] = findFixtureLinks(); + triggerEvent('mouseover', firstUserLink); + jest.runOnlyPendingTimers(); + triggerEvent('mouseleave', firstUserLink); + jest.runOnlyPendingTimers(); + triggerEvent('mouseover', firstUserLink); + jest.runOnlyPendingTimers(); - expect(samePopovers).toBe(true); + expect(findPopovers().length).toBe(1); }); - describe('when user link emits mouseenter event', () => { + describe('when user link emits mouseenter event with empty user cache', () => { let userLink; beforeEach(() => { UsersCache.retrieveById.mockReset(); - userLink = document.querySelector(selector); - - triggerEvent('mouseenter', userLink); - }); + [userLink] = findFixtureLinks(); - it('removes title attribute from user links', () => { - expect(userLink.getAttribute('title')).toBeFalsy(); - expect(userLink.dataset.originalTitle).toBeFalsy(); + triggerEvent('mouseover', userLink); }); - it('populates popovers with preloaded user data', () => { + it('populates popover with preloaded user data', () => { const { name, userId, username } = userLink.dataset; - const [firstPopover] = popovers; - expect(firstPopover.$props.user).toEqual( + expect(userLink.user).toEqual( expect.objectContaining({ name, userId, @@ -111,6 +129,21 @@ describe('User Popovers', () => { }), ); }); + }); + + describe('when user link emits mouseenter event', () => { + let userLink; + + beforeEach(() => { + [userLink] = findFixtureLinks(); + + triggerEvent('mouseover', userLink); + }); + + it('removes title attribute from user links', () => { + expect(userLink.getAttribute('title')).toBeFalsy(); + expect(userLink.dataset.originalTitle).toBeFalsy(); + }); it('fetches user info and status from the user cache', () => { const { userId } = userLink.dataset; @@ -118,42 +151,38 @@ describe('User Popovers', () => { expect(UsersCache.retrieveById).toHaveBeenCalledWith(userId); expect(UsersCache.retrieveStatusById).toHaveBeenCalledWith(userId); }); - }); - - it('removes aria-describedby attribute from the user link on mouseleave', () => { - const userLink = document.querySelector(selector); - userLink.setAttribute('aria-describedby', 'popover'); - triggerEvent('mouseleave', userLink); + it('removes aria-describedby attribute from the user link on mouseleave', () => { + userLink.setAttribute('aria-describedby', 'popover'); + triggerEvent('mouseleave', userLink); - expect(userLink.getAttribute('aria-describedby')).toBe(null); - }); - - it('updates toggle follow button and `UsersCache` when toggle follow button is clicked', async () => { - const [firstPopover] = popovers; - const withinFirstPopover = within(firstPopover.$el); - const findFollowButton = () => withinFirstPopover.queryByRole('button', { name: 'Follow' }); - const findUnfollowButton = () => withinFirstPopover.queryByRole('button', { name: 'Unfollow' }); + expect(userLink.getAttribute('aria-describedby')).toBe(null); + }); - const userLink = document.querySelector(selector); - triggerEvent('mouseenter', userLink); + it('updates toggle follow button and `UsersCache` when toggle follow button is clicked', async () => { + const [firstPopover] = findPopovers(); + const withinFirstPopover = within(firstPopover); + const findFollowButton = () => withinFirstPopover.queryByRole('button', { name: 'Follow' }); + const findUnfollowButton = () => + withinFirstPopover.queryByRole('button', { name: 'Unfollow' }); - await waitForPromises(); + jest.runOnlyPendingTimers(); - const { userId } = document.querySelector(selector).dataset; + const { userId } = document.querySelector(selector).dataset; - triggerEvent('click', findFollowButton()); + triggerEvent('click', findFollowButton()); - await waitForPromises(); + await waitForPromises(); - expect(findUnfollowButton()).not.toBe(null); - expect(UsersCache.updateById).toHaveBeenCalledWith(userId, { is_followed: true }); + expect(findUnfollowButton()).not.toBe(null); + expect(UsersCache.updateById).toHaveBeenCalledWith(userId, { is_followed: true }); - triggerEvent('click', findUnfollowButton()); + triggerEvent('click', findUnfollowButton()); - await waitForPromises(); + await waitForPromises(); - expect(findFollowButton()).not.toBe(null); - expect(UsersCache.updateById).toHaveBeenCalledWith(userId, { is_followed: false }); + expect(findFollowButton()).not.toBe(null); + expect(UsersCache.updateById).toHaveBeenCalledWith(userId, { is_followed: false }); + }); }); }); diff --git a/spec/frontend/vue_shared/components/ci_icon_spec.js b/spec/frontend/vue_shared/components/ci_icon_spec.js index 6d52db7ae65..1b502f9587c 100644 --- a/spec/frontend/vue_shared/components/ci_icon_spec.js +++ b/spec/frontend/vue_shared/components/ci_icon_spec.js @@ -5,6 +5,8 @@ import ciIcon from '~/vue_shared/components/ci_icon.vue'; describe('CI Icon component', () => { let wrapper; + const findIconWrapper = () => wrapper.find('[data-testid="ci-icon-wrapper"]'); + afterEach(() => { wrapper.destroy(); wrapper = null; @@ -23,6 +25,52 @@ describe('CI Icon component', () => { expect(wrapper.find(GlIcon).exists()).toBe(true); }); + describe('active icons', () => { + it.each` + isActive | cssClass + ${true} | ${'active'} + ${false} | ${'active'} + `('active should be $isActive', ({ isActive, cssClass }) => { + wrapper = shallowMount(ciIcon, { + propsData: { + status: { + icon: 'status_success', + }, + isActive, + }, + }); + + if (isActive) { + expect(findIconWrapper().classes()).toContain(cssClass); + } else { + expect(findIconWrapper().classes()).not.toContain(cssClass); + } + }); + }); + + describe('interactive icons', () => { + it.each` + isInteractive | cssClass + ${true} | ${'interactive'} + ${false} | ${'interactive'} + `('interactive should be $isInteractive', ({ isInteractive, cssClass }) => { + wrapper = shallowMount(ciIcon, { + propsData: { + status: { + icon: 'status_success', + }, + isInteractive, + }, + }); + + if (isInteractive) { + expect(findIconWrapper().classes()).toContain(cssClass); + } else { + expect(findIconWrapper().classes()).not.toContain(cssClass); + } + }); + }); + describe('rendering a status', () => { it.each` icon | group | cssClass diff --git a/spec/frontend/vue_shared/components/papa_parse_alert_spec.js b/spec/frontend/vue_shared/components/papa_parse_alert_spec.js index 9be2de17d01..ff4febd647e 100644 --- a/spec/frontend/vue_shared/components/papa_parse_alert_spec.js +++ b/spec/frontend/vue_shared/components/papa_parse_alert_spec.js @@ -22,7 +22,7 @@ describe('app/assets/javascripts/vue_shared/components/papa_parse_alert.vue', () it('should render alert with correct props', async () => { createComponent({ errorMessages: [{ code: 'MissingQuotes' }] }); - await nextTick; + await nextTick(); expect(findAlert().props()).toMatchObject({ variant: 'danger', @@ -37,7 +37,7 @@ describe('app/assets/javascripts/vue_shared/components/papa_parse_alert.vue', () createComponent({ errorMessages: [{ code: 'NotDefined', message: 'Error code is undefined' }], }); - await nextTick; + await nextTick(); expect(findAlert().text()).toContain('Error code is undefined'); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js index 31819d0e2f7..d5ea1a5acd1 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js @@ -152,13 +152,13 @@ describe('LabelsSelectRoot', () => { it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => { createComponent(); - await nextTick; + await nextTick(); expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true); }); it('renders `dropdown-title` component', async () => { createComponent(); - await nextTick; + await nextTick(); expect(wrapper.find(DropdownTitle).exists()).toBe(true); }); @@ -166,7 +166,7 @@ describe('LabelsSelectRoot', () => { createComponent(mockConfig, { default: 'None', }); - await nextTick; + await nextTick(); const valueComp = wrapper.find(DropdownValue); @@ -177,14 +177,14 @@ describe('LabelsSelectRoot', () => { it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', async () => { createComponent(); wrapper.vm.$store.dispatch('toggleDropdownButton'); - await nextTick; + await nextTick(); expect(wrapper.find(DropdownButton).exists()).toBe(true); }); it('renders `dropdown-contents` component when `showDropdownButton` & `showDropdownContents` prop is `true`', async () => { createComponent(); wrapper.vm.$store.dispatch('toggleDropdownContents'); - await nextTick; + await nextTick(); expect(wrapper.find(DropdownContents).exists()).toBe(true); }); diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb index 52f02fba4ec..d1c23fb51b7 100644 --- a/spec/helpers/environments_helper_spec.rb +++ b/spec/helpers/environments_helper_spec.rb @@ -18,7 +18,7 @@ RSpec.describe EnvironmentsHelper do it 'returns data' do expect(metrics_data).to include( - 'settings_path' => edit_project_integration_path(project, 'prometheus'), + 'settings_path' => edit_project_settings_integration_path(project, 'prometheus'), 'clusters_path' => project_clusters_path(project), 'metrics_dashboard_base_path' => project_metrics_dashboard_path(project, environment: environment), 'current_environment_name' => environment.name, diff --git a/spec/helpers/operations_helper_spec.rb b/spec/helpers/operations_helper_spec.rb index 857771ebba6..9e50712a386 100644 --- a/spec/helpers/operations_helper_spec.rb +++ b/spec/helpers/operations_helper_spec.rb @@ -32,7 +32,7 @@ RSpec.describe OperationsHelper do expect(subject).to eq( 'alerts_setup_url' => help_page_path('operations/incident_management/integrations.md', anchor: 'configuration'), 'alerts_usage_url' => project_alert_management_index_path(project), - 'prometheus_form_path' => project_integration_path(project, prometheus_integration), + 'prometheus_form_path' => project_settings_integration_path(project, prometheus_integration), 'prometheus_reset_key_path' => reset_alerting_token_project_settings_operations_path(project), 'prometheus_authorization_key' => nil, 'prometheus_api_url' => nil, diff --git a/spec/lib/bulk_imports/projects/pipelines/design_bundle_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/design_bundle_pipeline_spec.rb new file mode 100644 index 00000000000..39b539ece21 --- /dev/null +++ b/spec/lib/bulk_imports/projects/pipelines/design_bundle_pipeline_spec.rb @@ -0,0 +1,171 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Projects::Pipelines::DesignBundlePipeline do + let_it_be(:design) { create(:design, :with_file) } + + let(:portable) { create(:project) } + let(:tmpdir) { Dir.mktmpdir } + let(:design_bundle_path) { File.join(tmpdir, 'design.bundle') } + let(:entity) { create(:bulk_import_entity, :project_entity, project: portable, source_full_path: 'test') } + let(:tracker) { create(:bulk_import_tracker, entity: entity) } + let(:context) { BulkImports::Pipeline::Context.new(tracker) } + + subject(:pipeline) { described_class.new(context) } + + before do + design.repository.bundle_to_disk(design_bundle_path) + + allow(portable).to receive(:lfs_enabled?).and_return(true) + allow(Dir).to receive(:mktmpdir).with('bulk_imports').and_return(tmpdir) + end + + after do + FileUtils.remove_entry(tmpdir) if Dir.exist?(tmpdir) + end + + describe '#run' do + it 'imports design repository into destination project and removes tmpdir' do + allow(pipeline) + .to receive(:extract) + .and_return(BulkImports::Pipeline::ExtractedData.new(data: [design_bundle_path])) + + expect(portable.design_repository).to receive(:create_from_bundle).with(design_bundle_path).and_call_original + + pipeline.run + + expect(portable.design_repository.exists?).to eq(true) + end + end + + describe '#extract' do + it 'downloads & extracts design bundle filepath' do + download_service = instance_double("BulkImports::FileDownloadService") + decompression_service = instance_double("BulkImports::FileDecompressionService") + extraction_service = instance_double("BulkImports::ArchiveExtractionService") + + expect(BulkImports::FileDownloadService) + .to receive(:new) + .with( + configuration: context.configuration, + relative_url: "/#{entity.pluralized_name}/test/export_relations/download?relation=design", + tmpdir: tmpdir, + filename: 'design.tar.gz') + .and_return(download_service) + expect(BulkImports::FileDecompressionService) + .to receive(:new) + .with(tmpdir: tmpdir, filename: 'design.tar.gz') + .and_return(decompression_service) + expect(BulkImports::ArchiveExtractionService) + .to receive(:new) + .with(tmpdir: tmpdir, filename: 'design.tar') + .and_return(extraction_service) + + expect(download_service).to receive(:execute) + expect(decompression_service).to receive(:execute) + expect(extraction_service).to receive(:execute) + + extracted_data = pipeline.extract(context) + + expect(extracted_data.data).to contain_exactly(design_bundle_path) + end + end + + describe '#load' do + before do + allow(pipeline) + .to receive(:extract) + .and_return(BulkImports::Pipeline::ExtractedData.new(data: [design_bundle_path])) + end + + it 'creates design repository from bundle' do + expect(portable.design_repository).to receive(:create_from_bundle).with(design_bundle_path).and_call_original + + pipeline.load(context, design_bundle_path) + + expect(portable.design_repository.exists?).to eq(true) + end + + context 'when lfs is disabled' do + it 'returns' do + allow(portable).to receive(:lfs_enabled?).and_return(false) + + expect(portable.design_repository).not_to receive(:create_from_bundle) + + pipeline.load(context, design_bundle_path) + + expect(portable.design_repository.exists?).to eq(false) + end + end + + context 'when file does not exist' do + it 'returns' do + expect(portable.design_repository).not_to receive(:create_from_bundle) + + pipeline.load(context, File.join(tmpdir, 'bogus')) + + expect(portable.design_repository.exists?).to eq(false) + end + end + + context 'when path is directory' do + it 'returns' do + expect(portable.design_repository).not_to receive(:create_from_bundle) + + pipeline.load(context, tmpdir) + + expect(portable.design_repository.exists?).to eq(false) + end + end + + context 'when path is symlink' do + it 'returns' do + symlink = File.join(tmpdir, 'symlink') + + FileUtils.ln_s(File.join(tmpdir, design_bundle_path), symlink) + + expect(portable.design_repository).not_to receive(:create_from_bundle) + + pipeline.load(context, symlink) + + expect(portable.design_repository.exists?).to eq(false) + end + end + + context 'when path is not under tmpdir' do + it 'returns' do + expect { pipeline.load(context, '/home/test.txt') } + .to raise_error(StandardError, 'path /home/test.txt is not allowed') + end + end + + context 'when path is being traversed' do + it 'raises an error' do + expect { pipeline.load(context, File.join(tmpdir, '..')) } + .to raise_error(Gitlab::Utils::PathTraversalAttackError, 'Invalid path') + end + end + end + + describe '#after_run' do + it 'removes tmpdir' do + allow(FileUtils).to receive(:remove_entry).and_call_original + expect(FileUtils).to receive(:remove_entry).with(tmpdir).and_call_original + + pipeline.after_run(nil) + + expect(Dir.exist?(tmpdir)).to eq(false) + end + + context 'when tmpdir does not exist' do + it 'does not attempt to remove tmpdir' do + FileUtils.remove_entry(tmpdir) + + expect(FileUtils).not_to receive(:remove_entry).with(tmpdir) + + pipeline.after_run(nil) + end + end + end +end diff --git a/spec/lib/bulk_imports/projects/stage_spec.rb b/spec/lib/bulk_imports/projects/stage_spec.rb index e81d9cc5fb4..abfc4e1de04 100644 --- a/spec/lib/bulk_imports/projects/stage_spec.rb +++ b/spec/lib/bulk_imports/projects/stage_spec.rb @@ -28,6 +28,7 @@ RSpec.describe BulkImports::Projects::Stage do [5, BulkImports::Common::Pipelines::WikiPipeline], [5, BulkImports::Common::Pipelines::UploadsPipeline], [5, BulkImports::Common::Pipelines::LfsObjectsPipeline], + [5, BulkImports::Projects::Pipelines::DesignBundlePipeline], [5, BulkImports::Projects::Pipelines::AutoDevopsPipeline], [5, BulkImports::Projects::Pipelines::PipelineSchedulesPipeline], [6, BulkImports::Common::Pipelines::EntityFinisher] diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb index 8b57da8e60b..c2bd20798f1 100644 --- a/spec/lib/gitlab/data_builder/pipeline_spec.rb +++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb @@ -8,11 +8,11 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do let_it_be_with_reload(:pipeline) do create(:ci_pipeline, - project: project, - status: 'success', - sha: project.commit.sha, - ref: project.default_branch, - user: user) + project: project, + status: 'success', + sha: project.commit.sha, + ref: project.default_branch, + user: user) end let!(:build) { create(:ci_build, pipeline: pipeline) } @@ -48,6 +48,7 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do avatar_url: user.avatar_url(only_path: false), email: user.public_email }) + expect(data[:source_pipeline]).to be_nil end context 'build with runner' do @@ -132,6 +133,34 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do end end + context 'when the pipeline has an upstream' do + let(:source_pipeline_attrs) { data[:source_pipeline] } + + shared_examples 'source pipeline attributes' do + it 'has source pipeline attributes', :aggregate_failures do + expect(source_pipeline_attrs[:pipeline_id]).to eq upstream_pipeline.id + expect(source_pipeline_attrs[:job_id]).to eq pipeline.source_bridge.id + expect(source_pipeline_attrs[:project][:id]).to eq upstream_pipeline.project.id + expect(source_pipeline_attrs[:project][:web_url]).to eq upstream_pipeline.project.web_url + expect(source_pipeline_attrs[:project][:path_with_namespace]).to eq upstream_pipeline.project.full_path + end + end + + context 'in same project' do + let!(:upstream_pipeline) { create(:ci_pipeline, upstream_of: pipeline, project: project) } + + it_behaves_like 'source pipeline attributes' + end + + context 'in different project' do + let!(:upstream_pipeline) { create(:ci_pipeline, upstream_of: pipeline) } + + it_behaves_like 'source pipeline attributes' + + it { expect(source_pipeline_attrs[:project][:id]).not_to eq pipeline.project.id } + end + end + context 'avoids N+1 database queries' do it "with multiple builds" do # Preparing the pipeline with the minimal builds diff --git a/spec/lib/gitlab/database/migration_helpers/announce_database_spec.rb b/spec/lib/gitlab/database/migration_helpers/announce_database_spec.rb new file mode 100644 index 00000000000..57c51c9d9c2 --- /dev/null +++ b/spec/lib/gitlab/database/migration_helpers/announce_database_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::MigrationHelpers::AnnounceDatabase do + let(:migration) do + ActiveRecord::Migration.new('MyMigration', 1111).extend(described_class) + end + + describe '#announce' do + it 'prefixes message with database name' do + expect { migration.announce('migrating') }.to output(/^main: == 1111 MyMigration: migrating/).to_stdout + end + end + + describe '#say' do + it 'prefixes message with database name' do + expect { migration.say('transaction_open?()') }.to output(/^main: -- transaction_open?()/).to_stdout + end + + it 'prefixes subitem message with database name' do + expect { migration.say('0.0000s', true) }.to output(/^main: -> 0.0000s/).to_stdout + end + end + + describe '#write' do + it 'does not prefix empty write' do + expect { migration.write }.to output(/^$/).to_stdout + end + end +end diff --git a/spec/lib/gitlab/regex_requires_app_spec.rb b/spec/lib/gitlab/regex_requires_app_spec.rb new file mode 100644 index 00000000000..5808033dc4c --- /dev/null +++ b/spec/lib/gitlab/regex_requires_app_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Only specs that *cannot* be run with fast_spec_helper only +# See regex_spec for tests that do not require the full spec_helper +RSpec.describe Gitlab::Regex do + describe '.debian_architecture_regex' do + subject { described_class.debian_architecture_regex } + + it { is_expected.to match('amd64') } + it { is_expected.to match('kfreebsd-i386') } + + # may not be empty string + it { is_expected.not_to match('') } + # must start with an alphanumeric + it { is_expected.not_to match('-a') } + it { is_expected.not_to match('+a') } + it { is_expected.not_to match('.a') } + it { is_expected.not_to match('_a') } + # only letters, digits and characters '-' + it { is_expected.not_to match('a+b') } + it { is_expected.not_to match('a.b') } + it { is_expected.not_to match('a_b') } + it { is_expected.not_to match('a~') } + it { is_expected.not_to match('aé') } + + # More strict + # Enforce lowercase + it { is_expected.not_to match('AMD64') } + it { is_expected.not_to match('Amd64') } + it { is_expected.not_to match('aMD64') } + end + + describe '.npm_package_name_regex' do + subject { described_class.npm_package_name_regex } + + it { is_expected.to match('@scope/package') } + it { is_expected.to match('unscoped-package') } + it { is_expected.not_to match('@first-scope@second-scope/package') } + it { is_expected.not_to match('scope-without-at-symbol/package') } + it { is_expected.not_to match('@not-a-scoped-package') } + it { is_expected.not_to match('@scope/sub/package') } + it { is_expected.not_to match('@scope/../../package') } + it { is_expected.not_to match('@scope%2e%2e%2fpackage') } + it { is_expected.not_to match('@%2e%2e%2f/package') } + + context 'capturing group' do + [ + ['@scope/package', 'scope'], + ['unscoped-package', nil], + ['@not-a-scoped-package', nil], + ['@scope/sub/package', nil], + ['@inv@lid-scope/package', nil] + ].each do |package_name, extracted_scope_name| + it "extracts the scope name for #{package_name}" do + match = package_name.match(described_class.npm_package_name_regex) + expect(match&.captures&.first).to eq(extracted_scope_name) + end + end + end + end + + describe '.debian_distribution_regex' do + subject { described_class.debian_distribution_regex } + + it { is_expected.to match('buster') } + it { is_expected.to match('buster-updates') } + it { is_expected.to match('Debian10.5') } + + # Do not allow slash, even if this exists in the wild + it { is_expected.not_to match('jessie/updates') } + + # Do not allow Unicode + it { is_expected.not_to match('hé') } + end + + describe '.debian_component_regex' do + subject { described_class.debian_component_regex } + + it { is_expected.to match('main') } + it { is_expected.to match('non-free') } + + # Do not allow slash + it { is_expected.not_to match('non/free') } + + # Do not allow Unicode + it { is_expected.not_to match('hé') } + end +end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index b4c1f3b689b..73d41575c65 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' +require_relative '../../../lib/gitlab/regex' + +# All specs that can be run with fast_spec_helper only +# See regex_requires_app_spec for tests that require the full spec_helper RSpec.describe Gitlab::Regex do shared_examples_for 'project/group name chars regex' do it { is_expected.to match('gitlab-ce') } @@ -401,35 +405,6 @@ RSpec.describe Gitlab::Regex do it { is_expected.not_to match('%2e%2e%2f1.2.3') } end - describe '.npm_package_name_regex' do - subject { described_class.npm_package_name_regex } - - it { is_expected.to match('@scope/package') } - it { is_expected.to match('unscoped-package') } - it { is_expected.not_to match('@first-scope@second-scope/package') } - it { is_expected.not_to match('scope-without-at-symbol/package') } - it { is_expected.not_to match('@not-a-scoped-package') } - it { is_expected.not_to match('@scope/sub/package') } - it { is_expected.not_to match('@scope/../../package') } - it { is_expected.not_to match('@scope%2e%2e%2fpackage') } - it { is_expected.not_to match('@%2e%2e%2f/package') } - - context 'capturing group' do - [ - ['@scope/package', 'scope'], - ['unscoped-package', nil], - ['@not-a-scoped-package', nil], - ['@scope/sub/package', nil], - ['@inv@lid-scope/package', nil] - ].each do |package_name, extracted_scope_name| - it "extracts the scope name for #{package_name}" do - match = package_name.match(described_class.npm_package_name_regex) - expect(match&.captures&.first).to eq(extracted_scope_name) - end - end - end - end - describe '.nuget_version_regex' do subject { described_class.nuget_version_regex } @@ -621,60 +596,6 @@ RSpec.describe Gitlab::Regex do end end - describe '.debian_architecture_regex' do - subject { described_class.debian_architecture_regex } - - it { is_expected.to match('amd64') } - it { is_expected.to match('kfreebsd-i386') } - - # may not be empty string - it { is_expected.not_to match('') } - # must start with an alphanumeric - it { is_expected.not_to match('-a') } - it { is_expected.not_to match('+a') } - it { is_expected.not_to match('.a') } - it { is_expected.not_to match('_a') } - # only letters, digits and characters '-' - it { is_expected.not_to match('a+b') } - it { is_expected.not_to match('a.b') } - it { is_expected.not_to match('a_b') } - it { is_expected.not_to match('a~') } - it { is_expected.not_to match('aé') } - - # More strict - # Enforce lowercase - it { is_expected.not_to match('AMD64') } - it { is_expected.not_to match('Amd64') } - it { is_expected.not_to match('aMD64') } - end - - describe '.debian_distribution_regex' do - subject { described_class.debian_distribution_regex } - - it { is_expected.to match('buster') } - it { is_expected.to match('buster-updates') } - it { is_expected.to match('Debian10.5') } - - # Do not allow slash, even if this exists in the wild - it { is_expected.not_to match('jessie/updates') } - - # Do not allow Unicode - it { is_expected.not_to match('hé') } - end - - describe '.debian_component_regex' do - subject { described_class.debian_component_regex } - - it { is_expected.to match('main') } - it { is_expected.to match('non-free') } - - # Do not allow slash - it { is_expected.not_to match('non/free') } - - # Do not allow Unicode - it { is_expected.not_to match('hé') } - end - describe '.helm_channel_regex' do subject { described_class.helm_channel_regex } @@ -1020,4 +941,29 @@ RSpec.describe Gitlab::Regex do it { is_expected.not_to match('a' * 63 + '#') } it { is_expected.not_to match('') } end + + describe '.sep_by_1' do + subject { %r{\A #{described_class.sep_by_1(/\.+/, /[abcdef]{3}/)} \z}x } + + it { is_expected.to match('abc') } + it { is_expected.to match('abc.def') } + it { is_expected.to match('abc.def.caf') } + it { is_expected.to match('abc..def') } + it { is_expected.to match('abc..def..caf') } + it { is_expected.to match('abc...def') } + it { is_expected.to match('abc....def........caf') } + it { is_expected.to match((['abc'] * 100).join('.')) } + + it { is_expected.not_to match('') } + it { is_expected.not_to match('a') } + it { is_expected.not_to match('aaaa') } + it { is_expected.not_to match('foo') } + it { is_expected.not_to match('.abc') } + it { is_expected.not_to match('abc.') } + it { is_expected.not_to match('.abc.def') } + it { is_expected.not_to match('abc.def.') } + it { is_expected.not_to match('abc.defe.caf') } + it { is_expected.not_to match('abc!abc') } + it { is_expected.not_to match((['abc'] * 100).join('.') + '!') } + end end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/jira_imports_total_imported_issues_count_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/jira_imports_total_imported_issues_count_metric_spec.rb new file mode 100644 index 00000000000..f7a53cd3dcc --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/jira_imports_total_imported_issues_count_metric_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::Instrumentations::JiraImportsTotalImportedIssuesCountMetric do + let_it_be(:jira_import_state_1) { create(:jira_import_state, :finished, imported_issues_count: 3) } + let_it_be(:jira_import_state_2) { create(:jira_import_state, :finished, imported_issues_count: 2) } + + let(:expected_value) { 5 } + let(:expected_query) do + 'SELECT SUM("jira_imports"."imported_issues_count") FROM "jira_imports" WHERE "jira_imports"."status" = 4' + end + + it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all' } +end diff --git a/spec/presenters/service_hook_presenter_spec.rb b/spec/presenters/service_hook_presenter_spec.rb index 25ded17fb34..c7703593327 100644 --- a/spec/presenters/service_hook_presenter_spec.rb +++ b/spec/presenters/service_hook_presenter_spec.rb @@ -12,7 +12,7 @@ RSpec.describe ServiceHookPresenter do subject { service_hook.present.logs_details_path(web_hook_log) } let(:expected_path) do - "/#{project.namespace.path}/#{project.name}/-/integrations/#{integration.to_param}/hook_logs/#{web_hook_log.id}" + "/#{project.namespace.path}/#{project.name}/-/settings/integrations/#{integration.to_param}/hook_logs/#{web_hook_log.id}" end it { is_expected.to eq(expected_path) } @@ -22,7 +22,7 @@ RSpec.describe ServiceHookPresenter do subject { service_hook.present.logs_retry_path(web_hook_log) } let(:expected_path) do - "/#{project.namespace.path}/#{project.name}/-/integrations/#{integration.to_param}/hook_logs/#{web_hook_log.id}/retry" + "/#{project.namespace.path}/#{project.name}/-/settings/integrations/#{integration.to_param}/hook_logs/#{web_hook_log.id}/retry" end it { is_expected.to eq(expected_path) } diff --git a/spec/presenters/web_hook_log_presenter_spec.rb b/spec/presenters/web_hook_log_presenter_spec.rb index 5827f3378de..188737e0fb6 100644 --- a/spec/presenters/web_hook_log_presenter_spec.rb +++ b/spec/presenters/web_hook_log_presenter_spec.rb @@ -21,7 +21,7 @@ RSpec.describe WebHookLogPresenter do let(:web_hook) { create(:service_hook, integration: integration) } let(:integration) { create(:drone_ci_integration, project: project) } - it { is_expected.to eq(project_integration_hook_log_path(project, integration, web_hook_log)) } + it { is_expected.to eq(project_settings_integration_hook_log_path(project, integration, web_hook_log)) } end end @@ -41,7 +41,7 @@ RSpec.describe WebHookLogPresenter do let(:web_hook) { create(:service_hook, integration: integration) } let(:integration) { create(:drone_ci_integration, project: project) } - it { is_expected.to eq(retry_project_integration_hook_log_path(project, integration, web_hook_log)) } + it { is_expected.to eq(retry_project_settings_integration_hook_log_path(project, integration, web_hook_log)) } end end end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index e1e77b24202..47fd1622306 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -772,6 +772,58 @@ RSpec.describe 'project routing' do end end + describe Projects::Settings::IntegrationsController, 'routing' do + it 'to #index' do + expect(get('/gitlab/gitlabhq/-/settings/integrations')).to route_to('projects/settings/integrations#index', namespace_id: 'gitlab', project_id: 'gitlabhq') + end + + it 'to #edit' do + expect(get('/gitlab/gitlabhq/-/settings/integrations/acme/edit')).to route_to('projects/settings/integrations#edit', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'acme') + end + + it 'to #update' do + expect(put('/gitlab/gitlabhq/-/settings/integrations/acme')).to route_to('projects/settings/integrations#update', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'acme') + end + + it 'to #test' do + expect(put('/gitlab/gitlabhq/-/settings/integrations/acme/test')).to route_to('projects/settings/integrations#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'acme') + end + + context 'legacy routes' do + it 'to #edit' do + expect(get('/gitlab/gitlabhq/-/integrations/acme/edit')).to route_to('projects/settings/integrations#edit', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'acme') + end + + it 'to #update' do + expect(put('/gitlab/gitlabhq/-/integrations/acme')).to route_to('projects/settings/integrations#update', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'acme') + end + + it 'to #test' do + expect(put('/gitlab/gitlabhq/-/integrations/acme/test')).to route_to('projects/settings/integrations#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'acme') + end + end + end + + describe Projects::Settings::IntegrationHookLogsController do + it 'to #show' do + expect(get('/gitlab/gitlabhq/-/settings/integrations/acme/hook_logs/log')).to route_to('projects/settings/integration_hook_logs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', integration_id: 'acme', id: 'log') + end + + it 'to #retry' do + expect(post('/gitlab/gitlabhq/-/settings/integrations/acme/hook_logs/log/retry')).to route_to('projects/settings/integration_hook_logs#retry', namespace_id: 'gitlab', project_id: 'gitlabhq', integration_id: 'acme', id: 'log') + end + + context 'legacy routes' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/-/integrations/acme/hook_logs/log')).to route_to('projects/settings/integration_hook_logs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', integration_id: 'acme', id: 'log') + end + + it 'to #retry' do + expect(post('/gitlab/gitlabhq/-/integrations/acme/hook_logs/log/retry')).to route_to('projects/settings/integration_hook_logs#retry', namespace_id: 'gitlab', project_id: 'gitlabhq', integration_id: 'acme', id: 'log') + end + end + end + describe Projects::TemplatesController, 'routing' do describe '#show' do def show_with_template_type(template_type) diff --git a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb index efea6db8a43..39fc6c94793 100644 --- a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb +++ b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb @@ -419,7 +419,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do static: |- <p data-sourcepos="1:1-1:20" dir="auto"><del>Hi</del> Hello, world!</p> wysiwyg: |- - <p>~~Hi~~ Hello, world!</p> + <p><s>Hi</s> Hello, world!</p> 03_01__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001: canonical: | <p><strong>bold</strong></p> @@ -475,7 +475,16 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do "content": [ { "type": "text", - "text": "~~Hi~~ Hello, world!" + "marks": [ + { + "type": "strike" + } + ], + "text": "Hi" + }, + { + "type": "text", + "text": " Hello, world!" } ] } diff --git a/spec/views/projects/services/edit.html.haml_spec.rb b/spec/views/projects/settings/integrations/edit.html.haml_spec.rb index 372ccf82a68..5f3c45166ef 100644 --- a/spec/views/projects/services/edit.html.haml_spec.rb +++ b/spec/views/projects/settings/integrations/edit.html.haml_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'projects/services/edit' do +RSpec.describe 'projects/settings/integrations/edit' do let(:integration) { create(:drone_ci_integration, project: project) } let(:project) { create(:project) } diff --git a/yarn.lock b/yarn.lock index e27860a6734..a47703bad9b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3342,6 +3342,11 @@ catharsis@~0.8.9: dependencies: underscore-contrib "~0.3.0" +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -5233,6 +5238,11 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escape-string-regexp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== + escodegen@^1.14.1: version "1.14.3" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" @@ -8223,6 +8233,11 @@ lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +longest-streak@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.0.1.tgz#c97315b7afa0e7d9525db9a5a2953651432bdc5d" + integrity sha512-cHlYSUpL2s7Fb3394mYxwTYj8niTaNHUCLr0qdiCXQfSjfuA7CKofpX2uSwEfFDQ0EB7JcnMnm+GjbqqoinYYg== + loose-envify@^1.0.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -8344,6 +8359,11 @@ markdown-it@12.3.2, markdown-it@^12.0.0: mdurl "^1.0.1" uc.micro "^1.0.5" +markdown-table@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.2.tgz#9b59eb2c1b22fe71954a65ff512887065a7bb57c" + integrity sha512-y8j3a5/DkJCmS5x4dMCQL+OR0+2EAq3DOtio1COSHsmW2BGXnNCK3v12hJt1LrUz5iZH5g0LmuYOjDdI+czghA== + markdownlint-cli@0.31.0: version "0.31.0" resolved "https://registry.yarnpkg.com/markdownlint-cli/-/markdownlint-cli-0.31.0.tgz#a44264a71066475228292b7af19d3d18b827676d" @@ -8413,6 +8433,15 @@ mdast-util-definitions@^5.0.0: "@types/unist" "^2.0.0" unist-util-visit "^3.0.0" +mdast-util-find-and-replace@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.1.0.tgz#69728acd250749f8aac6e150e07d1fd15619e829" + integrity sha512-1w1jbqAd13oU78QPBf5223+xB+37ecNtQ1JElq2feWols5oEYAl+SgNDnOZipe7NfLemoEt362yUS15/wip4mw== + dependencies: + escape-string-regexp "^5.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^4.0.0" + mdast-util-from-markdown@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.2.0.tgz#84df2924ccc6c995dec1e2368b2b208ad0a76268" @@ -8431,6 +8460,63 @@ mdast-util-from-markdown@^1.0.0: unist-util-stringify-position "^3.0.0" uvu "^0.5.0" +mdast-util-gfm-autolink-literal@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.2.tgz#4032dcbaddaef7d4f2f3768ed830475bb22d3970" + integrity sha512-FzopkOd4xTTBeGXhXSBU0OCDDh5lUj2rd+HQqG92Ld+jL4lpUfgX2AT2OHAVP9aEeDKp7G92fuooSZcYJA3cRg== + dependencies: + "@types/mdast" "^3.0.0" + ccount "^2.0.0" + mdast-util-find-and-replace "^2.0.0" + micromark-util-character "^1.0.0" + +mdast-util-gfm-footnote@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.1.tgz#11d2d40a1a673a399c459e467fa85e00223191fe" + integrity sha512-p+PrYlkw9DeCRkTVw1duWqPRHX6Ywh2BNKJQcZbCwAuP/59B0Lk9kakuAd7KbQprVO4GzdW8eS5++A9PUSqIyw== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + micromark-util-normalize-identifier "^1.0.0" + +mdast-util-gfm-strikethrough@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.1.tgz#a4a74c36864ec6a6e3bbd31e1977f29beb475789" + integrity sha512-zKJbEPe+JP6EUv0mZ0tQUyLQOC+FADt0bARldONot/nefuISkaZFlmVK4tU6JgfyZGrky02m/I6PmehgAgZgqg== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + +mdast-util-gfm-table@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.4.tgz#0dbb25f04fd9c0877dc63b76203ecbdf5d945755" + integrity sha512-aEuoPwZyP4iIMkf2cLWXxx3EQ6Bmh2yKy9MVCg4i6Sd3cX80dcLEfXO/V4ul3pGH9czBK4kp+FAl+ZHmSUt9/w== + dependencies: + markdown-table "^3.0.0" + mdast-util-from-markdown "^1.0.0" + mdast-util-to-markdown "^1.3.0" + +mdast-util-gfm-task-list-item@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.1.tgz#6f35f09c6e2bcbe88af62fdea02ac199cc802c5c" + integrity sha512-KZ4KLmPdABXOsfnM6JHUIjxEvcx2ulk656Z/4Balw071/5qgnhz+H1uGtf2zIGnrnvDC8xR4Fj9uKbjAFGNIeA== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + +mdast-util-gfm@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-2.0.1.tgz#16fcf70110ae689a06d77e8f4e346223b64a0ea6" + integrity sha512-42yHBbfWIFisaAfV1eixlabbsa6q7vHeSPY+cg+BBjX51M8xhgMacqH9g6TftB/9+YkcI0ooV4ncfrJslzm/RQ== + dependencies: + mdast-util-from-markdown "^1.0.0" + mdast-util-gfm-autolink-literal "^1.0.0" + mdast-util-gfm-footnote "^1.0.0" + mdast-util-gfm-strikethrough "^1.0.0" + mdast-util-gfm-table "^1.0.0" + mdast-util-gfm-task-list-item "^1.0.0" + mdast-util-to-markdown "^1.0.0" + mdast-util-to-hast@^12.1.0: version "12.1.1" resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.1.1.tgz#89a2bb405eaf3b05eb8bf45157678f35eef5dbca" @@ -8447,7 +8533,20 @@ mdast-util-to-hast@^12.1.0: unist-util-position "^4.0.0" unist-util-visit "^4.0.0" -mdast-util-to-string@^3.1.0: +mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-1.3.0.tgz#38b6cdc8dc417de642a469c4fc2abdf8c931bd1e" + integrity sha512-6tUSs4r+KK4JGTTiQ7FfHmVOaDrLQJPmpjD6wPMlHGUVXoG9Vjc3jIeP+uyBWRf8clwB2blM+W7+KrlMYQnftA== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + longest-streak "^3.0.0" + mdast-util-to-string "^3.0.0" + micromark-util-decode-string "^1.0.0" + unist-util-visit "^4.0.0" + zwitch "^2.0.0" + +mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz#56c506d065fbf769515235e577b5a261552d56e9" integrity sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA== @@ -8555,7 +8654,7 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= -micromark-core-commonmark@^1.0.1: +micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1: version "1.0.6" resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz#edff4c72e5993d93724a3c206970f5a15b0585ad" integrity sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA== @@ -8577,6 +8676,86 @@ micromark-core-commonmark@^1.0.1: micromark-util-types "^1.0.1" uvu "^0.5.0" +micromark-extension-gfm-autolink-literal@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.3.tgz#dc589f9c37eaff31a175bab49f12290edcf96058" + integrity sha512-i3dmvU0htawfWED8aHMMAzAVp/F0Z+0bPh3YrbTPPL1v4YAlCZpy5rBO5p0LPYiZo0zFVkoYh7vDU7yQSiCMjg== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-extension-gfm-footnote@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.0.4.tgz#cbfd8873b983e820c494498c6dac0105920818d5" + integrity sha512-E/fmPmDqLiMUP8mLJ8NbJWJ4bTw6tS+FEQS8CcuDtZpILuOb2kjLqPEeAePF1djXROHXChM/wPJw0iS4kHCcIg== + dependencies: + micromark-core-commonmark "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-extension-gfm-strikethrough@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.4.tgz#162232c284ffbedd8c74e59c1525bda217295e18" + integrity sha512-/vjHU/lalmjZCT5xt7CcHVJGq8sYRm80z24qAKXzaHzem/xsDYb2yLL+NNVbYvmpLx3O7SYPuGL5pzusL9CLIQ== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-classify-character "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-extension-gfm-table@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.5.tgz#7b708b728f8dc4d95d486b9e7a2262f9cddbcbb4" + integrity sha512-xAZ8J1X9W9K3JTJTUL7G6wSKhp2ZYHrFk5qJgY/4B33scJzE2kpfRL6oiw/veJTbt7jiM/1rngLlOKPWr1G+vg== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-extension-gfm-tagfilter@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.1.tgz#fb2e303f7daf616db428bb6a26e18fda14a90a4d" + integrity sha512-Ty6psLAcAjboRa/UKUbbUcwjVAv5plxmpUTy2XC/3nJFL37eHej8jrHrRzkqcpipJliuBH30DTs7+3wqNcQUVA== + dependencies: + micromark-util-types "^1.0.0" + +micromark-extension-gfm-task-list-item@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.3.tgz#7683641df5d4a09795f353574d7f7f66e47b7fc4" + integrity sha512-PpysK2S1Q/5VXi72IIapbi/jliaiOFzv7THH4amwXeYXLq3l1uo8/2Be0Ac1rEwK20MQEsGH2ltAZLNY2KI/0Q== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-extension-gfm@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-2.0.1.tgz#40f3209216127a96297c54c67f5edc7ef2d1a2a2" + integrity sha512-p2sGjajLa0iYiGQdT0oelahRYtMWvLjy8J9LOCxzIQsllMCGLbsLW+Nc+N4vi02jcRJvedVJ68cjelKIO6bpDA== + dependencies: + micromark-extension-gfm-autolink-literal "^1.0.0" + micromark-extension-gfm-footnote "^1.0.0" + micromark-extension-gfm-strikethrough "^1.0.0" + micromark-extension-gfm-table "^1.0.0" + micromark-extension-gfm-tagfilter "^1.0.0" + micromark-extension-gfm-task-list-item "^1.0.0" + micromark-util-combine-extensions "^1.0.0" + micromark-util-types "^1.0.0" + micromark-factory-destination@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz#fef1cb59ad4997c496f887b6977aa3034a5a277e" @@ -10493,6 +10672,16 @@ rehype-raw@^6.1.1: hast-util-raw "^7.2.0" unified "^10.0.0" +remark-gfm@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-3.0.1.tgz#0b180f095e3036545e9dddac0e8df3fa5cfee54f" + integrity sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-gfm "^2.0.0" + micromark-extension-gfm "^2.0.0" + unified "^10.0.0" + remark-parse@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.1.tgz#6f60ae53edbf0cf38ea223fe643db64d112e0775" |