diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-06 12:18:56 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-06 12:18:56 +0300 |
commit | c06178d51ad9b8d4ce665047873615facfc9c1c5 (patch) | |
tree | 2a1acfea73fd0fbdd7d0e240fd7ac52224845f99 | |
parent | 0a0e8803b0e3e2fb83d74c9bafc32f4e9d825bcc (diff) |
Add latest changes from gitlab-org/gitlab@master
123 files changed, 1549 insertions, 1250 deletions
diff --git a/app/assets/javascripts/pages/projects/new/components/app.vue b/app/assets/javascripts/pages/projects/new/components/app.vue new file mode 100644 index 00000000000..60a4fbc3e6b --- /dev/null +++ b/app/assets/javascripts/pages/projects/new/components/app.vue @@ -0,0 +1,148 @@ +<script> +import createFromTemplateIllustration from '@gitlab/svgs/dist/illustrations/project-create-from-template-sm.svg'; +import blankProjectIllustration from '@gitlab/svgs/dist/illustrations/project-create-new-sm.svg'; +import importProjectIllustration from '@gitlab/svgs/dist/illustrations/project-import-sm.svg'; +import ciCdProjectIllustration from '@gitlab/svgs/dist/illustrations/project-run-CICD-pipelines-sm.svg'; +import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; +import { experiment } from '~/experimentation/utils'; +import { s__ } from '~/locale'; +import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue'; +import NewProjectPushTipPopover from './new_project_push_tip_popover.vue'; + +const NEW_REPO_EXPERIMENT = 'new_repo'; +const CI_CD_PANEL = 'cicd_for_external_repo'; +const PANELS = [ + { + key: 'blank', + name: 'blank_project', + selector: '#blank-project-pane', + title: s__('ProjectsNew|Create blank project'), + description: s__( + 'ProjectsNew|Create a blank project to house your files, plan your work, and collaborate on code, among other things.', + ), + illustration: blankProjectIllustration, + }, + { + key: 'template', + name: 'create_from_template', + selector: '#create-from-template-pane', + title: s__('ProjectsNew|Create from template'), + description: s__( + 'ProjectsNew|Create a project pre-populated with the necessary files to get you started quickly.', + ), + illustration: createFromTemplateIllustration, + }, + { + key: 'import', + name: 'import_project', + selector: '#import-project-pane', + title: s__('ProjectsNew|Import project'), + description: s__( + 'ProjectsNew|Migrate your data from an external source like GitHub, Bitbucket, or another instance of GitLab.', + ), + illustration: importProjectIllustration, + }, + { + key: 'ci', + name: CI_CD_PANEL, + selector: '#ci-cd-project-pane', + title: s__('ProjectsNew|Run CI/CD for external repository'), + description: s__('ProjectsNew|Connect your external repository to GitLab CI/CD.'), + illustration: ciCdProjectIllustration, + }, +]; + +export default { + components: { + NewNamespacePage, + NewProjectPushTipPopover, + }, + directives: { + SafeHtml, + }, + props: { + hasErrors: { + type: Boolean, + required: false, + default: false, + }, + isCiCdAvailable: { + type: Boolean, + required: false, + default: false, + }, + newProjectGuidelines: { + type: String, + required: false, + default: '', + }, + }, + + computed: { + decoratedPanels() { + const PANEL_TITLES = experiment(NEW_REPO_EXPERIMENT, { + use: () => ({ + blank: s__('ProjectsNew|Create blank project'), + import: s__('ProjectsNew|Import project'), + }), + try: () => ({ + blank: s__('ProjectsNew|Create blank project/repository'), + import: s__('ProjectsNew|Import project/repository'), + }), + }); + + return PANELS.map(({ key, title, ...el }) => ({ + ...el, + title: PANEL_TITLES[key] ?? title, + })); + }, + + availablePanels() { + return this.isCiCdAvailable + ? this.decoratedPanels + : this.decoratedPanels.filter((p) => p.name !== CI_CD_PANEL); + }, + }, + + methods: { + resetProjectErrors() { + const errorsContainer = document.querySelector('.project-edit-errors'); + if (errorsContainer) { + errorsContainer.innerHTML = ''; + } + }, + }, + EXPERIMENT: NEW_REPO_EXPERIMENT, +}; +</script> + +<template> + <new-namespace-page + :initial-breadcrumb="s__('New project')" + :panels="availablePanels" + :jump-to-last-persisted-panel="hasErrors" + :title="s__('ProjectsNew|Create new project')" + :experiment="$options.EXPERIMENT" + persistence-key="new_project_last_active_tab" + @panel-change="resetProjectErrors" + > + <template #extra-description> + <div + v-if="newProjectGuidelines" + id="new-project-guideline" + v-safe-html="newProjectGuidelines" + ></div> + </template> + <template #welcome-footer> + <div class="gl-pt-5 gl-text-center"> + <p> + {{ __('You can also create a project from the command line.') }} + <a ref="clipTip" href="#" @click.prevent> + {{ __('Show command') }} + </a> + <new-project-push-tip-popover :target="() => $refs.clipTip" /> + </p> + </div> + </template> + </new-namespace-page> +</template> diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/new_project_push_tip_popover.vue b/app/assets/javascripts/pages/projects/new/components/new_project_push_tip_popover.vue index e42d9154866..e42d9154866 100644 --- a/app/assets/javascripts/projects/experiment_new_project_creation/components/new_project_push_tip_popover.vue +++ b/app/assets/javascripts/pages/projects/new/components/new_project_push_tip_popover.vue diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js index e10e2872dce..f469c56e808 100644 --- a/app/assets/javascripts/pages/projects/new/index.js +++ b/app/assets/javascripts/pages/projects/new/index.js @@ -1,28 +1,44 @@ -import { deprecatedCreateFlash as createFlash } from '~/flash'; -import { __ } from '~/locale'; +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; import initProjectVisibilitySelector from '../../../project_visibility'; import initProjectNew from '../../../projects/project_new'; +import NewProjectCreationApp from './components/app.vue'; initProjectVisibilitySelector(); initProjectNew.bindEvents(); -import( - /* webpackChunkName: 'experiment_new_project_creation' */ '../../../projects/experiment_new_project_creation' -) - .then((m) => { - const el = document.querySelector('.js-experiment-new-project-creation'); +function initNewProjectCreation(el) { + const { + pushToCreateProjectCommand, + workingWithProjectsHelpPath, + newProjectGuidelines, + hasErrors, + isCiCdAvailable, + } = el.dataset; - if (!el) { - return; - } + const props = { + hasErrors: parseBoolean(hasErrors), + isCiCdAvailable: parseBoolean(isCiCdAvailable), + newProjectGuidelines, + }; - const config = { - hasErrors: 'hasErrors' in el.dataset, - isCiCdAvailable: 'isCiCdAvailable' in el.dataset, - newProjectGuidelines: el.dataset.newProjectGuidelines, - }; - m.default(el, config); - }) - .catch(() => { - createFlash(__('An error occurred while loading project creation UI')); + const provide = { + workingWithProjectsHelpPath, + pushToCreateProjectCommand, + }; + + return new Vue({ + el, + components: { + NewProjectCreationApp, + }, + provide, + render(h) { + return h(NewProjectCreationApp, { props }); + }, }); +} + +const el = document.querySelector('.js-new-project-creation'); + +initNewProjectCreation(el); diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue index a255e5cc72d..af5f77e2d64 100644 --- a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue +++ b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue @@ -1,5 +1,7 @@ <script> import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlIcon } from '@gitlab/ui'; +import { historyPushState } from '~/lib/utils/common_utils'; +import { setUrlParams } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; import { DEFAULT_FAILURE } from '~/pipeline_editor/constants'; import getAvailableBranches from '~/pipeline_editor/graphql/queries/available_branches.graphql'; @@ -55,6 +57,9 @@ export default { data: { currentBranch: newBranch }, }); + const updatedPath = setUrlParams({ branch_name: newBranch }); + historyPushState(updatedPath); + this.$emit('refetchContent'); }, }, diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue deleted file mode 100644 index 1060b37067e..00000000000 --- a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue +++ /dev/null @@ -1,201 +0,0 @@ -<script> -/* eslint-disable vue/no-v-html */ -import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; -import { experiment } from '~/experimentation/utils'; -import { __, s__ } from '~/locale'; -import { NEW_REPO_EXPERIMENT } from '../constants'; -import blankProjectIllustration from '../illustrations/blank-project.svg'; -import ciCdProjectIllustration from '../illustrations/ci-cd-project.svg'; -import createFromTemplateIllustration from '../illustrations/create-from-template.svg'; -import importProjectIllustration from '../illustrations/import-project.svg'; -import LegacyContainer from './legacy_container.vue'; -import WelcomePage from './welcome.vue'; - -const BLANK_PANEL = 'blank_project'; -const CI_CD_PANEL = 'cicd_for_external_repo'; -const LAST_ACTIVE_TAB_KEY = 'new_project_last_active_tab'; - -const PANELS = [ - { - key: 'blank', - name: BLANK_PANEL, - selector: '#blank-project-pane', - title: s__('ProjectsNew|Create blank project'), - description: s__( - 'ProjectsNew|Create a blank project to house your files, plan your work, and collaborate on code, among other things.', - ), - illustration: blankProjectIllustration, - }, - { - key: 'template', - name: 'create_from_template', - selector: '#create-from-template-pane', - title: s__('ProjectsNew|Create from template'), - description: s__( - 'Create a project pre-populated with the necessary files to get you started quickly.', - ), - illustration: createFromTemplateIllustration, - }, - { - key: 'import', - name: 'import_project', - selector: '#import-project-pane', - title: s__('ProjectsNew|Import project'), - description: s__( - 'Migrate your data from an external source like GitHub, Bitbucket, or another instance of GitLab.', - ), - illustration: importProjectIllustration, - }, - { - key: 'ci', - name: CI_CD_PANEL, - selector: '#ci-cd-project-pane', - title: s__('ProjectsNew|Run CI/CD for external repository'), - description: s__('ProjectsNew|Connect your external repository to GitLab CI/CD.'), - illustration: ciCdProjectIllustration, - }, -]; - -export default { - components: { - GlBreadcrumb, - GlIcon, - WelcomePage, - LegacyContainer, - }, - directives: { - SafeHtml, - }, - props: { - hasErrors: { - type: Boolean, - required: false, - default: false, - }, - isCiCdAvailable: { - type: Boolean, - required: false, - default: false, - }, - newProjectGuidelines: { - type: String, - required: false, - default: '', - }, - }, - - data() { - return { - activeTab: null, - }; - }, - - computed: { - decoratedPanels() { - const PANEL_TITLES = experiment(NEW_REPO_EXPERIMENT, { - use: () => ({ - blank: s__('ProjectsNew|Create blank project'), - import: s__('ProjectsNew|Import project'), - }), - try: () => ({ - blank: s__('ProjectsNew|Create blank project/repository'), - import: s__('ProjectsNew|Import project/repository'), - }), - }); - - return PANELS.map(({ key, title, ...el }) => ({ - ...el, - title: PANEL_TITLES[key] !== undefined ? PANEL_TITLES[key] : title, - })); - }, - - availablePanels() { - if (this.isCiCdAvailable) { - return this.decoratedPanels; - } - - return this.decoratedPanels.filter((p) => p.name !== CI_CD_PANEL); - }, - - activePanel() { - return this.decoratedPanels.find((p) => p.name === this.activeTab); - }, - - breadcrumbs() { - if (!this.activeTab || !this.activePanel) { - return null; - } - - return [ - { text: __('New project'), href: '#' }, - { text: this.activePanel.title, href: `#${this.activeTab}` }, - ]; - }, - }, - - created() { - this.handleLocationHashChange(); - - if (this.hasErrors) { - this.activeTab = localStorage.getItem(LAST_ACTIVE_TAB_KEY) || BLANK_PANEL; - } - - window.addEventListener('hashchange', () => { - this.handleLocationHashChange(); - this.resetProjectErrors(); - }); - this.$root.$on('clicked::link', (e) => { - window.location = e.target.href; - }); - }, - - methods: { - resetProjectErrors() { - const errorsContainer = document.querySelector('.project-edit-errors'); - if (errorsContainer) { - errorsContainer.innerHTML = ''; - } - }, - - handleLocationHashChange() { - this.activeTab = window.location.hash.substring(1) || null; - if (this.activeTab) { - localStorage.setItem(LAST_ACTIVE_TAB_KEY, this.activeTab); - } - }, - }, - - PANELS, -}; -</script> - -<template> - <welcome-page v-if="activeTab === null" :panels="availablePanels" /> - <div v-else class="row"> - <div class="col-lg-3"> - <div class="gl-text-white" v-html="activePanel.illustration"></div> - <h4>{{ activePanel.title }}</h4> - <p>{{ activePanel.description }}</p> - <div - v-if="newProjectGuidelines" - id="new-project-guideline" - v-safe-html="newProjectGuidelines" - ></div> - </div> - <div class="col-lg-9"> - <gl-breadcrumb v-if="breadcrumbs" :items="breadcrumbs"> - <template #separator> - <gl-icon name="chevron-right" :size="8" /> - </template> - </gl-breadcrumb> - <template v-for="panel in $options.PANELS"> - <legacy-container - v-if="activeTab === panel.name" - :key="panel.name" - class="gl-mt-3" - :selector="panel.selector" - /> - </template> - </div> - </div> -</template> diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue b/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue deleted file mode 100644 index d342ce4c9c2..00000000000 --- a/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue +++ /dev/null @@ -1,66 +0,0 @@ -<script> -/* eslint-disable vue/no-v-html */ -import Tracking from '~/tracking'; -import { NEW_REPO_EXPERIMENT } from '../constants'; -import NewProjectPushTipPopover from './new_project_push_tip_popover.vue'; - -const trackingMixin = Tracking.mixin({ ...gon.tracking_data, experiment: NEW_REPO_EXPERIMENT }); - -export default { - components: { - NewProjectPushTipPopover, - }, - mixins: [trackingMixin], - props: { - panels: { - type: Array, - required: true, - }, - }, -}; -</script> -<template> - <div class="container"> - <div class="blank-state-welcome"> - <h2 class="blank-state-welcome-title gl-mt-5! gl-mb-3!"> - {{ s__('ProjectsNew|Create new project') }} - </h2> - <p div class="blank-state-text"> </p> - </div> - <div class="row blank-state-row"> - <a - v-for="panel in panels" - :key="panel.name" - :href="`#${panel.name}`" - :data-qa-selector="`${panel.name}_link`" - class="blank-state blank-state-link experiment-new-project-page-blank-state" - @click="track('click_tab', { label: panel.name })" - > - <div class="blank-state-icon gl-text-white" v-html="panel.illustration"></div> - <div class="blank-state-body gl-pl-4!"> - <h3 class="blank-state-title experiment-new-project-page-blank-state-title"> - {{ panel.title }} - </h3> - <p class="blank-state-text"> - {{ panel.description }} - </p> - </div> - </a> - </div> - <div class="blank-state-welcome"> - <p> - {{ __('You can also create a project from the command line.') }} - <a - ref="clipTip" - href="#" - click.prevent - class="push-new-project-tip" - rel="noopener noreferrer" - > - {{ __('Show command') }} - </a> - <new-project-push-tip-popover :target="() => $refs.clipTip" /> - </p> - </div> - </div> -</template> diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/constants.js b/app/assets/javascripts/projects/experiment_new_project_creation/constants.js deleted file mode 100644 index 402ca887cf1..00000000000 --- a/app/assets/javascripts/projects/experiment_new_project_creation/constants.js +++ /dev/null @@ -1 +0,0 @@ -export const NEW_REPO_EXPERIMENT = 'new_repo'; diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/blank-project.svg b/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/blank-project.svg deleted file mode 100644 index f73ae70dba8..00000000000 --- a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/blank-project.svg +++ /dev/null @@ -1,9 +0,0 @@ -<svg width="82" height="80" viewBox="0 0 82 80" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M66.1912 8.19118H77.6176C78.2755 8.19118 78.8088 8.72448 78.8088 9.38235V69.6176C78.8088 70.2755 78.2755 70.8088 77.6176 70.8088H66.1912V8.19118Z" fill="#F0F0F0" stroke="#DBDBDB" stroke-width="2.38235"/> -<path d="M22.0517 19.2723L22.0094 10.1001C22.004 8.92546 22.8555 7.92221 24.0153 7.73664L63.3613 1.44139C64.8087 1.2098 66.12 2.32794 66.12 3.79382V75.8717C66.12 77.3323 64.8177 78.449 63.3742 78.2262L24.3037 72.1952C23.1461 72.0165 22.2902 71.023 22.2848 69.8517L22.2428 60.7554" stroke="#DBDBDB" stroke-width="2.38235"/> -<circle cx="23" cy="40" r="21" stroke="#6E49CB" stroke-width="2.38235"/> -<circle cx="23" cy="40" r="17" fill="#6E49CB"/> -<circle cx="23" cy="40" r="17" fill="white" fill-opacity="0.9"/> -<path d="M22.3125 48V33.3659" stroke="#6E49CB" stroke-width="2.38235" stroke-linecap="round"/> -<path d="M15 40.3049H30" stroke="#6E49CB" stroke-width="2.38235" stroke-linecap="round"/> -</svg> diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/ci-cd-project.svg b/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/ci-cd-project.svg deleted file mode 100644 index 8d6cf58f196..00000000000 --- a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/ci-cd-project.svg +++ /dev/null @@ -1,23 +0,0 @@ -<svg width="169" height="78" viewBox="0 0 169 78" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M115.571 41.5714L147.714 41.5714C158.365 41.5714 167 32.9369 167 22.2857C167 11.6345 158.365 3 147.714 3C137.063 3 128.429 11.6345 128.429 22.2857C128.429 27.3128 130.352 31.8907 133.503 35.3235" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/> -<path d="M115.107 41.5714H125.786C133.084 41.5714 139 47.4877 139 54.7857C139 62.0838 133.084 68 125.786 68C118.488 68 112.571 62.0838 112.571 54.7857C112.571 53.039 112.91 51.3715 113.526 49.8453" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/> -<path d="M87.5486 37H76.3943C75.6243 37 75 36.3746 75 35.6032C75 34.8318 75.6243 34.2064 76.3943 34.2064H87.5486C88.3187 34.2064 88.9429 34.8318 88.9429 35.6032C88.9429 36.3746 88.3187 37 87.5486 37Z" fill="#FC6D26"/> -<path d="M118.703 37H96.3943C95.6243 37 95 36.3746 95 35.6032C95 34.8318 95.6243 34.2064 96.3943 34.2064H118.703C119.473 34.2064 120.097 34.8318 120.097 35.6032C120.097 36.3746 119.473 37 118.703 37Z" fill="#FC6D26"/> -<path d="M118.703 37H96.3943C95.6243 37 95 36.3746 95 35.6032C95 34.8318 95.6243 34.2064 96.3943 34.2064H118.703C119.473 34.2064 120.097 34.8318 120.097 35.6032C120.097 36.3746 119.473 37 118.703 37Z" fill="white" fill-opacity="0.6"/> -<path d="M93.8573 32H71.3944C70.6243 32 70.0001 31.3746 70.0001 30.6032C70.0001 29.8318 70.6243 29.2064 71.3944 29.2064L93.8573 29.2064C94.6273 29.2064 95.2516 29.8318 95.2516 30.6032C95.2516 31.3746 94.6273 32 93.8573 32Z" fill="#6E49CB"/> -<path d="M93.8573 32H71.3944C70.6243 32 70.0001 31.3746 70.0001 30.6032C70.0001 29.8318 70.6243 29.2064 71.3944 29.2064L93.8573 29.2064C94.6273 29.2064 95.2516 29.8318 95.2516 30.6032C95.2516 31.3746 94.6273 32 93.8573 32Z" fill="white" fill-opacity="0.8"/> -<path d="M86.8573 49H71.3944C70.6243 49 70.0001 48.3746 70.0001 47.6032C70.0001 46.8317 70.6243 46.2064 71.3944 46.2064H86.8573C87.6273 46.2064 88.2516 46.8317 88.2516 47.6032C88.2516 48.3746 87.6273 49 86.8573 49Z" fill="#6E49CB"/> -<path d="M86.8573 49H71.3944C70.6243 49 70.0001 48.3746 70.0001 47.6032C70.0001 46.8317 70.6243 46.2064 71.3944 46.2064H86.8573C87.6273 46.2064 88.2516 46.8317 88.2516 47.6032C88.2516 48.3746 87.6273 49 86.8573 49Z" fill="white" fill-opacity="0.8"/> -<path d="M109.166 43L73.3944 43C72.6243 43 72.0001 42.3746 72.0001 41.6032C72.0001 40.8317 72.6243 40.2064 73.3944 40.2064L109.166 40.2064C109.936 40.2064 110.56 40.8317 110.56 41.6032C110.56 42.3746 109.936 43 109.166 43Z" fill="#6E49CB"/> -<path d="M109.166 43L73.3944 43C72.6243 43 72.0001 42.3746 72.0001 41.6032C72.0001 40.8317 72.6243 40.2064 73.3944 40.2064L109.166 40.2064C109.936 40.2064 110.56 40.8317 110.56 41.6032C110.56 42.3746 109.936 43 109.166 43Z" fill="white" fill-opacity="0.4"/> -<path d="M146.262 24.2349L143.048 21.0153C142.767 20.7338 142.282 20.7323 141.983 21.0313L140.394 22.6236C140.1 22.9181 140.088 23.4002 140.378 23.6903L145.344 28.6651C145.841 29.1637 146.666 29.1795 147.166 28.6793L147.866 27.9779L155.864 19.9653C156.171 19.658 156.167 19.1776 155.868 18.8786L154.279 17.2863C153.985 16.9918 153.495 16.9891 153.194 17.2903L146.262 24.2349Z" fill="#FC6D26"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M125.682 56.7113L123.087 59.3221C122.858 59.5529 122.547 59.6825 122.223 59.6824C121.898 59.6824 121.587 59.5526 121.358 59.3218C121.129 59.091 121 58.7779 121 58.4515C121 58.1251 121.129 57.8121 121.358 57.5813L123.087 55.8412L121.358 54.1011C121.129 53.8703 121 53.5573 121 53.2309C121 52.9045 121.129 52.5915 121.358 52.3606C121.587 52.1298 121.898 52.0001 122.223 52C122.547 51.9999 122.858 52.1296 123.087 52.3603L125.682 54.9711C125.911 55.2019 126.04 55.5149 126.04 55.8412C126.04 56.1675 125.911 56.4805 125.682 56.7113ZM131.796 56.7113L129.202 59.3221C129.088 59.4364 128.954 59.527 128.805 59.5888C128.657 59.6506 128.498 59.6824 128.337 59.6824C128.177 59.6824 128.018 59.6505 127.869 59.5886C127.721 59.5268 127.586 59.4361 127.472 59.3218C127.359 59.2075 127.269 59.0718 127.207 58.9225C127.146 58.7732 127.114 58.6131 127.114 58.4515C127.114 58.2899 127.146 58.1299 127.208 57.9806C127.269 57.8313 127.359 57.6956 127.473 57.5813L129.202 55.8412L127.473 54.1011C127.359 53.9868 127.269 53.8512 127.208 53.7018C127.146 53.5525 127.114 53.3925 127.114 53.2309C127.114 53.0693 127.146 52.9092 127.207 52.7599C127.269 52.6106 127.359 52.4749 127.472 52.3606C127.586 52.2463 127.721 52.1556 127.869 52.0938C128.018 52.0319 128.177 52 128.337 52C128.498 52 128.657 52.0318 128.805 52.0936C128.954 52.1554 129.088 52.246 129.202 52.3603L131.796 54.9711C132.026 55.2019 132.154 55.5149 132.154 55.8412C132.154 56.1675 132.026 56.4805 131.796 56.7113Z" fill="#6E49CB"/> -<path d="M2 26C2 28.415 14.4361 30.3727 29.7769 30.3727C33.7709 30.3727 37.568 30.24 41 30.0011" stroke="#DBDBDB" stroke-width="1.28173"/> -<path d="M2 50C2 52.415 14.4361 54.3727 29.7769 54.3727C35.6133 54.3727 41.0293 54.0893 45.5 53.6052" stroke="#DBDBDB" stroke-width="1.28173"/> -<path d="M57.5537 5V22M2 5V68.6673C2 73.1731 20.9696 75.5204 29.7769 75.5204C38.5842 75.5204 57.5537 73.1731 57.5537 68.6673V57" stroke="#DBDBDB" stroke-width="2.56346" stroke-linejoin="round"/> -<ellipse cx="29.7769" cy="5.64391" rx="27.7769" ry="3.64391" stroke="#DBDBDB" stroke-width="2.56346"/> -<ellipse cx="55.4286" cy="39.46" rx="17.4286" ry="17.46" stroke="#6E49CB" stroke-width="2.56346"/> -<ellipse cx="55.2458" cy="39.2696" rx="13.2458" ry="13.2696" fill="#6E49CB"/> -<ellipse cx="55.2458" cy="39.2696" rx="13.2458" ry="13.2696" fill="white" fill-opacity="0.9"/> -<path d="M61.763 38.5893C62.5797 39.0892 62.5797 40.2756 61.763 40.7756L52.951 46.1704C52.0969 46.6933 51 46.0787 51 45.0773L51 34.2875C51 33.2861 52.0969 32.6715 52.951 33.1944L61.763 38.5893Z" fill="#6E49CB"/> -</svg> diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/create-from-template.svg b/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/create-from-template.svg deleted file mode 100644 index 2ff4e4969b1..00000000000 --- a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/create-from-template.svg +++ /dev/null @@ -1,13 +0,0 @@ -<svg width="82" height="80" viewBox="0 0 82 80" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M68.1765 8.17647H79.6471C80.2968 8.17647 80.8235 8.70319 80.8235 9.35294V69.6471C80.8235 70.2968 80.2968 70.8235 79.6471 70.8235H68.1765V8.17647Z" fill="#F0F0F0" stroke="#DBDBDB" stroke-width="2.35294"/> -<path d="M24.0504 19L24.0093 10.0746C24.0039 8.9145 24.8449 7.92363 25.9905 7.74035L65.393 1.43595C66.8226 1.20721 68.1176 2.31155 68.1176 3.75934V75.903C68.1176 77.3456 66.8314 78.4485 65.4057 78.2284L26.2788 72.1887C25.1356 72.0122 24.2902 71.0309 24.2849 69.8742L24.244 61" stroke="#DBDBDB" stroke-width="2.35294"/> -<path d="M60.0194 11.1796L30.0195 15.2198C29.4357 15.2984 29 15.7966 29 16.3857V19.1235C29 19.8153 29.594 20.3578 30.283 20.2951L60.283 17.5679C60.889 17.5128 61.3529 17.0047 61.3529 16.3962V12.3455C61.3529 11.6334 60.7252 11.0845 60.0194 11.1796Z" fill="#DBDBDB" stroke="#DBDBDB" stroke-width="0.588235" stroke-linecap="round" stroke-linejoin="bevel"/> -<path d="M51.1704 29.1021L41.8902 29.8481C41.0202 29.918 40.5266 30.8776 40.9756 31.626L42.6523 34.4205C42.8676 34.7793 43.2573 34.9968 43.6758 34.9916L51.2794 34.8968C51.9233 34.8888 52.4412 34.3645 52.4412 33.7205V30.2748C52.4412 29.5879 51.8551 29.0471 51.1704 29.1021Z" fill="#DBDBDB" stroke="#DBDBDB" stroke-width="0.588235" stroke-linecap="round" stroke-linejoin="bevel"/> -<path d="M61.2104 70.6341V40.1765C61.2104 39.5267 60.6837 39 60.0339 39H44.9909C44.4469 39 43.9738 39.373 43.8469 39.9019L41.118 51.2721C41.0819 51.4226 41.0148 51.5672 40.923 51.6918C37.1778 56.7763 34.7228 57.4741 29.7135 59.6826C29.2815 59.873 29.0064 60.3064 29.0162 60.7783L29.1309 66.295C29.1428 66.8693 29.5679 67.3511 30.1362 67.4345L59.8631 71.7981C60.5732 71.9024 61.2104 71.3519 61.2104 70.6341Z" fill="#DBDBDB" stroke="#DBDBDB" stroke-width="0.588235" stroke-linecap="round" stroke-linejoin="bevel"/> -<path d="M43.5694 24L36 24.5" stroke="#DBDBDB" stroke-width="1.17647" stroke-linecap="round"/> -<circle cx="23" cy="40" r="21" stroke="#6E49CB" stroke-width="2.35294"/> -<circle cx="23" cy="40" r="17" fill="#6E49CB"/> -<circle cx="23" cy="40" r="17" fill="white" fill-opacity="0.9"/> -<path d="M22.3125 48V33" stroke="#6E49CB" stroke-width="2.35294" stroke-linecap="round"/> -<path d="M15 41.3148H30" stroke="#6E49CB" stroke-width="2.35294" stroke-linecap="round"/> -</svg> diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/import-project.svg b/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/import-project.svg deleted file mode 100644 index 46b4b097bb6..00000000000 --- a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/import-project.svg +++ /dev/null @@ -1,38 +0,0 @@ -<svg width="169" height="84" viewBox="0 0 169 84" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0)"> -<path d="M153.5 74.5714H165.684C166.411 74.5714 167 73.9822 167 73.2554V8.74461C167 8.01779 166.411 7.42859 165.684 7.42859H153.5" stroke="#DBDBDB" stroke-width="2.63203"/> -<path d="M107.94 57L108.014 72.9062C108.017 73.5536 108.49 74.1026 109.13 74.2008L151.913 80.7674C152.71 80.8897 153.429 80.273 153.429 79.4666V2.54193C153.429 1.73264 152.705 1.11511 151.906 1.24226L108.829 8.09543C108.187 8.19744 107.716 8.7519 107.719 9.4012L107.771 20.5" stroke="#DBDBDB" stroke-width="2.63203"/> -<path d="M133.539 52.5313L122.91 51.9925M137.311 52.7225L148.969 53.3135" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/> -<path d="M132.224 43.9783L124 43.6955M135.998 44.1081L147.665 44.5092" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/> -<path d="M148.238 12.3644L131.189 14.604M117.282 16.4529L126.416 15.2311" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/> -<path d="M149.032 36.8519L131.839 37.0342M125 37.0852L127.024 37.0852" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/> -<path d="M112.038 66.3444L120.582 67.4102M148.266 70.8634L134.595 69.1581M125.025 67.9644L129.468 68.5186" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/> -<path d="M114.352 23.3947L116.215 23.2387M129.258 22.147L119.433 22.9693M137.388 21.4665L145.18 20.8143" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/> -<path d="M135.832 29.2067L125.981 29.5888M138.724 28.9864L146.537 28.6833" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/> -<path d="M115.114 59.5557L128.942 60.8796M133.782 61.3429L145.19 62.4351" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/> -<path d="M53.4286 42.4286H21.2857C10.6345 42.4286 2.00002 33.7941 2.00002 23.1429C2.00002 12.4917 10.6345 3.85718 21.2857 3.85718C31.9369 3.85718 40.5714 12.4917 40.5714 23.1429C40.5714 28.17 38.648 32.7479 35.4969 36.1807" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/> -<path d="M53.0361 42.4286H42.3571C35.0591 42.4286 29.1428 48.3448 29.1428 55.6429C29.1428 62.9409 35.0591 68.8572 42.3571 68.8572C49.6552 68.8572 55.5714 62.9409 55.5714 55.6429C55.5714 53.8962 55.2325 52.2287 54.6169 50.7025" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4286 51.7144C38.4286 50.9254 39.0682 50.2858 39.8572 50.2858H44.1429C44.829 50.2858 45.4022 50.7695 45.5399 51.4146L47.7105 52.6677C48.3938 53.0622 48.6279 53.9359 48.2334 54.6192C47.3183 56.2042 45.5714 59.2248 45.4609 59.4191C45.1836 59.9063 44.7237 60.2858 44.1429 60.2858H39.8572C39.0682 60.2858 38.4286 59.6462 38.4286 58.8572V51.7144ZM39.8572 51.7144H44.1429V58.8572H39.8572L39.8572 51.7144ZM45.5714 56.3727L46.9962 53.9049L45.5714 53.0823V56.3727Z" fill="#FC6D26"/> -<path d="M25.5984 15.2331C25.8026 14.471 25.3503 13.6877 24.5882 13.4835C23.8261 13.2793 23.0428 13.7315 22.8386 14.4936L18.4017 31.0524C18.1975 31.8145 18.6497 32.5978 19.4118 32.802C20.1739 33.0062 20.9573 32.5539 21.1615 31.7918L25.5984 15.2331Z" fill="#6E49CB"/> -<path d="M17.2958 17.8469C17.8537 18.4048 17.8537 19.3093 17.2958 19.8672L14.0203 23.1428L17.2958 26.4183C17.8537 26.9762 17.8537 27.8807 17.2958 28.4386C16.738 28.9965 15.8334 28.9965 15.2755 28.4386L10.9898 24.1529C10.4319 23.595 10.4319 22.6905 10.9898 22.1326L15.2755 17.8469C15.8334 17.289 16.738 17.289 17.2958 17.8469Z" fill="#6E49CB"/> -<path d="M26.7041 17.8469C26.1462 18.4048 26.1462 19.3093 26.7041 19.8672L29.9797 23.1428L26.7041 26.4183C26.1462 26.9762 26.1462 27.8807 26.7041 28.4386C27.262 28.9965 28.1665 28.9965 28.7244 28.4386L33.0101 24.1529C33.568 23.595 33.568 22.6905 33.0101 22.1326L28.7244 17.8469C28.1665 17.289 27.262 17.289 26.7041 17.8469Z" fill="#6E49CB"/> -<path d="M50.5714 35.2857L62 35.2857C62.7889 35.2857 63.4285 35.9253 63.4285 36.7143C63.4285 37.5032 62.7889 38.1428 62 38.1428L50.5714 38.1428C49.7824 38.1428 49.1428 37.5032 49.1428 36.7143C49.1428 35.9253 49.7824 35.2857 50.5714 35.2857Z" fill="#FC6D26"/> -<path d="M50.5714 35.2857L62 35.2857C62.7889 35.2857 63.4285 35.9253 63.4285 36.7143C63.4285 37.5032 62.7889 38.1428 62 38.1428L50.5714 38.1428C49.7824 38.1428 49.1428 37.5032 49.1428 36.7143C49.1428 35.9253 49.7824 35.2857 50.5714 35.2857Z" fill="white" fill-opacity="0.6"/> -<path d="M70.5713 35.2857L83.4285 35.2857C84.2175 35.2857 84.8571 35.9253 84.8571 36.7143C84.8571 37.5032 84.2175 38.1428 83.4285 38.1428L70.5713 38.1428C69.7824 38.1428 69.1428 37.5032 69.1428 36.7143C69.1428 35.9253 69.7824 35.2857 70.5713 35.2857Z" fill="#FC6D26"/> -<path d="M76.2856 46.7144L92.1428 46.7144C92.9318 46.7144 93.5714 47.3539 93.5714 48.1429C93.5714 48.9319 92.9318 49.5715 92.1428 49.5715L76.2856 49.5715C75.4967 49.5715 74.8571 48.9319 74.8571 48.1429C74.8571 47.354 75.4967 46.7144 76.2856 46.7144Z" fill="#6E49CB"/> -<path d="M76.2856 46.7144L92.1428 46.7144C92.9318 46.7144 93.5714 47.3539 93.5714 48.1429C93.5714 48.9319 92.9318 49.5715 92.1428 49.5715L76.2856 49.5715C75.4967 49.5715 74.8571 48.9319 74.8571 48.1429C74.8571 47.354 75.4967 46.7144 76.2856 46.7144Z" fill="white" fill-opacity="0.8"/> -<path d="M62.7142 40.9999L90 40.9999C90.7889 40.9999 91.4285 41.6395 91.4285 42.4285C91.4285 43.2175 90.7889 43.8571 90 43.8571L62.7142 43.8571C61.9253 43.8571 61.2857 43.2175 61.2857 42.4285C61.2857 41.6395 61.9253 40.9999 62.7142 40.9999Z" fill="#6E49CB"/> -<path d="M62.7142 40.9999L90 40.9999C90.7889 40.9999 91.4285 41.6395 91.4285 42.4285C91.4285 43.2175 90.7889 43.8571 90 43.8571L62.7142 43.8571C61.9253 43.8571 61.2857 43.2175 61.2857 42.4285C61.2857 41.6395 61.9253 40.9999 62.7142 40.9999Z" fill="white" fill-opacity="0.6"/> -<path d="M69.8571 29.5714L91.5714 29.5714C92.3603 29.5714 92.9999 30.211 92.9999 31C92.9999 31.789 92.3603 32.4286 91.5714 32.4286L69.8571 32.4286C69.0681 32.4286 68.4285 31.789 68.4285 31C68.4285 30.211 69.0681 29.5714 69.8571 29.5714Z" fill="#6E49CB"/> -<path d="M69.8571 29.5714L91.5714 29.5714C92.3603 29.5714 92.9999 30.211 92.9999 31C92.9999 31.789 92.3603 32.4286 91.5714 32.4286L69.8571 32.4286C69.0681 32.4286 68.4285 31.789 68.4285 31C68.4285 30.211 69.0681 29.5714 69.8571 29.5714Z" fill="white" fill-opacity="0.8"/> -<circle cx="107.714" cy="38.8571" r="17.8571" stroke="#6E49CB" stroke-width="2.63203"/> -<circle cx="107.714" cy="38.8573" r="13.5714" fill="#6E49CB"/> -<circle cx="107.714" cy="38.8573" r="13.5714" fill="white" fill-opacity="0.9"/> -<path d="M111.431 35.0867L115.367 39.0232L111.431 42.9597C111.016 43.3744 110.344 43.3744 109.929 42.9597C109.515 42.545 109.515 41.8727 109.929 41.458L111.302 40.0851H101.123C100.537 40.0851 100.061 39.6097 100.061 39.0232C100.061 38.4367 100.537 37.9613 101.123 37.9613H111.302L109.929 36.5884C109.515 36.1737 109.515 35.5014 109.929 35.0867C110.344 34.672 111.016 34.672 111.431 35.0867Z" fill="#6E49CB"/> -</g> -<defs> -<clipPath id="clip0"> -<rect width="169" height="84" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/index.js b/app/assets/javascripts/projects/experiment_new_project_creation/index.js deleted file mode 100644 index ea686d4e1e8..00000000000 --- a/app/assets/javascripts/projects/experiment_new_project_creation/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import Vue from 'vue'; -import NewProjectCreationApp from './components/app.vue'; - -export default function initNewProjectCreation(el, props) { - const { pushToCreateProjectCommand, workingWithProjectsHelpPath } = el.dataset; - - return new Vue({ - el, - components: { - NewProjectCreationApp, - }, - provide: { - workingWithProjectsHelpPath, - pushToCreateProjectCommand, - }, - render(h) { - return h(NewProjectCreationApp, { props }); - }, - }); -} diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue index 13e86c3316f..aba63f89539 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue @@ -175,8 +175,10 @@ export default { this.updateAssignees([this.currentUser.username]); }, saveAssignees() { - this.isDirty = false; - this.updateAssignees(this.selected.map(({ username }) => username)); + if (this.isDirty) { + this.isDirty = false; + this.updateAssignees(this.selected.map(({ username }) => username)); + } this.$el.dispatchEvent(hideDropdownEvent); }, collapseWidget() { diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index c3a08f760a0..e85e416881c 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -95,7 +95,7 @@ export default { <gl-loading-icon v-if="loading" /> <span v-else data-testid="collapsed-count"> {{ participantCount }} </span> </div> - <div v-if="showParticipantLabel" class="title hide-collapsed"> + <div v-if="showParticipantLabel" class="title hide-collapsed gl-mb-2"> <gl-loading-icon v-if="loading" :inline="true" /> {{ participantLabel }} </div> @@ -105,10 +105,10 @@ export default { :key="participant.id" class="participants-author" > - <a :href="participant.web_url" class="author-link"> + <a :href="participant.web_url || participant.webUrl" class="author-link"> <user-avatar-image :lazy="true" - :img-src="participant.avatar_url" + :img-src="participant.avatar_url || participant.avatarUrl" :size="24" :tooltip-text="participant.name" css-classes="avatar-inline" diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue new file mode 100644 index 00000000000..d3043e6f6aa --- /dev/null +++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue @@ -0,0 +1,68 @@ +<script> +import { __ } from '~/locale'; +import { participantsQueries } from '~/sidebar/constants'; +import Participants from './participants.vue'; + +export default { + i18n: { + fetchingError: __('An error occurred while fetching participants'), + }, + components: { + Participants, + }, + props: { + iid: { + type: String, + required: true, + }, + fullPath: { + type: String, + required: true, + }, + issuableType: { + required: true, + type: String, + }, + }, + data() { + return { + participants: [], + }; + }, + apollo: { + participants: { + query() { + return participantsQueries[this.issuableType].query; + }, + variables() { + return { + fullPath: this.fullPath, + iid: this.iid, + }; + }, + update(data) { + return data.workspace?.issuable?.participants.nodes || []; + }, + error(error) { + this.$emit('fetch-error', { + message: this.$options.i18n.fetchingError, + error, + }); + }, + }, + }, + computed: { + isLoading() { + return this.$apollo.queries.participants.loading; + }, + }, +}; +</script> + +<template> + <participants + :loading="isLoading" + :participants="participants" + :number-of-less-participants="7" + /> +</template> diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js index 93469760ce7..3c747d19dc6 100644 --- a/app/assets/javascripts/sidebar/constants.js +++ b/app/assets/javascripts/sidebar/constants.js @@ -1,6 +1,7 @@ import { IssuableType } from '~/issue_show/constants'; import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql'; import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql'; +import epicParticipantsQuery from '~/sidebar/queries/epic_participants.query.graphql'; import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql'; import epicSubscribedQuery from '~/sidebar/queries/epic_subscribed.query.graphql'; import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql'; @@ -46,6 +47,9 @@ export const participantsQueries = { [IssuableType.MergeRequest]: { query: getMergeRequestParticipants, }, + [IssuableType.Epic]: { + query: epicParticipantsQuery, + }, }; export const confidentialityQueries = { diff --git a/app/assets/javascripts/sidebar/queries/epic_participants.query.graphql b/app/assets/javascripts/sidebar/queries/epic_participants.query.graphql new file mode 100644 index 00000000000..fbebc50ab08 --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/epic_participants.query.graphql @@ -0,0 +1,18 @@ +#import "~/graphql_shared/fragments/user.fragment.graphql" +#import "~/graphql_shared/fragments/user_availability.fragment.graphql" + +query epicParticipants($fullPath: ID!, $iid: ID) { + workspace: group(fullPath: $fullPath) { + __typename + issuable: epic(iid: $iid) { + __typename + id + participants { + nodes { + ...User + ...UserAvailability + } + } + } + } +} diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/legacy_container.vue b/app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue index d2fc2c66924..d2fc2c66924 100644 --- a/app/assets/javascripts/projects/experiment_new_project_creation/components/legacy_container.vue +++ b/app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue diff --git a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue new file mode 100644 index 00000000000..e9983af5401 --- /dev/null +++ b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue @@ -0,0 +1,71 @@ +<script> +import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; +import Vue from 'vue'; +import Tracking from '~/tracking'; + +export default { + directives: { + SafeHtml, + }, + props: { + title: { + type: String, + required: true, + }, + panels: { + type: Array, + required: true, + }, + experiment: { + type: String, + required: false, + default: null, + }, + }, + created() { + const trackingMixin = Tracking.mixin({ ...gon.tracking_data, experiment: this.experiment }); + const trackingInstance = new Vue({ + ...trackingMixin, + render() { + return null; + }, + }); + this.track = trackingInstance.track; + }, +}; +</script> +<template> + <div class="container"> + <h2 class="gl-my-7 gl-font-size-h1 gl-text-center"> + {{ title }} + </h2> + <div> + <div + v-for="panel in panels" + :key="panel.name" + class="new-namespace-panel-wrapper gl-display-inline-block gl-px-3 gl-mb-5" + > + <a + :href="`#${panel.name}`" + :data-qa-selector="`${panel.name}_link`" + class="new-namespace-panel gl-display-flex gl-flex-shrink-0 gl-flex-direction-column gl-lg-flex-direction-row gl-align-items-center gl-rounded-base gl-border-gray-100 gl-border-solid gl-border-1 gl-w-full gl-py-6 gl-px-8 gl-hover-text-decoration-none!" + @click="track('click_tab', { label: panel.name })" + > + <div + v-safe-html="panel.illustration" + class="new-namespace-panel-illustration gl-text-white gl-display-flex gl-flex-shrink-0 gl-justify-content-center" + ></div> + <div class="gl-pl-4"> + <h3 class="gl-font-size-h2 gl-reset-color"> + {{ panel.title }} + </h3> + <p class="gl-text-gray-900"> + {{ panel.description }} + </p> + </div> + </a> + </div> + </div> + <slot name="footer"></slot> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue new file mode 100644 index 00000000000..54313297b14 --- /dev/null +++ b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue @@ -0,0 +1,135 @@ +<script> +import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; + +import LegacyContainer from './components/legacy_container.vue'; +import WelcomePage from './components/welcome.vue'; + +export default { + components: { + GlBreadcrumb, + GlIcon, + WelcomePage, + LegacyContainer, + }, + directives: { + SafeHtml, + }, + props: { + title: { + type: String, + required: true, + }, + initialBreadcrumb: { + type: String, + required: true, + }, + panels: { + type: Array, + required: true, + }, + jumpToLastPersistedPanel: { + type: Boolean, + required: false, + default: false, + }, + persistenceKey: { + type: String, + required: true, + }, + experiment: { + type: String, + required: false, + default: null, + }, + }, + + data() { + return { + activePanelName: null, + }; + }, + + computed: { + activePanel() { + return this.panels.find((p) => p.name === this.activePanelName); + }, + + details() { + return this.activePanel.details || this.activePanel.description; + }, + + hasTextDetails() { + return typeof this.details === 'string'; + }, + + breadcrumbs() { + if (!this.activePanel) { + return null; + } + + return [ + { text: this.initialBreadcrumb, href: '#' }, + { text: this.activePanel.title, href: `#${this.activePanel.name}` }, + ]; + }, + }, + + created() { + this.handleLocationHashChange(); + + if (this.jumpToLastPersistedPanel) { + this.activePanelName = localStorage.getItem(this.persistenceKey) || this.panels[0].name; + } + + window.addEventListener('hashchange', () => { + this.handleLocationHashChange(); + this.$emit('panel-change'); + }); + + this.$root.$on('clicked::link', (e) => { + window.location = e.target.href; + }); + }, + + methods: { + handleLocationHashChange() { + this.activePanelName = window.location.hash.substring(1) || null; + if (this.activePanelName) { + localStorage.setItem(this.persistenceKey, this.activePanelName); + } + }, + }, +}; +</script> + +<template> + <welcome-page + v-if="activePanelName === null" + :panels="panels" + :title="title" + :experiment="experiment" + > + <template #footer> + <slot name="welcome-footer"> </slot> + </template> + </welcome-page> + <div v-else class="row"> + <div class="col-lg-3"> + <div v-safe-html="activePanel.illustration" class="gl-text-white"></div> + <h4>{{ activePanel.title }}</h4> + + <p v-if="hasTextDetails">{{ details }}</p> + <component :is="details" v-else /> + + <slot name="extra-description"></slot> + </div> + <div class="col-lg-9"> + <gl-breadcrumb v-if="breadcrumbs" :items="breadcrumbs"> + <template #separator> + <gl-icon name="chevron-right" :size="8" /> + </template> + </gl-breadcrumb> + <legacy-container :key="activePanel.name" class="gl-mt-3" :selector="activePanel.selector" /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/whats_new/components/feature.vue b/app/assets/javascripts/whats_new/components/feature.vue index 4f268b98fcd..09bec458dd3 100644 --- a/app/assets/javascripts/whats_new/components/feature.vue +++ b/app/assets/javascripts/whats_new/components/feature.vue @@ -1,11 +1,13 @@ <script> -import { GlBadge, GlIcon, GlLink, GlSafeHtmlDirective } from '@gitlab/ui'; +import { GlBadge, GlIcon, GlLink, GlSafeHtmlDirective, GlButton } from '@gitlab/ui'; +import { dateInWords, isValidDate } from '~/lib/utils/datetime_utility'; export default { components: { GlBadge, GlIcon, GlLink, + GlButton, }, directives: { SafeHtml: GlSafeHtmlDirective, @@ -16,11 +18,37 @@ export default { required: true, }, }, + computed: { + releaseDate() { + const { published_at } = this.feature; + const date = new Date(published_at); + + if (!isValidDate(date) || date.getTime() === 0) { + return ''; + } + + return dateInWords(date); + }, + }, }; </script> <template> - <div class="gl-pb-7 gl-pt-5 gl-px-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"> + <div class="gl-py-6 gl-px-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"> + <gl-link + :href="feature.url" + target="_blank" + data-track-event="click_whats_new_item" + :data-track-label="feature.title" + :data-track-property="feature.url" + > + <div + class="whats-new-item-image gl-bg-size-cover" + :style="`background-image: url(${feature.image_url});`" + > + <span class="gl-sr-only">{{ feature.title }}</span> + </div> + </gl-link> <gl-link :href="feature.url" target="_blank" @@ -29,39 +57,28 @@ export default { :data-track-label="feature.title" :data-track-property="feature.url" > - <h5 class="gl-font-lg" data-test-id="feature-title">{{ feature.title }}</h5> + <h5 class="gl-font-lg gl-mb-1" data-test-id="feature-title">{{ feature.title }}</h5> </gl-link> + <div v-if="releaseDate" class="gl-mb-3" data-testid="release-date">{{ releaseDate }}</div> <div v-if="feature.packages" class="gl-mb-3"> <gl-badge v-for="packageName in feature.packages" :key="packageName" - size="sm" - class="whats-new-item-badge gl-mr-2 gl-py-1!" + size="md" + class="whats-new-item-badge gl-mr-2" > <gl-icon name="license" />{{ packageName }} </gl-badge> </div> - <gl-link - :href="feature.url" - target="_blank" - data-track-event="click_whats_new_item" - :data-track-label="feature.title" - :data-track-property="feature.url" - > - <img - :alt="feature.title" - :src="feature.image_url" - class="img-thumbnail gl-px-8 gl-py-3 whats-new-item-image" - /> - </gl-link> - <div v-safe-html="feature.body" class="gl-pt-3"></div> - <gl-link + <div v-safe-html="feature.body" class="gl-pt-3 gl-line-height-20"></div> + <gl-button :href="feature.url" target="_blank" data-track-event="click_whats_new_item" :data-track-label="feature.title" :data-track-property="feature.url" - >{{ __('Learn more') }}</gl-link > + {{ __('Learn more') }} <gl-icon name="arrow-right" /> + </gl-button> </div> </template> diff --git a/app/assets/stylesheets/components/whats_new.scss b/app/assets/stylesheets/components/whats_new.scss index 0d3f7ab9d38..92e3c6b2afd 100644 --- a/app/assets/stylesheets/components/whats_new.scss +++ b/app/assets/stylesheets/components/whats_new.scss @@ -54,6 +54,7 @@ .whats-new-item-image { border-color: $gray-50; + height: 250px; } .whats-new-modal-backdrop { diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 1fe94a796f5..87e4bb50984 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -45,7 +45,6 @@ @import 'framework/toggle'; @import 'framework/typography'; @import 'framework/zen'; -@import 'framework/blank'; @import 'framework/wells'; @import 'framework/page_header'; @import 'framework/page_title'; diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss deleted file mode 100644 index 0ada5fabde9..00000000000 --- a/app/assets/stylesheets/framework/blank.scss +++ /dev/null @@ -1,136 +0,0 @@ -.blank-state-parent-container { - .section-container { - padding: 10px; - } - - .section-body { - width: 100%; - height: 100%; - padding-bottom: 25px; - border-radius: $border-radius-default; - } -} - -.blank-state-row { - display: flex; - flex-wrap: wrap; - justify-content: space-between; -} - -.blank-state-welcome { - text-align: center; - padding: $gl-padding 0 ($gl-padding * 2); - - .blank-state-welcome-title { - font-size: 24px; - } - - .blank-state-text { - margin-bottom: 0; - } -} - -.blank-state-link { - color: $gl-text-color; - margin-bottom: 15px; - - &:hover { - background-color: $gray-light; - text-decoration: none; - color: $gl-text-color; - } -} - -.blank-state-center { - padding-top: 20px; - padding-bottom: 20px; - text-align: center; -} - -.blank-state { - display: flex; - align-items: center; - padding: 20px 50px; - border: 1px solid $border-color; - border-radius: $border-radius-default; - min-height: 240px; - margin-bottom: $gl-padding; - width: calc(50% - #{$gl-padding-8}); - - @include media-breakpoint-down(sm) { - width: 100%; - flex-direction: column; - justify-content: center; - padding: 50px 20px; - - .column-small & { - width: 100%; - } - - } -} - -.blank-state, -.blank-state-center { - .blank-state-icon { - svg { - display: block; - margin: auto; - } - } - - .blank-state-title { - margin-top: 0; - font-size: 18px; - } - - .blank-state-body { - @include media-breakpoint-down(sm) { - text-align: center; - margin-top: 20px; - } - - @include media-breakpoint-up(sm) { - padding-left: 20px; - } - } -} - -@include media-breakpoint-up(lg) { - .column-large { - flex: 2; - } - - .column-small { - flex: 1; - margin-bottom: 15px; - - .blank-state { - max-width: 400px; - flex-wrap: wrap; - margin-left: 15px; - } - - .blank-state-icon { - margin-bottom: 30px; - } - } -} - -.experiment-new-project-page-blank-state { - @include media-breakpoint-down(md) { - flex-direction: column; - justify-content: center; - text-align: center; - } - - .blank-state-icon { - min-width: 215px; - } -} - -$experiment-new-project-indigo-700: #41419f; - -.experiment-new-project-page-blank-state-title { - color: $experiment-new-project-indigo-700; -} diff --git a/app/assets/stylesheets/page_bundles/new_namespace.scss b/app/assets/stylesheets/page_bundles/new_namespace.scss new file mode 100644 index 00000000000..60aa3c8f29f --- /dev/null +++ b/app/assets/stylesheets/page_bundles/new_namespace.scss @@ -0,0 +1,28 @@ +@import 'mixins_and_variables_and_functions'; + +$new-namespace-panel-illustration-width: 215px; +$new-namespace-panel-height: 240px; + +.new-namespace-panel-illustration { + width: $new-namespace-panel-illustration-width; +} + +.new-namespace-panel-wrapper { + @include media-breakpoint-down(md) { + width: 100%; + } + width: 50%; +} + +.new-namespace-panel { + &:hover { + background-color: $gray-10; + } + + color: $purple-700; + min-height: $new-namespace-panel-height; + text-align: center; + @include media-breakpoint-up(lg) { + text-align: left; + } +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index b9f5a427a24..0437fa19752 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -217,7 +217,6 @@ .title { color: $gl-text-color; - margin-bottom: $gl-padding-4; line-height: $gl-line-height-20; .avatar { diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 575b9642efe..1668cef0281 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -85,7 +85,7 @@ class ProjectsController < Projects::ApplicationController notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name } ) else - render 'new', locals: { active_tab: active_new_project_tab } + render 'new' end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index a78cd752223..7ff851fd20e 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -123,9 +123,7 @@ module GroupsHelper @has_group_title = true full_title = [] - ancestors = group.ancestors.with_route - - ancestors.reverse_each.with_index do |parent, index| + sorted_ancestors(group).with_route.reverse_each.with_index do |parent, index| if index > 0 add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true, for_dropdown: true), location: :before) else @@ -294,11 +292,20 @@ module GroupsHelper end def oldest_consecutively_locked_ancestor(group) - group.ancestors.find do |group| + sorted_ancestors(group).find do |group| !group.has_parent? || !group.parent.share_with_group_lock? end end + # Ancestors sorted by hierarchy depth in bottom-top order. + def sorted_ancestors(group) + if group.root_ancestor.use_traversal_ids? + group.ancestors(hierarchy_order: :asc) + else + group.ancestors + end + end + def default_help s_("GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually.") end diff --git a/app/models/concerns/repository_storage_movable.rb b/app/models/concerns/repository_storage_movable.rb index 990d50eb073..1dd8eebeff3 100644 --- a/app/models/concerns/repository_storage_movable.rb +++ b/app/models/concerns/repository_storage_movable.rb @@ -114,7 +114,7 @@ module RepositoryStorageMovable private def container_repository_writable - add_error(_('is read only')) if container&.repository_read_only? + add_error(_('is read-only')) if container&.repository_read_only? end def error_key diff --git a/app/models/group.rb b/app/models/group.rb index 625ecd516a4..aef5bdd6e88 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -839,7 +839,12 @@ class Group < Namespace end def uncached_ci_variables_for(ref, project, environment: nil) - list_of_ids = [self] + ancestors + list_of_ids = if root_ancestor.use_traversal_ids? + [self] + ancestors(hierarchy_order: :asc) + else + [self] + ancestors + end + variables = Ci::GroupVariable.where(group: list_of_ids) variables = variables.unprotected unless project.protected_for?(ref) diff --git a/app/models/issue.rb b/app/models/issue.rb index a0b939d0122..94b846d637e 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -123,7 +123,7 @@ class Issue < ApplicationRecord scope :with_prometheus_alert_events, -> { joins(:issues_prometheus_alert_events) } scope :with_self_managed_prometheus_alert_events, -> { joins(:issues_self_managed_prometheus_alert_events) } scope :with_api_entity_associations, -> { - preload(:timelogs, :closed_by, :assignees, :author, :notes, :labels, + preload(:timelogs, :closed_by, :assignees, :author, :labels, milestone: { project: [:route, { namespace: :route }] }, project: [:route, { namespace: :route }]) } diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb index c881734bd85..3ebf0894b98 100644 --- a/app/models/namespaces/traversal/linear.rb +++ b/app/models/namespaces/traversal/linear.rb @@ -58,11 +58,18 @@ module Namespaces end def self_and_descendants - if use_traversal_ids? - lineage(self) - else - super - end + return super unless use_traversal_ids? + + lineage(top: self) + end + + def ancestors(hierarchy_order: nil) + return super() unless use_traversal_ids? + return super() unless Feature.enabled?(:use_traversal_ids_for_ancestors, root_ancestor, default_enabled: :yaml) + + return self.class.none if parent_id.blank? + + lineage(bottom: parent, hierarchy_order: hierarchy_order) end private @@ -84,11 +91,29 @@ module Namespaces end # Search this namespace's lineage. Bound inclusively by top node. - def lineage(top) - raise UnboundedSearch, 'Must bound search by a top' unless top + def lineage(top: nil, bottom: nil, hierarchy_order: nil) + raise UnboundedSearch, 'Must bound search by either top or bottom' unless top || bottom + + skope = without_sti_condition + + if top + skope = skope.traversal_ids_contains("{#{top.id}}") + end + + if bottom + skope = skope.where(id: bottom.traversal_ids[0..-1]) + end + + # The original `with_depth` attribute in ObjectHierarchy increments as you + # walk away from the "base" namespace. This direction changes depending on + # if you are walking up the ancestors or down the descendants. + if hierarchy_order + depth_sql = "ABS(#{traversal_ids.count} - array_length(traversal_ids, 1))" + skope = skope.select(skope.arel_table[Arel.star], "#{depth_sql} as depth") + .order(depth: hierarchy_order) + end - without_sti_condition - .traversal_ids_contains("{#{top.id}}") + skope end end end diff --git a/app/models/service.rb b/app/models/service.rb index 51ac4555c1f..4d52056365e 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -268,7 +268,7 @@ class Service < ApplicationRecord private_class_method :instance_level_integration def self.create_from_active_default_integrations(scope, association, with_templates: false) - group_ids = scope.ancestors.select(:id) + group_ids = sorted_ancestors(scope).select(:id) array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]' from_union([ @@ -459,6 +459,15 @@ class Service < ApplicationRecord private + # Ancestors sorted by hierarchy depth in bottom-top order. + def self.sorted_ancestors(scope) + if scope.root_ancestor.use_traversal_ids? + Namespace.from(scope.ancestors(hierarchy_order: :asc)) + else + scope.ancestors + end + end + def validate_is_instance_or_template errors.add(:template, 'The service should be a service template or instance-level integration') if template? && instance_level? end diff --git a/app/services/clusters/aws/fetch_credentials_service.rb b/app/services/clusters/aws/fetch_credentials_service.rb index 7fd1b56657c..e38852c7ec7 100644 --- a/app/services/clusters/aws/fetch_credentials_service.rb +++ b/app/services/clusters/aws/fetch_credentials_service.rb @@ -54,7 +54,7 @@ module Clusters ## # If we haven't created a provider record yet, - # we restrict ourselves to read only access so + # we restrict ourselves to read-only access so # that we can safely expose credentials to the # frontend (to be used when populating the # creation form). diff --git a/app/views/projects/_archived_notice.html.haml b/app/views/projects/_archived_notice.html.haml index dcece8ab42f..5489e41d37b 100644 --- a/app/views/projects/_archived_notice.html.haml +++ b/app/views/projects/_archived_notice.html.haml @@ -2,4 +2,4 @@ .text-warning.center.prepend-top-20 %p = sprite_icon('warning-solid') - = _('Archived project! Repository and other project resources are read only') + = _('Archived project! Repository and other project resources are read-only') diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 609d3317315..c62853145b6 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -2,80 +2,38 @@ - @hide_top_links = true - page_title _('New Project') - header_title _("Projects"), dashboard_projects_path -- active_tab = local_assigns.fetch(:active_tab, 'blank') +- add_page_specific_style 'page_bundles/new_namespace' .project-edit-container.gl-mt-5 .project-edit-errors = render 'projects/errors' - .js-experiment-new-project-creation{ data: { is_ci_cd_available: (ci_cd_projects_available? if Gitlab.ee?), has_errors: @project.errors.any?, new_project_guidelines: brand_new_project_guidelines, push_to_create_project_command: push_to_create_project_command, working_with_projects_help_path: help_page_path("user/project/working_with_projects") } } + .js-new-project-creation{ data: { is_ci_cd_available: (ci_cd_projects_available? if Gitlab.ee?).to_s, has_errors: @project.errors.any?.to_s, new_project_guidelines: brand_new_project_guidelines, push_to_create_project_command: push_to_create_project_command, working_with_projects_help_path: help_page_path("user/project/working_with_projects") } } .row{ 'v-cloak': true } - .col-lg-3.profile-settings-sidebar - %h4.gl-mt-0 - = _('New project') - %p - - among_other_things_link = link_to _('among other things'), help_page_path("user/project/index.md", anchor: "project-features"), target: '_blank' - = _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link } - %p - = _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.') - = render_if_exists 'projects/new_ci_cd_banner_external_repo' - %p - - pages_getting_started_guide = link_to _('Pages getting started guide'), help_page_path("user/project/pages/index", anchor: "getting-started"), target: '_blank' - = _('Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}.').html_safe % { pages_getting_started_guide: pages_getting_started_guide } - .md - = brand_new_project_guidelines - %p - %strong= _("Tip:") - = _("You can also create a project from the command line.") - - .col-lg-9.js-toggle-container - %ul.nav.nav-tabs.nav-links.gitlab-tabs{ role: 'tablist' } - %li.nav-item{ role: 'presentation' } - %a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab', experiment_track_label: 'blank_project' }, role: 'tab' } - %span.d-none.d-sm-block= s_('ProjectsNew|Blank project') - %span.d-block.d-sm-none= s_('ProjectsNew|Blank') - %li.nav-item{ role: 'presentation' } - %a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab', experiment_track_label: 'create_from_template' }, role: 'tab' } - %span.d-none.d-sm-block.qa-project-create-from-template-tab= s_('ProjectsNew|Create from template') - %span.d-block.d-sm-none= s_('ProjectsNew|Template') - %li.nav-item{ role: 'presentation' } - %a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab', experiment_track_label: 'import_project' }, role: 'tab' } - %span.d-none.d-sm-block= s_('ProjectsNew|Import project') - %span.d-block.d-sm-none= s_('ProjectsNew|Import') - = render_if_exists 'projects/new_ci_cd_only_project_tab', active_tab: active_tab - - .tab-content.gitlab-tab-content - .tab-pane.js-toggle-container{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' } - = form_for @project, html: { class: 'new_project' } do |f| - = render 'new_project_fields', f: f, project_name_id: "blank-project-name" - - #create-from-template-pane.tab-pane.js-toggle-container.px-0.pb-0{ class: active_when(active_tab == 'template'), role: 'tabpanel' } - .card.card-slim.m-4.p-4 + #blank-project-pane.tab-pane.active + = form_for @project, html: { class: 'new_project' } do |f| + = render 'new_project_fields', f: f, project_name_id: "blank-project-name" + + #create-from-template-pane.tab-pane + .gl-card.gl-my-5 + .gl-card-body + %div + - contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing' + - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: contributing_templates_url } + = _('Learn how to %{link_start}contribute to the built-in templates%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } + = form_for @project, html: { class: 'new_project' } do |f| + .project-template + .form-group %div - - contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing' - - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: contributing_templates_url } - = _('Learn how to %{link_start}contribute to the built-in templates%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } - = form_for @project, html: { class: 'new_project' } do |f| - .project-template - .form-group - %div - = render 'project_templates', f: f, project: @project - - .tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' } - - if import_sources_enabled? - = render 'import_project_pane', active_tab: active_tab - - else - .nothing-here-block - %h4= s_('ProjectsNew|No import options available') - %p= s_('ProjectsNew|Contact an administrator to enable options for importing your project.') + = render 'project_templates', f: f, project: @project - = render_if_exists 'projects/new_ci_cd_only_project_pane', active_tab: active_tab + #import-project-pane.tab-pane.js-toggle-container + - if import_sources_enabled? + = render 'import_project_pane' + - else + .nothing-here-block + %h4= s_('ProjectsNew|No import options available') + %p= s_('ProjectsNew|Contact an administrator to enable options for importing your project.') -.save-project-loader.d-none - .center - %h2 - .gl-spinner.gl-spinner-md.align-text-bottom - = s_('ProjectsNew|Creating project & repository.') - %p - = s_('ProjectsNew|Please wait a moment, this page will automatically refresh when ready.') + = render_if_exists 'projects/new_ci_cd_only_project_pane' diff --git a/app/views/projects/settings/_archive.html.haml b/app/views/projects/settings/_archive.html.haml index 5e0f24cea21..be9bd3dfc01 100644 --- a/app/views/projects/settings/_archive.html.haml +++ b/app/views/projects/settings/_archive.html.haml @@ -14,7 +14,7 @@ method: :post, class: "gl-button btn btn-confirm" - else - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'archiving-a-project') } - %p= _("Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe } + %p= _("Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe } = link_to _('Archive project'), archive_project_path(@project), data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link' }, method: :post, class: "gl-button btn btn-warning" diff --git a/changelogs/unreleased/211373-remove-notes-preload-in-issues-api.yml b/changelogs/unreleased/211373-remove-notes-preload-in-issues-api.yml new file mode 100644 index 00000000000..a228310bc3a --- /dev/null +++ b/changelogs/unreleased/211373-remove-notes-preload-in-issues-api.yml @@ -0,0 +1,5 @@ +--- +title: Improve performance of project issues API +merge_request: 60981 +author: +type: performance diff --git a/changelogs/unreleased/211373-remove-subscribed-from-other-issue-lists.yml b/changelogs/unreleased/211373-remove-subscribed-from-other-issue-lists.yml new file mode 100644 index 00000000000..2ce52bc9720 --- /dev/null +++ b/changelogs/unreleased/211373-remove-subscribed-from-other-issue-lists.yml @@ -0,0 +1,5 @@ +--- +title: Remove subscribed field from todos, related issues, and epic issues API +merge_request: 60981 +author: +type: fixed diff --git a/changelogs/unreleased/267488-clean-up-the-create-project-ui-experiment.yml b/changelogs/unreleased/267488-clean-up-the-create-project-ui-experiment.yml new file mode 100644 index 00000000000..f285d7032ec --- /dev/null +++ b/changelogs/unreleased/267488-clean-up-the-create-project-ui-experiment.yml @@ -0,0 +1,5 @@ +--- +title: Make new project ui the only option +merge_request: 59452 +author: +type: changed diff --git a/changelogs/unreleased/294210-gitlab-elasticsearch-reindexing-store-settings-in-db.yml b/changelogs/unreleased/294210-gitlab-elasticsearch-reindexing-store-settings-in-db.yml new file mode 100644 index 00000000000..2c783080b73 --- /dev/null +++ b/changelogs/unreleased/294210-gitlab-elasticsearch-reindexing-store-settings-in-db.yml @@ -0,0 +1,5 @@ +--- +title: Store slice multiplier and max slices running for reindex in DB +merge_request: 60861 +author: +type: changed diff --git a/changelogs/unreleased/324749-linear_ancestors.yml b/changelogs/unreleased/324749-linear_ancestors.yml new file mode 100644 index 00000000000..00acd7ab5d6 --- /dev/null +++ b/changelogs/unreleased/324749-linear_ancestors.yml @@ -0,0 +1,5 @@ +--- +title: Linear traversal query for Namespace#ancestors +merge_request: 57137 +author: +type: performance diff --git a/changelogs/unreleased/329674-ui-polish-for-whats-new.yml b/changelogs/unreleased/329674-ui-polish-for-whats-new.yml new file mode 100644 index 00000000000..68e4534d69b --- /dev/null +++ b/changelogs/unreleased/329674-ui-polish-for-whats-new.yml @@ -0,0 +1,5 @@ +--- +title: Polish the "What's new" UI +merge_request: 60804 +author: Kev @KevSlashNull +type: changed diff --git a/changelogs/unreleased/terraform-ci-template-to-default-branch.yml b/changelogs/unreleased/terraform-ci-template-to-default-branch.yml new file mode 100644 index 00000000000..34bf5b6fc53 --- /dev/null +++ b/changelogs/unreleased/terraform-ci-template-to-default-branch.yml @@ -0,0 +1,5 @@ +--- +title: Merge branch 'mo-update-artifact-documentation' into 'master' +merge_request: 61084 +author: +type: other diff --git a/config/application.rb b/config/application.rb index 8f5b734b04e..4d734c713fb 100644 --- a/config/application.rb +++ b/config/application.rb @@ -207,6 +207,7 @@ module Gitlab config.assets.precompile << "page_bundles/merge_conflicts.css" config.assets.precompile << "page_bundles/merge_requests.css" config.assets.precompile << "page_bundles/milestone.css" + config.assets.precompile << "page_bundles/new_namespace.css" config.assets.precompile << "page_bundles/oncall_schedules.css" config.assets.precompile << "page_bundles/pipeline.css" config.assets.precompile << "page_bundles/pipeline_schedules.css" diff --git a/config/feature_flags/development/use_traversal_ids_for_ancestors.yml b/config/feature_flags/development/use_traversal_ids_for_ancestors.yml new file mode 100644 index 00000000000..57804957192 --- /dev/null +++ b/config/feature_flags/development/use_traversal_ids_for_ancestors.yml @@ -0,0 +1,8 @@ +--- +name: use_traversal_ids_for_ancestors +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57137 +rollout_issue_url: +milestone: '13.12' +type: development +group: group::access +default_enabled: false diff --git a/db/migrate/20210430154631_add_slice_multiplier_and_max_slices_to_elastic_reindexing_task.rb b/db/migrate/20210430154631_add_slice_multiplier_and_max_slices_to_elastic_reindexing_task.rb new file mode 100644 index 00000000000..be022c1b973 --- /dev/null +++ b/db/migrate/20210430154631_add_slice_multiplier_and_max_slices_to_elastic_reindexing_task.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +class AddSliceMultiplierAndMaxSlicesToElasticReindexingTask < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DEFAULT_MAX_TOTAL_SLICES_RUNNING = 60 + DEFAULT_SLICE_MULTIPLIER = 2 + + def change + add_column :elastic_reindexing_tasks, :max_slices_running, :integer, + limit: 2, + default: DEFAULT_MAX_TOTAL_SLICES_RUNNING, + null: false + add_column :elastic_reindexing_tasks, :slice_multiplier, :integer, + limit: 2, + default: DEFAULT_SLICE_MULTIPLIER, + null: false + end +end diff --git a/db/schema_migrations/20210430154631 b/db/schema_migrations/20210430154631 new file mode 100644 index 00000000000..6369c657d5a --- /dev/null +++ b/db/schema_migrations/20210430154631 @@ -0,0 +1 @@ +08f4cd1f8f7ddc336d0edee7581b0cb59e0cdc7b5f3cbeb1ccdcd7a1c52d366f
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 561e1083326..5837926893e 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -12447,6 +12447,8 @@ CREATE TABLE elastic_reindexing_tasks ( error_message text, documents_count_target integer, delete_original_index_at timestamp with time zone, + max_slices_running smallint DEFAULT 60 NOT NULL, + slice_multiplier smallint DEFAULT 2 NOT NULL, CONSTRAINT check_04151aca42 CHECK ((char_length(index_name_from) <= 255)), CONSTRAINT check_7f64acda8e CHECK ((char_length(error_message) <= 255)), CONSTRAINT check_85ebff7124 CHECK ((char_length(index_name_to) <= 255)), diff --git a/doc/administration/geo/index.md b/doc/administration/geo/index.md index 6cec13fde02..8866d1dc9ad 100644 --- a/doc/administration/geo/index.md +++ b/doc/administration/geo/index.md @@ -267,9 +267,9 @@ dashboard in your browser. Failures that happen during a backfill are scheduled to be retried at the end of the backfill. -## Remove Geo node +## Remove Geo site -For more information on removing a Geo node, see [Removing **secondary** Geo nodes](replication/remove_geo_node.md). +For more information on removing a Geo node, see [Removing **secondary** Geo nodes](replication/remove_geo_site.md). ## Disable Geo diff --git a/doc/administration/geo/replication/configuration.md b/doc/administration/geo/replication/configuration.md index 76910e73055..6d5f3e61ba0 100644 --- a/doc/administration/geo/replication/configuration.md +++ b/doc/administration/geo/replication/configuration.md @@ -287,7 +287,7 @@ Please note that disabling a **secondary** node stops the synchronization proces Please note that if `git_data_dirs` is customized on the **primary** node for multiple repository shards you must duplicate the same configuration on each **secondary** node. -Point your users to the ["Using a Geo Server" guide](using_a_geo_server.md). +Point your users to the [Using a Geo Site guide](usage.md). Currently, this is what is synced: diff --git a/doc/administration/geo/replication/remove_geo_node.md b/doc/administration/geo/replication/remove_geo_node.md index 09ea84b6c4b..697d8c6ae38 100644 --- a/doc/administration/geo/replication/remove_geo_node.md +++ b/doc/administration/geo/replication/remove_geo_node.md @@ -4,5 +4,5 @@ redirect_to: '../../geo/replication/remove_geo_site.md' This document was moved to [another location](../../geo/replication/remove_geo_site.md). -<!-- This redirect file can be deleted after 2022-04-01 --> +<!-- This redirect file can be deleted after 2021-06-01 --> <!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/administration/geo/replication/using_a_geo_server.md b/doc/administration/geo/replication/using_a_geo_server.md index e48e750f710..f8ce72ac3f8 100644 --- a/doc/administration/geo/replication/using_a_geo_server.md +++ b/doc/administration/geo/replication/using_a_geo_server.md @@ -4,5 +4,5 @@ redirect_to: '../../geo/replication/usage.md' This document was moved to [another location](../../geo/replication/usage.md). -<!-- This redirect file can be deleted after 2022-04-01 --> +<!-- This redirect file can be deleted after 2022-06-01 --> <!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/administration/geo/setup/index.md b/doc/administration/geo/setup/index.md index 5ec18e29f21..1afa4360cbc 100644 --- a/doc/administration/geo/setup/index.md +++ b/doc/administration/geo/setup/index.md @@ -25,7 +25,7 @@ If you installed GitLab using the Omnibus packages (highly recommended): 1. [Configure fast lookup of authorized SSH keys in the database](../../operations/fast_ssh_key_lookup.md). This step is required and needs to be done on **both** the **primary** and **secondary** nodes. 1. [Configure GitLab](../replication/configuration.md) to set the **primary** and **secondary** nodes. 1. Optional: [Configure a secondary LDAP server](../../auth/ldap/index.md) for the **secondary** node. See [notes on LDAP](../index.md#ldap). -1. [Follow the "Using a Geo Server" guide](../replication/using_a_geo_server.md). +1. Follow the [Using a Geo Site](../replication/usage.md) guide. ## Post-installation documentation diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index 2ed98178d69..3935e990590 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -108,9 +108,9 @@ The availability objectives for Gitaly clusters are: Gitaly Cluster supports: - [Strong consistency](praefect.md#strong-consistency) of the secondary replicas. -- [Automatic failover](praefect.md#automatic-failover-and-leader-election) from the primary to the secondary. +- [Automatic failover](praefect.md#automatic-failover-and-primary-election-strategies) from the primary to the secondary. - Reporting of possible data loss if replication queue is non-empty. -- Marking repositories as [read only](praefect.md#read-only-mode) if data loss is detected to prevent data inconsistencies. +- Marking repositories as [read-only](praefect.md#read-only-mode) if data loss is detected to prevent data inconsistencies. Follow the [Gitaly Cluster epic](https://gitlab.com/groups/gitlab-org/-/epics/1489) for improvements including @@ -248,10 +248,10 @@ Gitaly Cluster and [Geo](../geo/index.md) both provide redundancy. However the r The following table outlines the major differences between Gitaly Cluster and Geo: -| Tool | Nodes | Locations | Latency tolerance | Failover | Consistency | Provides redundancy for | -|:---------------|:---------|:----------|:-------------------|:----------------------------------------------------------------|:-----------------------------------------|:------------------------| -| Gitaly Cluster | Multiple | Single | Approximately 1 ms | [Automatic](praefect.md#automatic-failover-and-leader-election) | [Strong](praefect.md#strong-consistency) | Data storage in Git | -| Geo | Multiple | Multiple | Up to one minute | [Manual](../geo/disaster_recovery/index.md) | Eventual | Entire GitLab instance | +| Tool | Nodes | Locations | Latency tolerance | Failover | Consistency | Provides redundancy for | +|:---------------|:---------|:----------|:-------------------|:----------------------------------------------------------------------------|:-----------------------------------------|:------------------------| +| Gitaly Cluster | Multiple | Single | Approximately 1 ms | [Automatic](praefect.md#automatic-failover-and-primary-election-strategies) | [Strong](praefect.md#strong-consistency) | Data storage in Git | +| Geo | Multiple | Multiple | Up to one minute | [Manual](../geo/disaster_recovery/index.md) | Eventual | Entire GitLab instance | For more information, see: diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md index e53878e59f7..b55f3b6ea79 100644 --- a/doc/administration/gitaly/praefect.md +++ b/doc/administration/gitaly/praefect.md @@ -1079,32 +1079,33 @@ You can configure: current assignments: gitaly-1, gitaly-2 ``` -## Automatic failover and leader election +## Automatic failover and primary election strategies -Praefect regularly checks the health of each backend Gitaly node. This -information can be used to automatically failover to a new primary node if the -current primary node is found to be unhealthy. +Praefect regularly checks the health of each Gitaly node. This is used to automatically fail over +to a newly-elected primary Gitaly node if the current primary node is found to be unhealthy. -### Election strategies - -We recommend using [repository-specific primary nodes](#repository-specific-primary-nodes), -which is [planned to be the only available election strategy](https://gitlab.com/gitlab-org/gitaly/-/issues/3574) +We recommend using [repository-specific primary nodes](#repository-specific-primary-nodes). This is +[planned to be the only available election strategy](https://gitlab.com/gitlab-org/gitaly/-/issues/3574) from GitLab 14.0. -In the future, we are likely to implement support for: +### Repository-specific primary nodes + +> [Introduced](https://gitlab.com/gitlab-org/gitaly/-/issues/3492) in GitLab 13.12. -- A [Consul](../consul.md) strategy. -- A cloud-native strategy. +Gitaly Cluster supports electing repository-specific primary Gitaly nodes. Repository-specific +Gitaly primary nodes are enabled in `/etc/gitlab/gitlab.rb` by setting +`praefect['failover_election_strategy'] = 'per_repository'`. -#### Repository-specific primary nodes +Praefect's [deprecated election strategies](#deprecated-election-strategies): -Praefect's earlier election strategies elected a primary for each virtual storage, which was used as the -primary for each repository in the virtual storage. This model prevented horizontal scaling of a virtual -storage. The primary Gitaly node needed a replica of each repository and thus became the bottleneck. +- Elected a primary Gitaly node for each virtual storage, which was used as the primary node for + each repository in the virtual storage. +- Prevented horizontal scaling of a virtual storage. The primary Gitaly node needed a replica of + each repository and thus became the bottleneck. -The `per_repository` election strategy solves this problem by electing a primary separately for each repository. -Combined with [configurable replication factors](#configure-replication-factor), you can horizontally -scale storage capacity and distribute write load across Gitaly nodes. +The `per_repository` election strategy solves this problem by electing a primary Gitaly node separately for each +repository. Combined with [configurable replication factors](#configure-replication-factor), you can +horizontally scale storage capacity and distribute write load across Gitaly nodes. Primary elections are run when: @@ -1128,16 +1129,13 @@ If there are no healthy secondary nodes for a repository: - The unhealthy primary node is demoted and the repository is left without a primary node. - Operations that require a primary node fail until a primary is successfully elected. -Repository-specific primaries are enabled in `/etc/gitlab/gitlab.rb` by setting -`praefect['failover_election_strategy'] = 'per_repository'`. - -##### Migrate to repository-specific primary nodes +#### Migrate to repository-specific primary Gitaly nodes -New Gitaly clusters can start using the `per_repository` election strategy immediately. +New Gitaly Clusters can start using the `per_repository` election strategy immediately. To migrate existing clusters: -1. Praefect didn't historically keep database records of every repository stored on the cluster. When +1. Praefect nodes didn't historically keep database records of every repository stored on the cluster. When the `per_repository` election strategy is configured, Praefect expects to have database records of each repository. A [background migration](https://gitlab.com/gitlab-org/gitaly/-/merge_requests/2749) is included in GitLab 13.6 and later to create any missing database records for repositories. Before migrating @@ -1161,35 +1159,39 @@ To migrate existing clusters: The migration is ran when Praefect starts up. If the migration is unsuccessful, you can restart a Praefect node to reattempt it. The migration only runs with `sql` election strategy configured. -1. Running two different election strategies side by side can cause a split brain, where different Praefects - consider repositories to have different primaries. To avoid this, all Praefects should be shut down prior - to changing the election strategy. +1. Running two different election strategies side by side can cause a split brain, where different + Praefect nodes consider repositories to have different primaries. To avoid this, shut down + all Praefect nodes before changing the election strategy. - This can be done by running `gitlab-ctl stop praefect` on the Praefect nodes. + Do this by running `gitlab-ctl stop praefect` on the Praefect nodes. 1. On the Praefect nodes, configure the election strategy in `/etc/gitlab/gitlab.rb` with `praefect['failover_election_strategy'] = 'per_repository'`. -1. Finally, run `gitlab-ctl reconfigure` to reconfigure and restart the Praefects. +1. Finally, run `gitlab-ctl reconfigure` to reconfigure and restart the Praefect nodes. -#### Deprecated election strategies +### Deprecated election strategies WARNING: The below election strategies are deprecated and are scheduled for removal in GitLab 14.0. Migrate to [repository-specific primary nodes](#repository-specific-primary-nodes). - **PostgreSQL:** Enabled by default until GitLab 14.0, and equivalent to: - `praefect['failover_election_strategy'] = 'sql'`. This configuration - option allows multiple Praefect nodes to coordinate via the - PostgreSQL database to elect a primary Gitaly node. This configuration - causes Praefect nodes to elect a new primary, monitor its health, - and elect a new primary if the current one has not been reachable in - 10 seconds by a majority of the Praefect nodes. + `praefect['failover_election_strategy'] = 'sql'`. + + This configuration option: + + - Allows multiple Praefect nodes to coordinate via the PostgreSQL database to elect a primary + Gitaly node. + - Causes Praefect nodes to elect a new primary Gitaly node, monitor its health, and elect a new primary + Gitaly node if the current one is not reached within 10 seconds by a majority of the Praefect + nodes. - **Memory:** Enabled by setting `praefect['failover_election_strategy'] = 'local'` - in `/etc/gitlab/gitlab.rb` on the Praefect node. If a sufficient number of health - checks fail for the current primary backend Gitaly node, and new primary will - be elected. **Do not use with multiple Praefect nodes!** Using with multiple - Praefect nodes is likely to result in a split brain. + in `/etc/gitlab/gitlab.rb` on the Praefect node. + + If a sufficient number of health checks fail for the current primary Gitaly node, a new primary is + elected. **Do not use with multiple Praefect nodes!** Using with multiple Praefect nodes is + likely to result in a split brain. ## Primary Node Failure @@ -1438,7 +1440,7 @@ GitLab repositories can be associated with projects, groups, and snippets. Each have a separate API to schedule the respective repositories to move. To move all repositories on a GitLab instance, each of these types must be scheduled to move for each storage. -Each repository is made read only for the duration of the move. The repository is not writable +Each repository is made read-only for the duration of the move. The repository is not writable until the move has completed. After creating and configuring Gitaly Cluster: diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md index d9a9c456235..14e54513536 100644 --- a/doc/administration/packages/container_registry.md +++ b/doc/administration/packages/container_registry.md @@ -947,7 +947,7 @@ To enable the read-only mode: sudo gitlab-ctl reconfigure ``` - This command sets the Container Registry into the read only mode. + This command sets the Container Registry into the read-only mode. 1. Next, trigger one of the garbage collect commands: diff --git a/doc/administration/postgresql/replication_and_failover.md b/doc/administration/postgresql/replication_and_failover.md index 7e25bd69539..878d2b536cb 100644 --- a/doc/administration/postgresql/replication_and_failover.md +++ b/doc/administration/postgresql/replication_and_failover.md @@ -120,7 +120,7 @@ Few notes on the service itself: - The service runs under a system account, by default `gitlab-consul`. - If you are using a different username, you will have to specify it. We will refer to it with `CONSUL_USERNAME`, -- There will be a database user created with read only access to the repmgr +- There will be a database user created with read-only access to the repmgr database - Passwords will be stored in the following locations: - `/etc/gitlab/gitlab.rb`: hashed diff --git a/doc/api/epic_issues.md b/doc/api/epic_issues.md index c4a8e2d40cc..3644375ad0a 100644 --- a/doc/api/epic_issues.md +++ b/doc/api/epic_issues.md @@ -107,7 +107,6 @@ Example response: "award_emoji": "http://localhost:3001/api/v4/projects/8/issues/6/award_emoji", "project": "http://localhost:3001/api/v4/projects/8" }, - "subscribed": true, "epic_issue_id": 2 } ] diff --git a/doc/api/issue_links.md b/doc/api/issue_links.md index e6e5a0b3d25..4dfa9b2f532 100644 --- a/doc/api/issue_links.md +++ b/doc/api/issue_links.md @@ -51,7 +51,6 @@ Parameters: "description" : null, "updated_at" : "2016-01-07T12:44:33.959Z", "milestone" : null, - "subscribed" : true, "user_notes_count": 0, "due_date": null, "web_url": "http://example.com/example/example/issues/14", diff --git a/doc/api/project_analytics.md b/doc/api/project_analytics.md deleted file mode 100644 index d89c173dd3e..00000000000 --- a/doc/api/project_analytics.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -redirect_to: 'dora4_project_analytics.md' ---- - -This document was moved to [another location](dora4_project_analytics.md). - -<!-- This redirect file can be deleted after <2021-04-25>. --> -<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/api/todos.md b/doc/api/todos.md index 3b2e3acb38b..864f87f988e 100644 --- a/doc/api/todos.md +++ b/doc/api/todos.md @@ -102,7 +102,6 @@ Example Response: }, "merge_when_pipeline_succeeds": false, "merge_status": "cannot_be_merged", - "subscribed": true, "user_notes_count": 7 }, "target_url": "https://gitlab.example.com/gitlab-org/gitlab-foss/-/merge_requests/7", @@ -176,7 +175,6 @@ Example Response: }, "merge_when_pipeline_succeeds": false, "merge_status": "cannot_be_merged", - "subscribed": true, "user_notes_count": 7 }, "target_url": "https://gitlab.example.com/gitlab-org/gitlab-foss/-/merge_requests/7", diff --git a/doc/ci/examples/artifactory_and_gitlab/index.md b/doc/ci/examples/artifactory_and_gitlab/index.md deleted file mode 100644 index a1a7de26cf2..00000000000 --- a/doc/ci/examples/artifactory_and_gitlab/index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -redirect_to: '../README.md#contributed-examples' ---- - -This document was moved to [another location](../README.md#contributed-examples). - -<!-- This redirect file can be deleted after 2021-04-18. --> -<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md deleted file mode 100644 index a1a7de26cf2..00000000000 --- a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -redirect_to: '../README.md#contributed-examples' ---- - -This document was moved to [another location](../README.md#contributed-examples). - -<!-- This redirect file can be deleted after 2021-04-18. --> -<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md deleted file mode 100644 index 9408c26f06f..00000000000 --- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -redirect_to: '../README.md#contributed-examples' ---- - -This document was moved to [another location](../README.md#contributed-examples). - -<!-- This redirect file can be deleted after 2021-04-19. --> -<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md deleted file mode 100644 index 057b950289d..00000000000 --- a/doc/ci/examples/test-scala-application.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -redirect_to: 'README.md#contributed-examples' ---- - -This document was moved to [another location](README.md#contributed-examples). - -<!-- This redirect file can be deleted after 2021-04-18. --> -<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md deleted file mode 100644 index 057b6ec126f..00000000000 --- a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -redirect_to: '../README.md' ---- - -This example is no longer available. [View other examples](../README.md). - -<!-- This redirect file can be deleted after <2021-04-05>. --> -<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/ci/variables/deprecated_variables.md b/doc/ci/variables/deprecated_variables.md deleted file mode 100644 index 8d23ec1fd97..00000000000 --- a/doc/ci/variables/deprecated_variables.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -redirect_to: 'README.md' ---- - -This documentation page was removed. For information about variables, see [GitLab CI/CD environment variables](README.md) - -<!-- This redirect file can be deleted after 2021-04-14. --> -<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/ci/yaml/visualization.md b/doc/ci/yaml/visualization.md deleted file mode 100644 index ff3b0456eca..00000000000 --- a/doc/ci/yaml/visualization.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -redirect_to: '../pipeline_editor/index.md#visualize-ci-configuration' ---- - -This document was moved to [another location](../pipeline_editor/index.md#visualize-ci-configuration). - -<!-- This redirect file can be deleted after 2021-04-13. --> -<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/development/product_analytics/snowplow.md b/doc/development/product_analytics/snowplow.md deleted file mode 100644 index 4e2f6530126..00000000000 --- a/doc/development/product_analytics/snowplow.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -redirect_to: '../snowplow/index.md' ---- - -This document was moved to [another location](../snowplow/index.md). - -<!-- This redirect file can be deleted after April 1, 2021. --> -<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/development/product_analytics/usage_ping.md b/doc/development/product_analytics/usage_ping.md deleted file mode 100644 index 43acf5b7e3f..00000000000 --- a/doc/development/product_analytics/usage_ping.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -redirect_to: '../usage_ping/index.md' ---- - -This document was moved to [another location](../usage_ping/index.md). - -<!-- This redirect file can be deleted after April 1, 2021. --> -<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md index e0cfc3cba80..234e3d7b5f1 100644 --- a/doc/development/usage_ping/dictionary.md +++ b/doc/development/usage_ping/dictionary.md @@ -4692,7 +4692,7 @@ Tiers: `free`, `premium`, `ultimate` Projects with repository mirroring enabled -[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216181920_projects_mirrored_with_pipelines_enabled.yml) +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_all/20210216181920_projects_mirrored_with_pipelines_enabled.yml) Group: `group::continuous integration` @@ -10532,6 +10532,30 @@ Status: `implemented` Tiers: `premium`, `ultimate` +### `redis_hll_counters.epics_usage.g_project_management_users_removing_epic_emoji_monthly` + +Counts of MAU removing emoji on epic + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210505071850_g_project_management_users_removing_epic_emoji_monthly.yml) + +Group: `group::product planning` + +Status: `implemented` + +Tiers: `premium`, `ultimate` + +### `redis_hll_counters.epics_usage.g_project_management_users_removing_epic_emoji_weekly` + +Counts of WAU removing emoji on epic + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210505071932_g_project_management_users_removing_epic_emoji_weekly.yml) + +Group: `group::product planning` + +Status: `implemented` + +Tiers: `premium`, `ultimate` + ### `redis_hll_counters.epics_usage.g_project_management_users_setting_epic_confidential_monthly` Count of MAU making epics confidential @@ -16360,7 +16384,7 @@ Tiers: `free` Count creator_id from projects with repository mirroring enabled. -[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216181934_projects_mirrored_with_pipelines_enabled.yml) +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_all/20210216181934_projects_mirrored_with_pipelines_enabled.yml) Group: `group::continuous integration` @@ -18280,7 +18304,7 @@ Tiers: `free` Count creator_id from projects with repository mirroring enabled. -[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210216181943_projects_mirrored_with_pipelines_enabled.yml) +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210216181943_projects_mirrored_with_pipelines_enabled.yml) Group: `group::continuous integration` diff --git a/doc/operations/incident_management/alert_notifications.md b/doc/operations/incident_management/alert_notifications.md deleted file mode 100644 index 4f46c2bec71..00000000000 --- a/doc/operations/incident_management/alert_notifications.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -redirect_to: 'paging.md' ---- - -This document was moved to [another location](paging.md). - -<!-- This redirect file can be deleted after 2021-04-21 --> -<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/topics/web_application_firewall/index.md b/doc/topics/web_application_firewall/index.md deleted file mode 100644 index 297b2f7eaaa..00000000000 --- a/doc/topics/web_application_firewall/index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -redirect_to: '../../user/project/clusters/protect/web_application_firewall/index.md' ---- - -This document was moved to [another location](../../user/project/clusters/protect/web_application_firewall/index.md). - -<!-- This redirect file can be deleted after <2021-04-01>. --> -<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/topics/web_application_firewall/quick_start_guide.md b/doc/topics/web_application_firewall/quick_start_guide.md deleted file mode 100644 index 4d7244f88fa..00000000000 --- a/doc/topics/web_application_firewall/quick_start_guide.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -redirect_to: '../../user/project/clusters/protect/web_application_firewall/quick_start_guide.md' ---- - -This document was moved to [another location](../../user/project/clusters/protect/web_application_firewall/quick_start_guide.md). - -<!-- This redirect file can be deleted after <2021-04-01>. --> -<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/user/project/clusters/securing.md b/doc/user/project/clusters/securing.md deleted file mode 100644 index d734db6bac9..00000000000 --- a/doc/user/project/clusters/securing.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -redirect_to: 'protect/index.md' ---- - -This document was moved to [another location](protect/index.md). - -<!-- This redirect file can be deleted after <2021-04-01>. --> -<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/user/project/file_lock.md b/doc/user/project/file_lock.md index b51a1b79a13..576de63d00d 100644 --- a/doc/user/project/file_lock.md +++ b/doc/user/project/file_lock.md @@ -15,7 +15,7 @@ Although branching strategies usually work well enough for source code and plain text because different versions can be merged together, they do not work for binary files. -When file locking is setup, lockable files are **read only** by default. +When file locking is setup, lockable files are **read-only** by default. When a file is locked, only the user who locked the file may modify it. This user is said to "hold the lock" or have "taken the lock", since only one user diff --git a/doc/user/project/working_with_projects.md b/doc/user/project/working_with_projects.md index 43df1cce70f..ddca0b64f81 100644 --- a/doc/user/project/working_with_projects.md +++ b/doc/user/project/working_with_projects.md @@ -41,7 +41,8 @@ For a list of words that can't be used as project names see To create a new blank project on the **New project** page: -1. On the **Blank project** tab, provide the following information: +1. Click **Create blank project** +1. Provide the following information: - The name of your project in the **Project name** field. You can't use special characters, but you can use spaces, hyphens, underscores, or even emoji. When adding the name, the **Project slug** auto populates. @@ -86,7 +87,8 @@ Built-in templates are project templates that are: To use a built-in template on the **New project** page: -1. On the **Create from template** tab, select the **Built-in** tab. +1. Click **Create from template** +1. Select the **Built-in** tab. 1. From the list of available built-in templates, click the: - **Preview** button to look at the template source itself. - **Use template** button to start creating the project. @@ -99,7 +101,8 @@ GitLab is developing Enterprise templates to help you streamline audit managemen To create a new project with an Enterprise template, on the **New project** page: -1. On the **Create from template** tab, select the **Built-in** tab. +1. Click **Create from template** +1. Select the **Built-in** tab. 1. From the list of available built-in Enterprise templates, click the: - **Preview** button to look at the template source itself. - **Use template** button to start creating the project. @@ -123,11 +126,12 @@ quickly starting projects. Custom projects are available at the [instance-level](../../user/admin_area/custom_project_templates.md) from the **Instance** tab, or at the [group-level](../../user/group/custom_project_templates.md) -from the **Group** tab, under the **Create from template** tab. +from the **Group** tab, on the **Create from template** page. To use a custom project template on the **New project** page: -1. On the **Create from template** tab, select the **Instance** tab or the **Group** tab. +1. Click **Create from template** +1. Select the **Instance** tab or the **Group** tab. 1. From the list of available custom templates, click the: - **Preview** button to look at the template source itself. - **Use template** button to start creating the project. diff --git a/lib/api/issue_links.rb b/lib/api/issue_links.rb index 1cd5bde224b..0b4f4e06d0b 100644 --- a/lib/api/issue_links.rb +++ b/lib/api/issue_links.rb @@ -21,12 +21,12 @@ module API related_issues = source_issue.related_issues(current_user) do |issues| issues.with_api_entity_associations.preload_awardable end - related_issues.each { |issue| issue.lazy_subscription(current_user, user_project) } # preload subscriptions present related_issues, with: Entities::RelatedIssue, current_user: current_user, - project: user_project + project: user_project, + include_subscribed: false end desc 'Relate issues' do diff --git a/lib/api/todos.rb b/lib/api/todos.rb index afc1525cbe2..06e6541164f 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -79,7 +79,7 @@ module API next unless collection targets = collection.map(&:target) - options[type] = { issuable_metadata: Gitlab::IssuableMetadata.new(current_user, targets).data } + options[type] = { issuable_metadata: Gitlab::IssuableMetadata.new(current_user, targets).data, include_subscribed: false } end end end diff --git a/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml index 7e2828d010f..6b9db1c2e0f 100644 --- a/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml @@ -56,6 +56,6 @@ apply: - terraform apply -input=false $PLAN dependencies: - plan - when: manual - only: - - master + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + when: manual diff --git a/lib/gitlab/database/loose_index_scan_distinct_count.rb b/lib/gitlab/database/loose_index_scan_distinct_count.rb index 0c8a9ad25d9..26be07f91c4 100644 --- a/lib/gitlab/database/loose_index_scan_distinct_count.rb +++ b/lib/gitlab/database/loose_index_scan_distinct_count.rb @@ -11,7 +11,7 @@ module Gitlab # This query will read each element in the index matching the project_id filter. # If for a project_id has 100_000 issues, all 100_000 elements will be read. # - # A loose index scan will read only one entry from the index for each project_id to reduce the number of disk reads. + # A loose index scan will only read one entry from the index for each project_id to reduce the number of disk reads. # # Usage: # diff --git a/lib/gitlab/usage_data_counters/known_events/epic_events.yml b/lib/gitlab/usage_data_counters/known_events/epic_events.yml index 4d77bd01878..ccf385bbdfd 100644 --- a/lib/gitlab/usage_data_counters/known_events/epic_events.yml +++ b/lib/gitlab/usage_data_counters/known_events/epic_events.yml @@ -63,6 +63,12 @@ aggregation: daily feature_flag: track_epics_activity +- name: g_project_management_users_removing_epic_emoji + category: epics_usage + redis_slot: project_management + aggregation: daily + feature_flag: track_epics_activity + # start date events - name: g_project_management_users_setting_epic_start_date_as_fixed diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1614049afaf..45cb69a93f4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1463,9 +1463,6 @@ msgstr "" msgid "A project containing issues for each audit inquiry in the HIPAA Audit Protocol published by the U.S. Department of Health & Human Services" msgstr "" -msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}." -msgstr "" - msgid "A project’s repository name defines its URL (the one you use to access the project via a browser) and its place on the file disk where GitLab is installed. %{link_start}Learn more.%{link_end}" msgstr "" @@ -3184,9 +3181,6 @@ msgstr "" msgid "All epics" msgstr "" -msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings." -msgstr "" - msgid "All groups and projects" msgstr "" @@ -3502,6 +3496,9 @@ msgstr "" msgid "An error occurred while fetching markdown preview" msgstr "" +msgid "An error occurred while fetching participants" +msgstr "" + msgid "An error occurred while fetching participants." msgstr "" @@ -3595,9 +3592,6 @@ msgstr "" msgid "An error occurred while loading merge requests." msgstr "" -msgid "An error occurred while loading project creation UI" -msgstr "" - msgid "An error occurred while loading the access tokens form, please try again." msgstr "" @@ -4170,16 +4164,13 @@ msgstr "" msgid "Archived in this version" msgstr "" -msgid "Archived project! Repository and other project resources are read only" -msgstr "" - msgid "Archived project! Repository and other project resources are read-only" msgstr "" msgid "Archived projects" msgstr "" -msgid "Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}" +msgid "Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}" msgstr "" msgid "Are you ABSOLUTELY SURE you wish to delete this project?" @@ -5592,9 +5583,6 @@ msgstr "" msgid "CI/CD configuration file" msgstr "" -msgid "CI/CD for external repo" -msgstr "" - msgid "CICDAnalytics|%{percent}%{percentSymbol}" msgstr "" @@ -9250,9 +9238,6 @@ msgstr "" msgid "Create a personal access token on your account to pull or push via %{protocol}." msgstr "" -msgid "Create a project pre-populated with the necessary files to get you started quickly." -msgstr "" - msgid "Create an account using:" msgstr "" @@ -12989,9 +12974,6 @@ msgstr "" msgid "Errors:" msgstr "" -msgid "Escalation Policies" -msgstr "" - msgid "Escalation policies" msgstr "" @@ -17356,9 +17338,6 @@ msgstr "" msgid "Inform users without uploaded SSH keys that they can't push over SSH until one is added" msgstr "" -msgid "Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}." -msgstr "" - msgid "Infrastructure" msgstr "" @@ -20925,9 +20904,6 @@ msgstr "" msgid "Middleman project with Static Site Editor support" msgstr "" -msgid "Migrate your data from an external source like GitHub, Bitbucket, or another instance of GitLab." -msgstr "" - msgid "Migrated %{success_count}/%{total_count} files." msgstr "" @@ -23404,9 +23380,6 @@ msgstr "" msgid "Pages Domain" msgstr "" -msgid "Pages getting started guide" -msgstr "" - msgid "Pagination|Go to first page" msgstr "" @@ -25828,12 +25801,6 @@ msgstr "" msgid "ProjectsNew|Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository." msgstr "" -msgid "ProjectsNew|Blank" -msgstr "" - -msgid "ProjectsNew|Blank project" -msgstr "" - msgid "ProjectsNew|Connect your external repository to GitLab CI/CD." msgstr "" @@ -25846,6 +25813,9 @@ msgstr "" msgid "ProjectsNew|Create a blank project to house your files, plan your work, and collaborate on code, among other things." msgstr "" +msgid "ProjectsNew|Create a project pre-populated with the necessary files to get you started quickly." +msgstr "" + msgid "ProjectsNew|Create blank project" msgstr "" @@ -25858,9 +25828,6 @@ msgstr "" msgid "ProjectsNew|Create new project" msgstr "" -msgid "ProjectsNew|Creating project & repository." -msgstr "" - msgid "ProjectsNew|Description format" msgstr "" @@ -25876,10 +25843,10 @@ msgstr "" msgid "ProjectsNew|Initialize repository with a README" msgstr "" -msgid "ProjectsNew|No import options available" +msgid "ProjectsNew|Migrate your data from an external source like GitHub, Bitbucket, or another instance of GitLab." msgstr "" -msgid "ProjectsNew|Please wait a moment, this page will automatically refresh when ready." +msgid "ProjectsNew|No import options available" msgstr "" msgid "ProjectsNew|Project description %{tag_start}(optional)%{tag_end}" @@ -25888,9 +25855,6 @@ msgstr "" msgid "ProjectsNew|Run CI/CD for external repository" msgstr "" -msgid "ProjectsNew|Template" -msgstr "" - msgid "ProjectsNew|Visibility Level" msgstr "" @@ -31160,6 +31124,9 @@ msgstr "" msgid "SuperSonics|Type" msgstr "" +msgid "SuperSonics|Upload a legacy license" +msgstr "" + msgid "SuperSonics|Valid From" msgstr "" @@ -33489,9 +33456,6 @@ msgstr[1] "" msgid "Time|s" msgstr "" -msgid "Tip:" -msgstr "" - msgid "Tip: Hover over a job to see the jobs it depends on to run." msgstr "" @@ -37447,9 +37411,6 @@ msgstr "" msgid "already shared with this group" msgstr "" -msgid "among other things" -msgstr "" - msgid "and" msgstr "" @@ -38103,7 +38064,7 @@ msgstr "" msgid "is not valid. The iteration group has to match the iteration cadence group." msgstr "" -msgid "is read only" +msgid "is read-only" msgstr "" msgid "is too long (%{current_value}). The maximum size is %{max_size}." @@ -260,7 +260,6 @@ module QA module Project autoload :New, 'qa/page/project/new' - autoload :NewExperiment, 'qa/page/project/new_experiment' autoload :Show, 'qa/page/project/show' autoload :Activity, 'qa/page/project/activity' autoload :Menu, 'qa/page/project/menu' diff --git a/qa/qa/flow/project.rb b/qa/qa/flow/project.rb index 8a9e2c86332..397806b33a3 100644 --- a/qa/qa/flow/project.rb +++ b/qa/qa/flow/project.rb @@ -6,11 +6,7 @@ module QA module_function def go_to_create_project_from_template - if Page::Project::NewExperiment.perform(&:shown?) - Page::Project::NewExperiment.perform(&:click_create_from_template_link) - else - Page::Project::New.perform(&:click_create_from_template_tab) - end + Page::Project::New.perform(&:click_create_from_template_link) end end end diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index 9da6bb75eb8..ae076e0bbe7 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -8,11 +8,6 @@ module QA include Page::Component::Select2 include Page::Component::VisibilitySetting - view 'app/views/projects/new.html.haml' do - element :project_create_from_template_tab - element :import_project_tab, "Import project" # rubocop:disable QA/ElementWithPattern - end - view 'app/views/projects/_new_project_fields.html.haml' do element :initialize_with_readme_checkbox element :project_namespace_select @@ -29,6 +24,19 @@ module QA element :template_option_row end + view 'app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue' do + element :blank_project_link, ':data-qa-selector="`${panel.name}_link`"' # rubocop:disable QA/ElementWithPattern + element :create_from_template_link, ':data-qa-selector="`${panel.name}_link`"' # rubocop:disable QA/ElementWithPattern + end + + def click_blank_project_link + click_element :blank_project_link + end + + def click_create_from_template_link + click_element :create_from_template_link + end + def choose_test_namespace choose_namespace(Runtime::Namespace.path) end @@ -60,6 +68,10 @@ module QA click_element(:project_create_from_template_tab) end + def set_visibility(visibility) + choose visibility.capitalize + end + def click_github_link click_link 'GitHub' end diff --git a/qa/qa/page/project/new_experiment.rb b/qa/qa/page/project/new_experiment.rb deleted file mode 100644 index 813f7f6cefe..00000000000 --- a/qa/qa/page/project/new_experiment.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -module QA - module Page - module Project - class NewExperiment < Page::Base - view 'app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue' do - element :blank_project_link, ':data-qa-selector="`${panel.name}_link`"' # rubocop:disable QA/ElementWithPattern - element :create_from_template_link, ':data-qa-selector="`${panel.name}_link`"' # rubocop:disable QA/ElementWithPattern - end - - def shown? - has_element? :blank_project_link - end - - def click_blank_project_link - click_element :blank_project_link - end - - def click_create_from_template_link - click_element :create_from_template_link - end - end - end - end -end diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index aaa882cffde..96a60381146 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -83,7 +83,7 @@ module QA end end - Page::Project::NewExperiment.perform(&:click_blank_project_link) if Page::Project::NewExperiment.perform(&:shown?) + Page::Project::New.perform(&:click_blank_project_link) Page::Project::New.perform do |new_page| new_page.choose_test_namespace diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb index 61e7efbc56c..603e757096f 100644 --- a/spec/features/admin/admin_appearance_spec.rb +++ b/spec/features/admin/admin_appearance_spec.rb @@ -37,7 +37,7 @@ RSpec.describe 'Admin Appearance' do expect_custom_sign_in_appearance(appearance) end - it 'preview new project page appearance' do + it 'preview new project page appearance', :js do sign_in(admin) gitlab_enable_admin_mode_sign_in(admin) @@ -86,10 +86,11 @@ RSpec.describe 'Admin Appearance' do expect_custom_sign_in_appearance(appearance) end - it 'custom new project page' do + it 'custom new project page', :js do sign_in(admin) gitlab_enable_admin_mode_sign_in(admin) visit new_project_path + find('[data-qa-selector="blank_project_link"]').click expect_custom_new_project_appearance(appearance) end diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index 7119039d5ff..e1fbe2da9f1 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -25,8 +25,8 @@ RSpec.describe 'New project', :js do expect(page).to have_no_selector('a', text: 'New project/repository') end - expect(page).to have_selector('.blank-state-title', text: 'Create blank project') - expect(page).to have_no_selector('.blank-state-title', text: 'Create blank project/repository') + expect(page).to have_selector('h3', text: 'Create blank project') + expect(page).to have_no_selector('h3', text: 'Create blank project/repository') end it 'when in candidate renders "project/repository"' do @@ -40,7 +40,7 @@ RSpec.describe 'New project', :js do expect(page).to have_selector('a', text: 'New project/repository') end - expect(page).to have_selector('.blank-state-title', text: 'Create blank project/repository') + expect(page).to have_selector('h3', text: 'Create blank project/repository') end context 'with combined_menu feature disabled' do @@ -240,7 +240,7 @@ RSpec.describe 'New project', :js do find('[data-qa-selector="import_project_link"]').click first('.js-import-git-toggle-button').click - page.within '.toggle-import-form' do + page.within '#import-project-pane' do expect(page).not_to have_css('input#project_initialize_with_readme') expect(page).not_to have_content('Initialize repository with a README') end diff --git a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js index 2fd8e524e7a..0948b08f942 100644 --- a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js +++ b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js @@ -90,7 +90,7 @@ describe('Edit feature flag form', () => { expect(wrapper.find(GlToggle).props('value')).toBe(true); }); - it('should alert users the flag is read only', () => { + it('should alert users the flag is read-only', () => { expect(findAlert().text()).toContain('GitLab is moving to a new way of managing feature flags'); }); diff --git a/spec/frontend/feature_flags/components/form_spec.js b/spec/frontend/feature_flags/components/form_spec.js index 00d557c11cf..6c3fce68618 100644 --- a/spec/frontend/feature_flags/components/form_spec.js +++ b/spec/frontend/feature_flags/components/form_spec.js @@ -281,7 +281,7 @@ describe('feature flag form', () => { }); }); - it('renders read only name', () => { + it('renders read-only name', () => { expect(wrapper.find('.js-scope-all').exists()).toEqual(true); }); }); diff --git a/spec/frontend/pages/projects/new/components/app_spec.js b/spec/frontend/pages/projects/new/components/app_spec.js new file mode 100644 index 00000000000..b604e636243 --- /dev/null +++ b/spec/frontend/pages/projects/new/components/app_spec.js @@ -0,0 +1,77 @@ +import { shallowMount } from '@vue/test-utils'; +import { assignGitlabExperiment } from 'helpers/experimentation_helper'; +import App from '~/pages/projects/new/components/app.vue'; +import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue'; + +describe('Experimental new project creation app', () => { + let wrapper; + + const findNewNamespacePage = () => wrapper.findComponent(NewNamespacePage); + + const createComponent = (propsData) => { + wrapper = shallowMount(App, { propsData }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('new_repo experiment', () => { + it('passes new_repo experiment', () => { + createComponent(); + + expect(findNewNamespacePage().props().experiment).toBe('new_repo'); + }); + + describe('when in the candidate variant', () => { + assignGitlabExperiment('new_repo', 'candidate'); + + it('has "repository" in the panel title', () => { + createComponent(); + + expect(findNewNamespacePage().props().panels[0].title).toBe( + 'Create blank project/repository', + ); + }); + }); + + describe('when in the control variant', () => { + assignGitlabExperiment('new_repo', 'control'); + + it('has "project" in the panel title', () => { + createComponent(); + + expect(findNewNamespacePage().props().panels[0].title).toBe('Create blank project'); + }); + }); + }); + + it('passes custom new project guideline text to underlying component', () => { + const DEMO_GUIDELINES = 'Demo guidelines'; + const guidelineSelector = '#new-project-guideline'; + createComponent({ + newProjectGuidelines: DEMO_GUIDELINES, + }); + + expect(wrapper.find(guidelineSelector).text()).toBe(DEMO_GUIDELINES); + }); + + it.each` + isCiCdAvailable | outcome + ${false} | ${'do not show CI/CD panel'} + ${true} | ${'show CI/CD panel'} + `('$outcome when isCiCdAvailable is $isCiCdAvailable', ({ isCiCdAvailable }) => { + createComponent({ + isCiCdAvailable, + }); + + expect( + Boolean( + wrapper + .findComponent(NewNamespacePage) + .props() + .panels.find((p) => p.name === 'cicd_for_external_repo'), + ), + ).toBe(isCiCdAvailable); + }); +}); diff --git a/spec/frontend/projects/experiment_new_project_creation/components/new_project_push_tip_popover_spec.js b/spec/frontend/pages/projects/new/components/new_project_push_tip_popover_spec.js index 1ce16640d4a..d4cf8c78600 100644 --- a/spec/frontend/projects/experiment_new_project_creation/components/new_project_push_tip_popover_spec.js +++ b/spec/frontend/pages/projects/new/components/new_project_push_tip_popover_spec.js @@ -1,6 +1,6 @@ import { GlPopover, GlFormInputGroup } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import NewProjectPushTipPopover from '~/projects/experiment_new_project_creation/components/new_project_push_tip_popover.vue'; +import NewProjectPushTipPopover from '~/pages/projects/new/components/new_project_push_tip_popover.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; describe('New project push tip popover', () => { diff --git a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js index 0400547b917..42885bddcaa 100644 --- a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js +++ b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js @@ -123,11 +123,28 @@ describe('Pipeline editor branch switcher', () => { describe('when switching branches', () => { beforeEach(async () => { + jest.spyOn(window.history, 'pushState').mockImplementation(() => {}); mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches); createComponentWithApollo(); await waitForPromises(); }); + it('updates session history when selecting a different branch', async () => { + const branch = findDropdownItems().at(1); + await branch.vm.$emit('click'); + + expect(window.history.pushState).toHaveBeenCalled(); + expect(window.history.pushState.mock.calls[0][2]).toContain(`?branch_name=${branch.text()}`); + }); + + it('does not update session history when selecting current branch', async () => { + const branch = findDropdownItems().at(0); + await branch.vm.$emit('click'); + + expect(branch.text()).toBe(mockDefaultBranch); + expect(window.history.pushState).not.toHaveBeenCalled(); + }); + it('emits the refetchContent event when selecting a different branch', async () => { const branch = findDropdownItems().at(1); diff --git a/spec/frontend/projects/experiment_new_project_creation/components/app_spec.js b/spec/frontend/projects/experiment_new_project_creation/components/app_spec.js deleted file mode 100644 index 204e7a7c394..00000000000 --- a/spec/frontend/projects/experiment_new_project_creation/components/app_spec.js +++ /dev/null @@ -1,144 +0,0 @@ -import { GlBreadcrumb } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { assignGitlabExperiment } from 'helpers/experimentation_helper'; -import App from '~/projects/experiment_new_project_creation/components/app.vue'; -import LegacyContainer from '~/projects/experiment_new_project_creation/components/legacy_container.vue'; -import WelcomePage from '~/projects/experiment_new_project_creation/components/welcome.vue'; - -describe('Experimental new project creation app', () => { - let wrapper; - - const createComponent = (propsData) => { - wrapper = shallowMount(App, { propsData }); - }; - - afterEach(() => { - wrapper.destroy(); - window.location.hash = ''; - wrapper = null; - }); - - const findWelcomePage = () => wrapper.findComponent(WelcomePage); - const findPanel = (panelName) => - findWelcomePage() - .props() - .panels.find((p) => p.name === panelName); - const findPanelHeader = () => wrapper.find('h4'); - - describe('new_repo experiment', () => { - describe('when in the candidate variant', () => { - assignGitlabExperiment('new_repo', 'candidate'); - - it('has "repository" in the panel title', () => { - createComponent(); - - expect(findPanel('blank_project').title).toBe('Create blank project/repository'); - }); - - describe('when hash is not empty on load', () => { - beforeEach(() => { - window.location.hash = '#blank_project'; - createComponent(); - }); - - it('renders "project/repository"', () => { - expect(findPanelHeader().text()).toBe('Create blank project/repository'); - }); - }); - }); - - describe('when in the control variant', () => { - assignGitlabExperiment('new_repo', 'control'); - - it('has "project" in the panel title', () => { - createComponent(); - - expect(findPanel('blank_project').title).toBe('Create blank project'); - }); - - describe('when hash is not empty on load', () => { - beforeEach(() => { - window.location.hash = '#blank_project'; - createComponent(); - }); - - it('renders "project"', () => { - expect(findPanelHeader().text()).toBe('Create blank project'); - }); - }); - }); - }); - - describe('with empty hash', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders welcome page', () => { - expect(wrapper.find(WelcomePage).exists()).toBe(true); - }); - - it('does not render breadcrumbs', () => { - expect(wrapper.find(GlBreadcrumb).exists()).toBe(false); - }); - }); - - it('renders blank project container if there are errors', () => { - createComponent({ hasErrors: true }); - expect(wrapper.find(WelcomePage).exists()).toBe(false); - expect(wrapper.find(LegacyContainer).exists()).toBe(true); - }); - - describe('when hash is not empty on load', () => { - beforeEach(() => { - window.location.hash = '#blank_project'; - createComponent(); - }); - - it('renders relevant container', () => { - expect(wrapper.find(WelcomePage).exists()).toBe(false); - expect(wrapper.find(LegacyContainer).exists()).toBe(true); - }); - - it('renders breadcrumbs', () => { - expect(wrapper.find(GlBreadcrumb).exists()).toBe(true); - }); - }); - - describe('display custom new project guideline text', () => { - beforeEach(() => { - window.location.hash = '#blank_project'; - }); - - it('does not render new project guideline if undefined', () => { - createComponent(); - expect(wrapper.find('div#new-project-guideline').exists()).toBe(false); - }); - - it('render new project guideline if defined', () => { - const guidelineSelector = 'div#new-project-guideline'; - - createComponent({ - newProjectGuidelines: '<h4>Internal Guidelines</h4><p>lorem ipsum</p>', - }); - expect(wrapper.find(guidelineSelector).exists()).toBe(true); - expect(wrapper.find(guidelineSelector).html()).toContain('<h4>Internal Guidelines</h4>'); - expect(wrapper.find(guidelineSelector).html()).toContain('<p>lorem ipsum</p>'); - }); - }); - - it('renders relevant container when hash changes', () => { - createComponent(); - expect(wrapper.find(WelcomePage).exists()).toBe(true); - - window.location.hash = '#blank_project'; - const ev = document.createEvent('HTMLEvents'); - ev.initEvent('hashchange', false, false); - window.dispatchEvent(ev); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(WelcomePage).exists()).toBe(false); - expect(wrapper.find(LegacyContainer).exists()).toBe(true); - }); - }); -}); diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js index 3fbd863ab40..2e665a494f2 100644 --- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js +++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js @@ -208,6 +208,22 @@ describe('Sidebar assignees widget', () => { ]); }); + it('does not trigger mutation or fire event when editing and exiting without making changes', async () => { + createComponent(); + + await waitForPromises(); + + findEditableItem().vm.$emit('open'); + + await waitForPromises(); + + findEditableItem().vm.$emit('close'); + + expect(findEditableItem().props('isDirty')).toBe(false); + expect(updateIssueAssigneesMutationSuccess).toHaveBeenCalledTimes(0); + expect(wrapper.emitted('assignees-updated')).toBe(undefined); + }); + describe('when expanded', () => { beforeEach(async () => { createComponent(); diff --git a/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js b/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js new file mode 100644 index 00000000000..57b9a10b23e --- /dev/null +++ b/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js @@ -0,0 +1,89 @@ +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import Participants from '~/sidebar/components/participants/participants.vue'; +import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue'; +import epicParticipantsQuery from '~/sidebar/queries/epic_participants.query.graphql'; +import { epicParticipantsResponse } from '../../mock_data'; + +Vue.use(VueApollo); + +describe('Sidebar Participants Widget', () => { + let wrapper; + let fakeApollo; + + const findParticipants = () => wrapper.findComponent(Participants); + + const createComponent = ({ + participantsQueryHandler = jest.fn().mockResolvedValue(epicParticipantsResponse()), + } = {}) => { + fakeApollo = createMockApollo([[epicParticipantsQuery, participantsQueryHandler]]); + + wrapper = shallowMount(SidebarParticipantsWidget, { + apolloProvider: fakeApollo, + propsData: { + fullPath: 'group', + iid: '1', + issuableType: 'epic', + }, + stubs: { + Participants, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + fakeApollo = null; + }); + + it('passes a `loading` prop as true to child component when query is loading', () => { + createComponent(); + + expect(findParticipants().props('loading')).toBe(true); + }); + + describe('when participants are loaded', () => { + beforeEach(() => { + createComponent({ + participantsQueryHandler: jest.fn().mockResolvedValue(epicParticipantsResponse()), + }); + return waitForPromises(); + }); + + it('passes a `loading` prop as false to editable item', () => { + expect(findParticipants().props('loading')).toBe(false); + }); + + it('passes participants to child component', () => { + expect(findParticipants().props('participants')).toEqual( + epicParticipantsResponse().data.workspace.issuable.participants.nodes, + ); + }); + }); + + describe('when error occurs', () => { + it('emits error event with correct parameters', async () => { + const mockError = new Error('mayday'); + + createComponent({ + participantsQueryHandler: jest.fn().mockRejectedValue(mockError), + }); + + await waitForPromises(); + + const [ + [ + { + message, + error: { networkError }, + }, + ], + ] = wrapper.emitted('fetch-error'); + expect(message).toBe(wrapper.vm.$options.i18n.fetchingError); + expect(networkError).toEqual(mockError); + }); + }); +}); diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js index 8a969d64467..b052038661a 100644 --- a/spec/frontend/sidebar/mock_data.js +++ b/spec/frontend/sidebar/mock_data.js @@ -262,6 +262,31 @@ export const issuableStartDateResponse = (startDate = null) => ({ }, }); +export const epicParticipantsResponse = () => ({ + data: { + workspace: { + __typename: 'Group', + issuable: { + __typename: 'Epic', + id: 'gid://gitlab/Epic/4', + participants: { + nodes: [ + { + id: 'gid://gitlab/User/2', + avatarUrl: + 'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon', + name: 'Jacki Kub', + username: 'francina.skiles', + webUrl: '/franc', + status: null, + }, + ], + }, + }, + }, + }, +}); + export const issueReferenceResponse = (reference) => ({ data: { workspace: { diff --git a/spec/frontend/projects/experiment_new_project_creation/components/legacy_container_spec.js b/spec/frontend/vue_shared/new_namespace/components/legacy_container_spec.js index 6fc36d6362c..52f36aa0e77 100644 --- a/spec/frontend/projects/experiment_new_project_creation/components/legacy_container_spec.js +++ b/spec/frontend/vue_shared/new_namespace/components/legacy_container_spec.js @@ -1,6 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import LegacyContainer from '~/projects/experiment_new_project_creation/components/legacy_container.vue'; +import LegacyContainer from '~/vue_shared/new_namespace/components/legacy_container.vue'; describe('Legacy container component', () => { let wrapper; diff --git a/spec/frontend/projects/experiment_new_project_creation/components/welcome_spec.js b/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js index 9fd1230806e..602213fca83 100644 --- a/spec/frontend/projects/experiment_new_project_creation/components/welcome_spec.js +++ b/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js @@ -3,8 +3,7 @@ import { nextTick } from 'vue'; import { mockTracking } from 'helpers/tracking_helper'; import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants'; import { getExperimentData } from '~/experimentation/utils'; -import NewProjectPushTipPopover from '~/projects/experiment_new_project_creation/components/new_project_push_tip_popover.vue'; -import WelcomePage from '~/projects/experiment_new_project_creation/components/welcome.vue'; +import WelcomePage from '~/vue_shared/new_namespace/components/welcome.vue'; jest.mock('~/experimentation/utils', () => ({ getExperimentData: jest.fn() })); @@ -12,8 +11,18 @@ describe('Welcome page', () => { let wrapper; let trackingSpy; - const createComponent = (propsData) => { - wrapper = shallowMount(WelcomePage, { propsData }); + const DEFAULT_PROPS = { + title: 'Create new something', + }; + + const createComponent = ({ propsData, slots }) => { + wrapper = shallowMount(WelcomePage, { + slots, + propsData: { + ...DEFAULT_PROPS, + ...propsData, + }, + }); }; beforeEach(() => { @@ -29,7 +38,7 @@ describe('Welcome page', () => { }); it('tracks link clicks', async () => { - createComponent({ panels: [{ name: 'test', href: '#' }] }); + createComponent({ propsData: { experiment: 'foo', panels: [{ name: 'test', href: '#' }] } }); const link = wrapper.find('a'); link.trigger('click'); await nextTick(); @@ -38,11 +47,11 @@ describe('Welcome page', () => { }); }); - it('adds new_repo experiment data if in experiment', async () => { + it('adds experiment data if in experiment', async () => { const mockExperimentData = 'data'; getExperimentData.mockReturnValue(mockExperimentData); - createComponent({ panels: [{ name: 'test', href: '#' }] }); + createComponent({ propsData: { experiment: 'foo', panels: [{ name: 'test', href: '#' }] } }); const link = wrapper.find('a'); link.trigger('click'); await nextTick(); @@ -57,12 +66,13 @@ describe('Welcome page', () => { }); }); - it('renders new project push tip popover', () => { - createComponent({ panels: [{ name: 'test', href: '#' }] }); - - const popover = wrapper.findComponent(NewProjectPushTipPopover); + it('renders footer slot if provided', () => { + const DUMMY = 'Test message'; + createComponent({ + slots: { footer: DUMMY }, + propsData: { panels: [{ name: 'test', href: '#' }] }, + }); - expect(popover.exists()).toBe(true); - expect(popover.props().target()).toBe(wrapper.find({ ref: 'clipTip' }).element); + expect(wrapper.text()).toContain(DUMMY); }); }); diff --git a/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js b/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js new file mode 100644 index 00000000000..30937921900 --- /dev/null +++ b/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js @@ -0,0 +1,114 @@ +import { GlBreadcrumb } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import LegacyContainer from '~/vue_shared/new_namespace/components/legacy_container.vue'; +import WelcomePage from '~/vue_shared/new_namespace/components/welcome.vue'; +import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue'; + +describe('Experimental new project creation app', () => { + let wrapper; + + const findWelcomePage = () => wrapper.findComponent(WelcomePage); + const findLegacyContainer = () => wrapper.findComponent(LegacyContainer); + const findBreadcrumb = () => wrapper.findComponent(GlBreadcrumb); + + const DEFAULT_PROPS = { + title: 'Create something', + initialBreadcrumb: 'Something', + panels: [ + { name: 'panel1', selector: '#some-selector1' }, + { name: 'panel2', selector: '#some-selector2' }, + ], + persistenceKey: 'DEMO-PERSISTENCE-KEY', + }; + + const createComponent = ({ slots, propsData } = {}) => { + wrapper = shallowMount(NewNamespacePage, { + slots, + propsData: { + ...DEFAULT_PROPS, + ...propsData, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + window.location.hash = ''; + }); + + it('passes experiment to welcome component if provided', () => { + const EXPERIMENT = 'foo'; + createComponent({ propsData: { experiment: EXPERIMENT } }); + + expect(findWelcomePage().props().experiment).toBe(EXPERIMENT); + }); + + describe('with empty hash', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders welcome page', () => { + expect(findWelcomePage().exists()).toBe(true); + }); + + it('does not render breadcrumbs', () => { + expect(findBreadcrumb().exists()).toBe(false); + }); + }); + + it('renders first container if jumpToLastPersistedPanel passed', () => { + createComponent({ propsData: { jumpToLastPersistedPanel: true } }); + expect(findWelcomePage().exists()).toBe(false); + expect(findLegacyContainer().exists()).toBe(true); + }); + + describe('when hash is not empty on load', () => { + beforeEach(() => { + window.location.hash = `#${DEFAULT_PROPS.panels[1].name}`; + createComponent(); + }); + + it('renders relevant container', () => { + expect(findWelcomePage().exists()).toBe(false); + + const container = findLegacyContainer(); + + expect(container.exists()).toBe(true); + expect(container.props().selector).toBe(DEFAULT_PROPS.panels[1].selector); + }); + + it('renders breadcrumbs', () => { + const breadcrumb = findBreadcrumb(); + expect(breadcrumb.exists()).toBe(true); + expect(breadcrumb.props().items[0].text).toBe(DEFAULT_PROPS.initialBreadcrumb); + }); + }); + + it('renders extra description if provided', () => { + window.location.hash = `#${DEFAULT_PROPS.panels[1].name}`; + const EXTRA_DESCRIPTION = 'Some extra description'; + createComponent({ + slots: { + 'extra-description': EXTRA_DESCRIPTION, + }, + }); + + expect(wrapper.text()).toContain(EXTRA_DESCRIPTION); + }); + + it('renders relevant container when hash changes', async () => { + createComponent(); + expect(findWelcomePage().exists()).toBe(true); + + window.location.hash = `#${DEFAULT_PROPS.panels[0].name}`; + const ev = document.createEvent('HTMLEvents'); + ev.initEvent('hashchange', false, false); + window.dispatchEvent(ev); + + await nextTick(); + expect(findWelcomePage().exists()).toBe(false); + expect(findLegacyContainer().exists()).toBe(true); + }); +}); diff --git a/spec/frontend/whats_new/components/feature_spec.js b/spec/frontend/whats_new/components/feature_spec.js new file mode 100644 index 00000000000..9e9cb59c0d6 --- /dev/null +++ b/spec/frontend/whats_new/components/feature_spec.js @@ -0,0 +1,46 @@ +import { shallowMount } from '@vue/test-utils'; +import Feature from '~/whats_new/components/feature.vue'; + +describe("What's new single feature", () => { + /** @type {import("@vue/test-utils").Wrapper} */ + let wrapper; + + const exampleFeature = { + title: 'Compliance pipeline configurations', + body: + '<p>We are thrilled to announce that it is now possible to define enforceable pipelines that will run for any project assigned a corresponding compliance framework.</p>', + stage: 'Manage', + 'self-managed': true, + 'gitlab-com': true, + packages: ['Ultimate'], + url: 'https://docs.gitlab.com/ee/user/project/settings/#compliance-pipeline-configuration', + image_url: 'https://img.youtube.com/vi/upLJ_equomw/hqdefault.jpg', + published_at: '2021-04-22T00:00:00.000Z', + release: '13.11', + }; + + const findReleaseDate = () => wrapper.find('[data-testid="release-date"]'); + + const createWrapper = ({ feature } = {}) => { + wrapper = shallowMount(Feature, { + propsData: { feature }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('renders the date', () => { + createWrapper({ feature: exampleFeature }); + expect(findReleaseDate().text()).toBe('April 22, 2021'); + }); + + describe('when the published_at is null', () => { + it("doesn't render the date", () => { + createWrapper({ feature: { ...exampleFeature, published_at: null } }); + expect(findReleaseDate().exists()).toBe(false); + }); + }); +}); diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 992be7258f3..ad6852f63df 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -96,9 +96,31 @@ RSpec.describe GroupsHelper do subject { helper.group_title(very_deep_nested_group) } - it 'outputs the groups in the correct order' do - expect(subject) - .to match(%r{<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*</li>.*<a.*>#{very_deep_nested_group.name}</a>}m) + context 'traversal queries' do + shared_examples 'correct ancestor order' do + it 'outputs the groups in the correct order' do + expect(subject) + .to match(%r{<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*</li>.*<a.*>#{very_deep_nested_group.name}</a>}m) + end + end + + context 'recursive' do + before do + stub_feature_flags(use_traversal_ids: false) + end + + include_examples 'correct ancestor order' + end + + context 'linear' do + before do + stub_feature_flags(use_traversal_ids: true) + + very_deep_nested_group.reload # make sure traversal_ids are reloaded + end + + include_examples 'correct ancestor order' + end end it 'enqueues the elements in the breadcrumb schema list' do @@ -122,101 +144,121 @@ RSpec.describe GroupsHelper do end describe '#share_with_group_lock_help_text' do - let_it_be_with_reload(:root_group) { create(:group) } - let_it_be_with_reload(:subgroup) { create(:group, parent: root_group) } - let_it_be_with_reload(:sub_subgroup) { create(:group, parent: subgroup) } - let_it_be(:root_owner) { create(:user) } - let_it_be(:sub_owner) { create(:user) } - let_it_be(:sub_sub_owner) { create(:user) } - - let(:possible_help_texts) do - { - default_help: "This setting will be applied to all subgroups unless overridden by a group owner", - ancestor_locked_but_you_can_override: %r{This setting is applied on <a .+>.+</a>\. You can override the setting or .+}, - ancestor_locked_so_ask_the_owner: /This setting is applied on .+\. To share projects in this group with another group, ask the owner to override the setting or remove the share with group lock from .+/, - ancestor_locked_and_has_been_overridden: /This setting is applied on .+ and has been overridden on this subgroup/ - } - end - - let(:possible_linked_ancestors) do - { - root_group: root_group, - subgroup: subgroup - } - end - - let(:users) do - { - root_owner: root_owner, - sub_owner: sub_owner, - sub_sub_owner: sub_sub_owner - } - end - - subject { helper.share_with_group_lock_help_text(sub_subgroup) } - - before_all do - root_group.add_owner(root_owner) - subgroup.add_owner(sub_owner) - sub_subgroup.add_owner(sub_sub_owner) - end - - # rubocop:disable Layout/SpaceBeforeComma - where(:root_share_with_group_locked, :subgroup_share_with_group_locked, :sub_subgroup_share_with_group_locked, :current_user, :help_text, :linked_ancestor) do - [ - [false , false , false , :root_owner , :default_help , nil], - [false , false , false , :sub_owner , :default_help , nil], - [false , false , false , :sub_sub_owner , :default_help , nil], - [false , false , true , :root_owner , :default_help , nil], - [false , false , true , :sub_owner , :default_help , nil], - [false , false , true , :sub_sub_owner , :default_help , nil], - [false , true , false , :root_owner , :ancestor_locked_and_has_been_overridden , :subgroup], - [false , true , false , :sub_owner , :ancestor_locked_and_has_been_overridden , :subgroup], - [false , true , false , :sub_sub_owner , :ancestor_locked_and_has_been_overridden , :subgroup], - [false , true , true , :root_owner , :ancestor_locked_but_you_can_override , :subgroup], - [false , true , true , :sub_owner , :ancestor_locked_but_you_can_override , :subgroup], - [false , true , true , :sub_sub_owner , :ancestor_locked_so_ask_the_owner , :subgroup], - [true , false , false , :root_owner , :default_help , nil], - [true , false , false , :sub_owner , :default_help , nil], - [true , false , false , :sub_sub_owner , :default_help , nil], - [true , false , true , :root_owner , :default_help , nil], - [true , false , true , :sub_owner , :default_help , nil], - [true , false , true , :sub_sub_owner , :default_help , nil], - [true , true , false , :root_owner , :ancestor_locked_and_has_been_overridden , :root_group], - [true , true , false , :sub_owner , :ancestor_locked_and_has_been_overridden , :root_group], - [true , true , false , :sub_sub_owner , :ancestor_locked_and_has_been_overridden , :root_group], - [true , true , true , :root_owner , :ancestor_locked_but_you_can_override , :root_group], - [true , true , true , :sub_owner , :ancestor_locked_so_ask_the_owner , :root_group], - [true , true , true , :sub_sub_owner , :ancestor_locked_so_ask_the_owner , :root_group] - ] - end - # rubocop:enable Layout/SpaceBeforeComma + context 'traversal queries' do + let_it_be_with_reload(:root_group) { create(:group) } + let_it_be_with_reload(:subgroup) { create(:group, parent: root_group) } + let_it_be_with_reload(:sub_subgroup) { create(:group, parent: subgroup) } + let_it_be(:root_owner) { create(:user) } + let_it_be(:sub_owner) { create(:user) } + let_it_be(:sub_sub_owner) { create(:user) } + + let(:possible_help_texts) do + { + default_help: "This setting will be applied to all subgroups unless overridden by a group owner", + ancestor_locked_but_you_can_override: %r{This setting is applied on <a .+>.+</a>\. You can override the setting or .+}, + ancestor_locked_so_ask_the_owner: /This setting is applied on .+\. To share projects in this group with another group, ask the owner to override the setting or remove the share with group lock from .+/, + ancestor_locked_and_has_been_overridden: /This setting is applied on .+ and has been overridden on this subgroup/ + } + end - with_them do - before do - root_group.update_column(:share_with_group_lock, true) if root_share_with_group_locked - subgroup.update_column(:share_with_group_lock, true) if subgroup_share_with_group_locked - sub_subgroup.update_column(:share_with_group_lock, true) if sub_subgroup_share_with_group_locked - - allow(helper).to receive(:current_user).and_return(users[current_user]) - allow(helper).to receive(:can?) - .with(users[current_user], :change_share_with_group_lock, subgroup) - .and_return(Ability.allowed?(users[current_user], :change_share_with_group_lock, subgroup)) - - ancestor = possible_linked_ancestors[linked_ancestor] - if ancestor - allow(helper).to receive(:can?) - .with(users[current_user], :read_group, ancestor) - .and_return(Ability.allowed?(users[current_user], :read_group, ancestor)) - allow(helper).to receive(:can?) - .with(users[current_user], :admin_group, ancestor) - .and_return(Ability.allowed?(users[current_user], :admin_group, ancestor)) + let(:possible_linked_ancestors) do + { + root_group: root_group, + subgroup: subgroup + } + end + + let(:users) do + { + root_owner: root_owner, + sub_owner: sub_owner, + sub_sub_owner: sub_sub_owner + } + end + + subject { helper.share_with_group_lock_help_text(sub_subgroup) } + + before_all do + root_group.add_owner(root_owner) + subgroup.add_owner(sub_owner) + sub_subgroup.add_owner(sub_sub_owner) + end + + shared_examples 'correct ancestor order' do + # rubocop:disable Layout/SpaceBeforeComma + where(:root_share_with_group_locked, :subgroup_share_with_group_locked, :sub_subgroup_share_with_group_locked, :current_user, :help_text, :linked_ancestor) do + [ + [false , false , false , :root_owner , :default_help , nil], + [false , false , false , :sub_owner , :default_help , nil], + [false , false , false , :sub_sub_owner , :default_help , nil], + [false , false , true , :root_owner , :default_help , nil], + [false , false , true , :sub_owner , :default_help , nil], + [false , false , true , :sub_sub_owner , :default_help , nil], + [false , true , false , :root_owner , :ancestor_locked_and_has_been_overridden , :subgroup], + [false , true , false , :sub_owner , :ancestor_locked_and_has_been_overridden , :subgroup], + [false , true , false , :sub_sub_owner , :ancestor_locked_and_has_been_overridden , :subgroup], + [false , true , true , :root_owner , :ancestor_locked_but_you_can_override , :subgroup], + [false , true , true , :sub_owner , :ancestor_locked_but_you_can_override , :subgroup], + [false , true , true , :sub_sub_owner , :ancestor_locked_so_ask_the_owner , :subgroup], + [true , false , false , :root_owner , :default_help , nil], + [true , false , false , :sub_owner , :default_help , nil], + [true , false , false , :sub_sub_owner , :default_help , nil], + [true , false , true , :root_owner , :default_help , nil], + [true , false , true , :sub_owner , :default_help , nil], + [true , false , true , :sub_sub_owner , :default_help , nil], + [true , true , false , :root_owner , :ancestor_locked_and_has_been_overridden , :root_group], + [true , true , false , :sub_owner , :ancestor_locked_and_has_been_overridden , :root_group], + [true , true , false , :sub_sub_owner , :ancestor_locked_and_has_been_overridden , :root_group], + [true , true , true , :root_owner , :ancestor_locked_but_you_can_override , :root_group], + [true , true , true , :sub_owner , :ancestor_locked_so_ask_the_owner , :root_group], + [true , true , true , :sub_sub_owner , :ancestor_locked_so_ask_the_owner , :root_group] + ] + end + # rubocop:enable Layout/SpaceBeforeComma + + with_them do + before do + root_group.update_column(:share_with_group_lock, true) if root_share_with_group_locked + subgroup.update_column(:share_with_group_lock, true) if subgroup_share_with_group_locked + sub_subgroup.update_column(:share_with_group_lock, true) if sub_subgroup_share_with_group_locked + + allow(helper).to receive(:current_user).and_return(users[current_user]) + allow(helper).to receive(:can?) + .with(users[current_user], :change_share_with_group_lock, subgroup) + .and_return(Ability.allowed?(users[current_user], :change_share_with_group_lock, subgroup)) + + ancestor = possible_linked_ancestors[linked_ancestor] + if ancestor + allow(helper).to receive(:can?) + .with(users[current_user], :read_group, ancestor) + .and_return(Ability.allowed?(users[current_user], :read_group, ancestor)) + allow(helper).to receive(:can?) + .with(users[current_user], :admin_group, ancestor) + .and_return(Ability.allowed?(users[current_user], :admin_group, ancestor)) + end + end + + it 'has the correct help text with correct ancestor links' do + expect(subject).to match(possible_help_texts[help_text]) + expect(subject).to match(possible_linked_ancestors[linked_ancestor].name) unless help_text == :default_help + end end end - it 'has the correct help text with correct ancestor links' do - expect(subject).to match(possible_help_texts[help_text]) - expect(subject).to match(possible_linked_ancestors[linked_ancestor].name) unless help_text == :default_help + context 'recursive' do + before do + stub_feature_flags(use_traversal_ids: false) + end + + include_examples 'correct ancestor order' + end + + context 'linear' do + before do + stub_feature_flags(use_traversal_ids: true) + end + + include_examples 'correct ancestor order' end end end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 7a578ad3c90..7f06e66ad50 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -779,7 +779,7 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do end.not_to change(user, :failed_attempts) end - context 'when the database is read only' do + context 'when the database is read-only' do before do allow(Gitlab::Database).to receive(:read_only?).and_return(true) end diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index cc9f005b189..8714945dffc 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -395,13 +395,13 @@ RSpec.describe Gitlab::Database do allow(ActiveRecord::Base.connection).to receive(:execute).and_call_original end - it 'detects a read only database' do + it 'detects a read-only database' do allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "t" }]) expect(described_class.db_read_only?).to be_truthy end - it 'detects a read only database' do + it 'detects a read-only database' do allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => true }]) expect(described_class.db_read_only?).to be_truthy diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 2116c9e0128..ae9c697e0b9 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -1034,7 +1034,7 @@ RSpec.describe Gitlab::GitAccess do end end - context 'when the repository is read only' do + context 'when the repository is read-only' do let(:project) { create(:project, :repository, :read_only) } it 'denies push access' do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index abb0b3d0b6f..b701556e735 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -427,6 +427,50 @@ RSpec.describe Group do end end + context 'traversal queries' do + let_it_be(:group, reload: true) { create(:group, :nested) } + + context 'recursive' do + before do + stub_feature_flags(use_traversal_ids: false) + end + + it_behaves_like 'namespace traversal' + + describe '#self_and_descendants' do + it { expect(group.self_and_descendants.to_sql).not_to include 'traversal_ids @>' } + end + + describe '#ancestors' do + it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' } + end + end + + context 'linear' do + it_behaves_like 'namespace traversal' + + describe '#self_and_descendants' do + it { expect(group.self_and_descendants.to_sql).to include 'traversal_ids @>' } + end + + describe '#ancestors' do + it { expect(group.ancestors.to_sql).to include "\"namespaces\".\"id\" = #{group.parent_id}" } + + it 'hierarchy order' do + expect(group.ancestors(hierarchy_order: :asc).to_sql).to include 'ORDER BY "depth" ASC' + end + + context 'ancestor linear queries feature flag disabled' do + before do + stub_feature_flags(use_traversal_ids_for_ancestors: false) + end + + it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' } + end + end + end + end + describe '.without_integration' do let(:another_group) { create(:group) } let(:instance_integration) { build(:jira_service, :instance) } @@ -1798,13 +1842,35 @@ RSpec.describe Group do allow(project).to receive(:protected_for?).with('ref').and_return(true) end - it 'returns all variables belong to the group and parent groups' do - expected_array1 = [protected_variable, ci_variable] - expected_array2 = [variable_child, variable_child_2, variable_child_3] - got_array = group_child_3.ci_variables_for('ref', project).to_a + context 'traversal queries' do + shared_examples 'correct ancestor order' do + it 'returns all variables belong to the group and parent groups' do + expected_array1 = [protected_variable, ci_variable] + expected_array2 = [variable_child, variable_child_2, variable_child_3] + got_array = group_child_3.ci_variables_for('ref', project).to_a + + expect(got_array.shift(2)).to contain_exactly(*expected_array1) + expect(got_array).to eq(expected_array2) + end + end + + context 'recursive' do + before do + stub_feature_flags(use_traversal_ids: false) + end - expect(got_array.shift(2)).to contain_exactly(*expected_array1) - expect(got_array).to eq(expected_array2) + include_examples 'correct ancestor order' + end + + context 'linear' do + before do + stub_feature_flags(use_traversal_ids: true) + + group_child_3.reload # make sure traversal_ids are reloaded + end + + include_examples 'correct ancestor order' + end end end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 7c9e7986aab..e72208474e8 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -936,32 +936,6 @@ RSpec.describe Namespace do end end - context 'when use_traversal_ids feature flag is true' do - let_it_be(:namespace, reload: true) { create(:namespace) } - - it_behaves_like 'namespace traversal' - - describe '#self_and_descendants' do - subject { namespace.self_and_descendants } - - it { expect(subject.to_sql).to include 'traversal_ids @>' } - end - end - - context 'when use_traversal_ids feature flag is false' do - before do - stub_feature_flags(use_traversal_ids: false) - end - - it_behaves_like 'namespace traversal' - - describe '#self_and_descendants' do - subject { namespace.self_and_descendants } - - it { expect(subject.to_sql).not_to include 'traversal_ids @>' } - end - end - describe '#users_with_descendants' do let(:user_a) { create(:user) } let(:user_b) { create(:user) } diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 5d2cfcb7611..6b18d1f0cfa 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -597,23 +597,49 @@ RSpec.describe Service do context 'passing a group' do let!(:sub_subgroup) { create(:group, parent: subgroup) } - it 'creates a service from the subgroup-level integration' do - described_class.create_from_active_default_integrations(sub_subgroup, :group_id) + context 'traversal queries' do + shared_examples 'correct ancestor order' do + it 'creates a service from the subgroup-level integration' do + described_class.create_from_active_default_integrations(sub_subgroup, :group_id) - expect(sub_subgroup.reload.services.size).to eq(1) - expect(sub_subgroup.reload.services.first.api_url).to eq(subgroup_integration.api_url) - expect(sub_subgroup.reload.services.first.inherit_from_id).to eq(subgroup_integration.id) - end + sub_subgroup.reload + + expect(sub_subgroup.services.size).to eq(1) + expect(sub_subgroup.services.first.api_url).to eq(subgroup_integration.api_url) + expect(sub_subgroup.services.first.inherit_from_id).to eq(subgroup_integration.id) + end + + context 'having a service inheriting settings' do + let!(:subgroup_integration) { create(:prometheus_service, group: subgroup, project: nil, inherit_from_id: group_integration.id, api_url: 'https://prometheus.subgroup.com/') } + + it 'creates a service from the group-level integration' do + described_class.create_from_active_default_integrations(sub_subgroup, :group_id) + + sub_subgroup.reload + + expect(sub_subgroup.services.size).to eq(1) + expect(sub_subgroup.services.first.api_url).to eq(group_integration.api_url) + expect(sub_subgroup.services.first.inherit_from_id).to eq(group_integration.id) + end + end + end + + context 'recursive' do + before do + stub_feature_flags(use_traversal_ids: false) + end + + include_examples 'correct ancestor order' + end - context 'having a service inheriting settings' do - let!(:subgroup_integration) { create(:prometheus_service, group: subgroup, project: nil, inherit_from_id: group_integration.id, api_url: 'https://prometheus.subgroup.com/') } + context 'linear' do + before do + stub_feature_flags(use_traversal_ids: true) - it 'creates a service from the group-level integration' do - described_class.create_from_active_default_integrations(sub_subgroup, :group_id) + sub_subgroup.reload # make sure traversal_ids are reloaded + end - expect(sub_subgroup.reload.services.size).to eq(1) - expect(sub_subgroup.reload.services.first.api_url).to eq(group_integration.api_url) - expect(sub_subgroup.reload.services.first.inherit_from_id).to eq(group_integration.id) + include_examples 'correct ancestor order' end end end diff --git a/spec/presenters/group_member_presenter_spec.rb b/spec/presenters/group_member_presenter_spec.rb index 6bd3005fbb6..352f81356e0 100644 --- a/spec/presenters/group_member_presenter_spec.rb +++ b/spec/presenters/group_member_presenter_spec.rb @@ -142,7 +142,7 @@ RSpec.describe GroupMemberPresenter do let(:expected_roles) { { 'Developer' => 30, 'Maintainer' => 40, 'Owner' => 50, 'Reporter' => 20 } } before do - entity.parent = group + entity.update!(parent: group) end end end diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb index da0bae8d5e7..07fa1d40f7b 100644 --- a/spec/requests/api/issues/get_project_issues_spec.rb +++ b/spec/requests/api/issues/get_project_issues_spec.rb @@ -186,7 +186,7 @@ RSpec.describe API::Issues do it 'avoids N+1 queries' do get api("/projects/#{project.id}/issues", user) - create_list(:issue, 3, project: project, closed_by: user) + issues = create_list(:issue, 3, project: project, closed_by: user) control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do get api("/projects/#{project.id}/issues", user) @@ -195,6 +195,9 @@ RSpec.describe API::Issues do milestone = create(:milestone, project: project) create(:issue, project: project, milestone: milestone, closed_by: create(:user)) + create(:note_on_issue, project: project, noteable: issues[0]) + create(:note_on_issue, project: project, noteable: issues[1]) + expect do get api("/projects/#{project.id}/issues", user) end.not_to exceed_all_query_limit(control_count) diff --git a/spec/services/boards/visits/create_service_spec.rb b/spec/services/boards/visits/create_service_spec.rb index a9a8754825b..64faa2cf07b 100644 --- a/spec/services/boards/visits/create_service_spec.rb +++ b/spec/services/boards/visits/create_service_spec.rb @@ -18,7 +18,7 @@ RSpec.describe Boards::Visits::CreateService do expect(service.execute(project_board)).to eq nil end - it 'returns nil when database is read only' do + it 'returns nil when database is read-only' do allow(Gitlab::Database).to receive(:read_only?) { true } expect(service.execute(project_board)).to eq nil diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb index 3a1197970f4..21f59fb5994 100644 --- a/spec/services/groups/transfer_service_spec.rb +++ b/spec/services/groups/transfer_service_spec.rb @@ -240,6 +240,7 @@ RSpec.describe Groups::TransferService do end context 'when the group is allowed to be transferred' do + let_it_be(:new_parent_group, reload: true) { create(:group, :public) } let_it_be(:new_parent_group_integration) { create(:slack_service, group: new_parent_group, project: nil, webhook: 'http://new-group.slack.com') } before do @@ -273,11 +274,10 @@ RSpec.describe Groups::TransferService do end context 'with a group integration' do - let_it_be(:instance_integration) { create(:slack_service, :instance, webhook: 'http://project.slack.com') } - let(:new_created_integration) { Service.find_by(group: group) } context 'with an inherited integration' do + let_it_be(:instance_integration) { create(:slack_service, :instance, webhook: 'http://project.slack.com') } let_it_be(:group_integration) { create(:slack_service, group: group, project: nil, webhook: 'http://group.slack.com', inherit_from_id: instance_integration.id) } it 'replaces inherited integrations', :aggregate_failures do @@ -603,6 +603,7 @@ RSpec.describe Groups::TransferService do create(:group_member, :owner, group: new_parent_group, user: user) create(:group, :private, parent: group, require_two_factor_authentication: true) group.update!(require_two_factor_authentication: true) + new_parent_group.reload # make sure traversal_ids are reloaded end it 'does not update group two factor authentication setting' do diff --git a/spec/services/merge_requests/mergeability_check_service_spec.rb b/spec/services/merge_requests/mergeability_check_service_spec.rb index e0baf5af8b4..32d7991e593 100644 --- a/spec/services/merge_requests/mergeability_check_service_spec.rb +++ b/spec/services/merge_requests/mergeability_check_service_spec.rb @@ -87,7 +87,7 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar described_class.new(merge_request).async_execute end - context 'when read only DB' do + context 'when read-only DB' do before do allow(Gitlab::Database).to receive(:read_only?) { true } end @@ -258,7 +258,7 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar end end - context 'when read only DB' do + context 'when read-only DB' do it 'returns ServiceResponse.error' do allow(Gitlab::Database).to receive(:read_only?) { true } diff --git a/spec/services/projects/update_repository_storage_service_spec.rb b/spec/services/projects/update_repository_storage_service_spec.rb index 828667fdfc2..5b15b7d5f34 100644 --- a/spec/services/projects/update_repository_storage_service_spec.rb +++ b/spec/services/projects/update_repository_storage_service_spec.rb @@ -36,7 +36,7 @@ RSpec.describe Projects::UpdateRepositoryStorageService do end context 'when the move succeeds' do - it 'moves the repository to the new storage and unmarks the repository as read only' do + it 'moves the repository to the new storage and unmarks the repository as read-only' do old_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do project.repository.path_to_repo end diff --git a/spec/services/snippets/update_repository_storage_service_spec.rb b/spec/services/snippets/update_repository_storage_service_spec.rb index 6ba09a9dca9..50b28a5a125 100644 --- a/spec/services/snippets/update_repository_storage_service_spec.rb +++ b/spec/services/snippets/update_repository_storage_service_spec.rb @@ -31,7 +31,7 @@ RSpec.describe Snippets::UpdateRepositoryStorageService do end context 'when the move succeeds' do - it 'moves the repository to the new storage and unmarks the repository as read only' do + it 'moves the repository to the new storage and unmarks the repository as read-only' do old_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do snippet.repository.path_to_repo end diff --git a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb index 5ecc5c08bbd..a4eb6a839c0 100644 --- a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb +++ b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb @@ -35,7 +35,7 @@ RSpec.shared_examples 'issuable notes filter' do get :discussions, params: params.merge(notes_filter: notes_filter) end - it 'does not set notes filter when database is in read only mode' do + it 'does not set notes filter when database is in read-only mode' do allow(Gitlab::Database).to receive(:read_only?).and_return(true) notes_filter = UserPreference::NOTES_FILTERS[:only_comments] diff --git a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb index 819cf6018fe..3f1588c46b3 100644 --- a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb @@ -36,7 +36,7 @@ RSpec.shared_examples 'handles repository moves' do container.set_repository_read_only! expect(subject).not_to be_valid - expect(subject.errors[error_key].first).to match(/is read only/) + expect(subject.errors[error_key].first).to match(/is read-only/) end end end diff --git a/spec/support/shared_examples/namespaces/namespace_traversal_examples.rb b/spec/support/shared_examples/namespaces/traversal_examples.rb index 36e5808fa28..77a1705627e 100644 --- a/spec/support/shared_examples/namespaces/namespace_traversal_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_examples.rb @@ -39,16 +39,17 @@ RSpec.shared_examples 'namespace traversal' do end describe '#ancestors' do - let(:group) { create(:group) } - let(:nested_group) { create(:group, parent: group) } - let(:deep_nested_group) { create(:group, parent: nested_group) } - let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + let_it_be(:group) { create(:group) } + let_it_be(:nested_group) { create(:group, parent: group) } + let_it_be(:deep_nested_group) { create(:group, parent: nested_group) } + let_it_be(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } it 'returns the correct ancestors' do - expect(very_deep_nested_group.ancestors).to include(group, nested_group, deep_nested_group) - expect(deep_nested_group.ancestors).to include(group, nested_group) - expect(nested_group.ancestors).to include(group) - expect(group.ancestors).to eq([]) + # #reload is called to make sure traversal_ids are reloaded + expect(very_deep_nested_group.reload.ancestors).to contain_exactly(group, nested_group, deep_nested_group) + expect(deep_nested_group.reload.ancestors).to contain_exactly(group, nested_group) + expect(nested_group.reload.ancestors).to contain_exactly(group) + expect(group.reload.ancestors).to eq([]) end describe '#recursive_ancestors' do diff --git a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb index 1fb1b9f79b2..275ddebc18c 100644 --- a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb +++ b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb @@ -47,7 +47,7 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| expect(original_repository_double).to receive(:remove) end - it "moves the project and its #{repository_type} repository to the new storage and unmarks the repository as read only" do + it "moves the project and its #{repository_type} repository to the new storage and unmarks the repository as read-only" do old_project_repository_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do project.repository.path_to_repo end diff --git a/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb b/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb index e67fc4ab04a..97304680316 100644 --- a/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb +++ b/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb @@ -27,7 +27,7 @@ RSpec.shared_examples 'moves repository shard in bulk' do container.set_repository_read_only! expect(subject).to receive(:log_info) - .with(/Container #{container.full_path} \(#{container.id}\) was skipped: #{container.class} is read only/) + .with(/Container #{container.full_path} \(#{container.id}\) was skipped: #{container.class} is read-only/) expect { subject.execute(source_storage_name, destination_storage_name) } .to change(move_service_klass, :count).by(0) end |