Commit b01bbc35 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 05b7c24d 26dcd829
...@@ -793,10 +793,22 @@ export const navigateToDiffFileIndex = ({ commit, state }, index) => { ...@@ -793,10 +793,22 @@ export const navigateToDiffFileIndex = ({ commit, state }, index) => {
commit(types.VIEW_DIFF_FILE, fileHash); commit(types.VIEW_DIFF_FILE, fileHash);
}; };
export const setFileByFile = ({ commit }, { fileByFile }) => { export const setFileByFile = ({ state, commit }, { fileByFile }) => {
const fileViewMode = fileByFile ? DIFF_VIEW_FILE_BY_FILE : DIFF_VIEW_ALL_FILES; const fileViewMode = fileByFile ? DIFF_VIEW_FILE_BY_FILE : DIFF_VIEW_ALL_FILES;
commit(types.SET_FILE_BY_FILE, fileByFile); commit(types.SET_FILE_BY_FILE, fileByFile);
Cookies.set(DIFF_FILE_BY_FILE_COOKIE_NAME, fileViewMode); Cookies.set(DIFF_FILE_BY_FILE_COOKIE_NAME, fileViewMode);
return axios
.put(state.endpointUpdateUser, {
view_diffs_file_by_file: fileByFile,
})
.then(() => {
// https://gitlab.com/gitlab-org/gitlab/-/issues/326961
// We can't even do a simple console warning here because
// the pipeline will fail. However, the issue above will
// eventually handle errors appropriately.
// console.warn('Saving the file-by-fil user preference failed.');
});
}; };
export function reviewFile({ commit, state }, { file, reviewed = true }) { export function reviewFile({ commit, state }, { file, reviewed = true }) {
......
<script> <script>
/* eslint-disable vue/no-v-html */ /* eslint-disable vue/no-v-html */
import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { experiment } from '~/experimentation/utils';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { NEW_REPO_EXPERIMENT } from '../constants';
import blankProjectIllustration from '../illustrations/blank-project.svg'; import blankProjectIllustration from '../illustrations/blank-project.svg';
import ciCdProjectIllustration from '../illustrations/ci-cd-project.svg'; import ciCdProjectIllustration from '../illustrations/ci-cd-project.svg';
import createFromTemplateIllustration from '../illustrations/create-from-template.svg'; import createFromTemplateIllustration from '../illustrations/create-from-template.svg';
...@@ -13,8 +14,10 @@ import WelcomePage from './welcome.vue'; ...@@ -13,8 +14,10 @@ import WelcomePage from './welcome.vue';
const BLANK_PANEL = 'blank_project'; const BLANK_PANEL = 'blank_project';
const CI_CD_PANEL = 'cicd_for_external_repo'; const CI_CD_PANEL = 'cicd_for_external_repo';
const LAST_ACTIVE_TAB_KEY = 'new_project_last_active_tab'; const LAST_ACTIVE_TAB_KEY = 'new_project_last_active_tab';
const PANELS = [ const PANELS = [
{ {
key: 'blank',
name: BLANK_PANEL, name: BLANK_PANEL,
selector: '#blank-project-pane', selector: '#blank-project-pane',
title: s__('ProjectsNew|Create blank project'), title: s__('ProjectsNew|Create blank project'),
...@@ -24,6 +27,7 @@ const PANELS = [ ...@@ -24,6 +27,7 @@ const PANELS = [
illustration: blankProjectIllustration, illustration: blankProjectIllustration,
}, },
{ {
key: 'template',
name: 'create_from_template', name: 'create_from_template',
selector: '#create-from-template-pane', selector: '#create-from-template-pane',
title: s__('ProjectsNew|Create from template'), title: s__('ProjectsNew|Create from template'),
...@@ -33,6 +37,7 @@ const PANELS = [ ...@@ -33,6 +37,7 @@ const PANELS = [
illustration: createFromTemplateIllustration, illustration: createFromTemplateIllustration,
}, },
{ {
key: 'import',
name: 'import_project', name: 'import_project',
selector: '#import-project-pane', selector: '#import-project-pane',
title: s__('ProjectsNew|Import project'), title: s__('ProjectsNew|Import project'),
...@@ -42,6 +47,7 @@ const PANELS = [ ...@@ -42,6 +47,7 @@ const PANELS = [
illustration: importProjectIllustration, illustration: importProjectIllustration,
}, },
{ {
key: 'ci',
name: CI_CD_PANEL, name: CI_CD_PANEL,
selector: '#ci-cd-project-pane', selector: '#ci-cd-project-pane',
title: s__('ProjectsNew|Run CI/CD for external repository'), title: s__('ProjectsNew|Run CI/CD for external repository'),
...@@ -86,11 +92,27 @@ export default { ...@@ -86,11 +92,27 @@ export default {
computed: { computed: {
availablePanels() { availablePanels() {
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'),
}),
});
const updatedPanels = PANELS.map(({ key, title, ...el }) => ({
...el,
title: PANEL_TITLES[key] !== undefined ? PANEL_TITLES[key] : title,
}));
if (this.isCiCdAvailable) { if (this.isCiCdAvailable) {
return PANELS; return updatedPanels;
} }
return PANELS.filter((p) => p.name !== CI_CD_PANEL); return updatedPanels.filter((p) => p.name !== CI_CD_PANEL);
}, },
activePanel() { activePanel() {
......
<script> <script>
/* eslint-disable vue/no-v-html */ /* eslint-disable vue/no-v-html */
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { NEW_REPO_EXPERIMENT } from '../constants';
import NewProjectPushTipPopover from './new_project_push_tip_popover.vue'; import NewProjectPushTipPopover from './new_project_push_tip_popover.vue';
const trackingMixin = Tracking.mixin(gon.tracking_data); const trackingMixin = Tracking.mixin({ ...gon.tracking_data, experiment: NEW_REPO_EXPERIMENT });
export default { export default {
components: { components: {
......
...@@ -74,6 +74,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -74,6 +74,7 @@ class ProjectsController < Projects::ApplicationController
@project = ::Projects::CreateService.new(current_user, project_params(attributes: project_params_create_attributes)).execute @project = ::Projects::CreateService.new(current_user, project_params(attributes: project_params_create_attributes)).execute
if @project.saved? if @project.saved?
experiment(:new_repo, user: current_user).track(:project_created)
experiment(:new_project_readme, actor: current_user).track( experiment(:new_project_readme, actor: current_user).track(
:created, :created,
property: active_new_project_tab, property: active_new_project_tab,
......
...@@ -52,6 +52,8 @@ ...@@ -52,6 +52,8 @@
= stylesheet_link_tag 'performance_bar' if performance_bar_enabled? = stylesheet_link_tag 'performance_bar' if performance_bar_enabled?
-# Rendering this above Gon, to use in JS later
= render 'layouts/header/new_repo_experiment'
= Gon::Base.render_data(nonce: content_security_policy_nonce) = Gon::Base.render_data(nonce: content_security_policy_nonce)
= javascript_include_tag locale_path unless I18n.locale == :en = javascript_include_tag locale_path unless I18n.locale == :en
......
%li.header-new.dropdown{ data: { track_label: "new_dropdown", track_event: "click_dropdown", track_value: "" } } %li.header-new.dropdown{ data: { track_label: "new_dropdown", track_event: "click_dropdown", track_experiment: "new_repo" } }
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", id: "js-onboarding-new-project-link", title: _("New..."), ref: 'tooltip', aria: { label: _("New...") }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", id: "js-onboarding-new-project-link", title: _("New..."), ref: 'tooltip', aria: { label: _("New...") }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do
= sprite_icon('plus-square') = sprite_icon('plus-square')
= sprite_icon('chevron-down', css_class: 'caret-down') = sprite_icon('chevron-down', css_class: 'caret-down')
...@@ -37,8 +37,7 @@ ...@@ -37,8 +37,7 @@
= render 'layouts/header/project_invite_members_new_dropdown_item' = render 'layouts/header/project_invite_members_new_dropdown_item'
%li.divider %li.divider
%li.dropdown-bold-header GitLab %li.dropdown-bold-header GitLab
- if current_user.can_create_project? = content_for :new_repo_experiment
%li= link_to _('New project'), new_project_path, class: 'qa-global-new-project-link'
- if current_user.can_create_group? - if current_user.can_create_group?
%li= link_to _('New group'), new_group_path %li= link_to _('New group'), new_group_path
- if current_user.can?(:create_snippet) - if current_user.can?(:create_snippet)
......
- content_for :new_repo_experiment do
- if current_user&.can_create_project?
- experiment(:new_repo, user: current_user) do |e|
- e.use do
%li= link_to _('New project'), new_project_path, class: 'qa-global-new-project-link', data: { track_experiment: 'new_repo', track_event: 'click_link', track_label: 'plus_menu_dropdown' }
- e.try do
%li= link_to _('New project/repository'), new_project_path, class: 'qa-global-new-project-link', data: { track_experiment: 'new_repo', track_event: 'click_link', track_label: 'plus_menu_dropdown' }
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
-# https://gitlab.com/gitlab-org/gitlab-foss/issues/49713 for more information. -# https://gitlab.com/gitlab-org/gitlab-foss/issues/49713 for more information.
%ul.list-unstyled.navbar-sub-nav %ul.list-unstyled.navbar-sub-nav
- if dashboard_nav_link?(:projects) - if dashboard_nav_link?(:projects)
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown", data: { track_label: "projects_dropdown", track_event: "click_dropdown" } }) do = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown", data: { track_label: "projects_dropdown", track_event: "click_dropdown", track_experiment: "new_repo" } }) do
%button{ type: 'button', data: { toggle: "dropdown" } } %button{ type: 'button', data: { toggle: "dropdown" } }
= _('Projects') = _('Projects')
= sprite_icon('chevron-down', css_class: 'caret-down') = sprite_icon('chevron-down', css_class: 'caret-down')
......
...@@ -11,14 +11,21 @@ ...@@ -11,14 +11,21 @@
= nav_link(path: 'projects#trending') do = nav_link(path: 'projects#trending') do
= link_to explore_root_path, data: { track_label: "projects_dropdown_explore_projects", track_event: "click_link" } do = link_to explore_root_path, data: { track_label: "projects_dropdown_explore_projects", track_event: "click_link" } do
= _('Explore projects') = _('Explore projects')
= nav_link(path: 'projects/new#blank_project', - experiment(:new_repo, user: current_user) do |e|
html_options: { class: 'gl-border-0 gl-border-t-1 gl-border-solid gl-border-gray-100' }, - e.use do
data: { track_label: "projects_dropdown_blank_project", track_event: "click_link" }) do = nav_link(path: 'projects/new#blank_project', html_options: { class: 'gl-border-0 gl-border-t-1 gl-border-solid gl-border-gray-100' }) do
= link_to new_project_path(anchor: 'blank_project') do = link_to new_project_path(anchor: 'blank_project'), data: { track_label: "projects_dropdown_blank_project", track_event: "click_link", track_experiment: "new_repo" } do
= _('Create blank project') = _('Create blank project')
= nav_link(path: 'projects/new#import_project') do = nav_link(path: 'projects/new#import_project') do
= link_to new_project_path(anchor: 'import_project'), data: { track_label: "projects_dropdown_import_project", track_event: "click_link" } do = link_to new_project_path(anchor: 'import_project'), data: { track_label: "projects_dropdown_import_project", track_event: "click_link", track_experiment: "new_repo" } do
= _('Import project') = _('Import project')
- e.try do
= nav_link(path: 'projects/new#blank_project', html_options: { class: 'gl-border-0 gl-border-t-1 gl-border-solid gl-border-gray-100' }) do
= link_to new_project_path(anchor: 'blank_project'), data: { track_label: "projects_dropdown_blank_project", track_event: "click_link", track_experiment: "new_repo" } do
= _('Create blank project/repository')
= nav_link(path: 'projects/new#import_project') do
= link_to new_project_path(anchor: 'import_project'), data: { track_label: "projects_dropdown_import_project", track_event: "click_link", track_experiment: "new_repo" } do
= _('Import project/repository')
= nav_link(path: 'projects/new#create_from_template') do = nav_link(path: 'projects/new#create_from_template') do
= link_to new_project_path(anchor: 'create_from_template'), data: { track_label: "projects_dropdown_create_from_template", track_event: "click_link" } do = link_to new_project_path(anchor: 'create_from_template'), data: { track_label: "projects_dropdown_create_from_template", track_event: "click_link" } do
= _('Create from template') = _('Create from template')
......
---
title: Sync single-file mode user preference when changed from the MR cog menu checkbox
merge_request: 55931
author:
type: changed
---
name: new_repo
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55818
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285153
milestone: '13.11'
type: experiment
group: group::adoption
default_enabled: false
...@@ -412,7 +412,7 @@ in the regression issue as fixes are addressed. ...@@ -412,7 +412,7 @@ in the regression issue as fixes are addressed.
In order to track things that can be improved in the GitLab codebase, In order to track things that can be improved in the GitLab codebase,
we use the ~"technical debt" label in the [GitLab issue tracker](https://gitlab.com/gitlab-org/gitlab/-/issues). we use the ~"technical debt" label in the [GitLab issue tracker](https://gitlab.com/gitlab-org/gitlab/-/issues).
For missed user experience requirements, we use the ~"UX debt" label. We use the ~"UX debt" label when we choose to deviate from the MVC, in a way that harms the user experience.
These labels should be added to issues that describe things that can be improved, These labels should be added to issues that describe things that can be improved,
shortcuts that have been taken, features that need additional attention, and all shortcuts that have been taken, features that need additional attention, and all
......
...@@ -1143,6 +1143,22 @@ Profiles: ...@@ -1143,6 +1143,22 @@ Profiles:
UnicodeFuzzing: true UnicodeFuzzing: true
``` ```
## Troubleshooting
### Failed to start scanner session (version header not found)
The API Fuzzing engine outputs an error message when it cannot establish a connection with the scanner application component. The error message is shown in the job output window of the `apifuzzer_fuzz` job. A common cause of this issue is changing the `FUZZAPI_API` variable from its default.
**Error message**
- In [GitLab 13.11 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/323939), `Failed to start scanner session (version header not found).`
- In GitLab 13.10 and earlier, `API Security version header not found. Are you sure that you are connecting to the API Security server?`.
**Solution**
- Remove the `FUZZAPI_API` variable from the `.gitlab-ci.yml` file. The value will be inherited from the API Fuzzing CI/CD template. We recommend this method instead of manually setting a value.
- If removing the variable is not possible, check to see if this value has changed in the latest version of the [API Fuzzing CI/CD template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml). If so, update the value in the `.gitlab-ci.yml` file.
<!-- <!--
### Target Container ### Target Container
......
<script>
import { GlIcon, GlPopover, GlLink } from '@gitlab/ui';
import { REPLICATION_STATUS_UI, REPLICATION_PAUSE_URL } from 'ee/geo_nodes_beta/constants';
import { __, s__ } from '~/locale';
export default {
name: 'GeoNodeReplicationStatus',
i18n: {
pauseHelpText: s__('Geo|Geo nodes are paused using a command run on the node'),
learnMore: __('Learn more'),
},
components: {
GlIcon,
GlPopover,
GlLink,
},
props: {
node: {
type: Object,
required: true,
},
},
computed: {
replicationStatusUi() {
return this.node.enabled ? REPLICATION_STATUS_UI.enabled : REPLICATION_STATUS_UI.disabled;
},
},
REPLICATION_PAUSE_URL,
};
</script>
<template>
<div class="gl-display-flex gl-align-items-center">
<gl-icon
:name="replicationStatusUi.icon"
:class="replicationStatusUi.color"
data-testid="replication-status-icon"
/>
<span
class="gl-font-weight-bold"
:class="replicationStatusUi.color"
data-testid="replication-status-text"
>{{ replicationStatusUi.text }}</span
>
<gl-icon
ref="replicationStatus"
name="question"
class="gl-text-blue-500 gl-cursor-pointer gl-ml-2"
/>
<gl-popover :target="() => $refs.replicationStatus.$el" placement="top" triggers="hover focus">
<p class="gl-font-base">
{{ $options.i18n.pauseHelpText }}
</p>
<gl-link :href="$options.REPLICATION_PAUSE_URL" target="_blank">{{
$options.i18n.learnMore
}}</gl-link>
</gl-popover>
</div>
</template>
<script> <script>
import { GlCard, GlButton } from '@gitlab/ui'; import { GlCard, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import GeoNodeReplicationStatus from './geo_node_replication_status.vue';
import GeoNodeSyncSettings from './geo_node_sync_settings.vue';
export default { export default {
name: 'GeoNodeReplicationSummary', name: 'GeoNodeReplicationSummary',
...@@ -14,6 +16,8 @@ export default { ...@@ -14,6 +16,8 @@ export default {
components: { components: {
GlCard, GlCard,
GlButton, GlButton,
GeoNodeReplicationStatus,
GeoNodeSyncSettings,
}, },
props: { props: {
node: { node: {
...@@ -38,10 +42,12 @@ export default { ...@@ -38,10 +42,12 @@ export default {
> >
</template> </template>
<div class="gl-display-flex gl-flex-direction-column gl-mb-5"> <div class="gl-display-flex gl-flex-direction-column gl-mb-5">
<span data-testid="replication-status">{{ $options.i18n.replicationStatus }}</span> <span>{{ $options.i18n.replicationStatus }}</span>
<geo-node-replication-status class="gl-mt-3" :node="node" />
</div> </div>
<div class="gl-display-flex gl-flex-direction-column gl-mb-5"> <div class="gl-display-flex gl-flex-direction-column gl-mb-5">
<span data-testid="sync-settings">{{ $options.i18n.syncSettings }}</span> <span>{{ $options.i18n.syncSettings }}</span>
<geo-node-sync-settings class="gl-mt-2" :node="node" />
</div> </div>
<span data-testid="replication-counts">{{ $options.i18n.replicationCounts }}</span> <span data-testid="replication-counts">{{ $options.i18n.replicationCounts }}</span>
</gl-card> </gl-card>
......
<script>
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { sprintf, __, s__ } from '~/locale';
export default {
name: 'GeoNodeSyncSettings',
i18n: {
full: __('Full'),
groups: __('groups'),
syncLabel: s__('Geo|Selective (%{syncLabel})'),
pendingEvents: s__('Geo|%{timeAgoStr} (%{pendingEvents} events)'),
},
props: {
node: {
type: Object,
required: true,
},
},
computed: {
syncType() {
if (this.node.selectiveSyncType === null || this.node.selectiveSyncType === '') {
return this.$options.i18n.full;
}
// Renaming namespaces to groups in the UI for Geo Selective Sync
const syncLabel =
this.node.selectiveSyncType === 'namespaces'
? this.$options.i18n.groups
: this.node.selectiveSyncType;
return sprintf(this.$options.i18n.syncLabel, { syncLabel });
},
eventTimestampEmpty() {
return !this.node.lastEventTimestamp || !this.node.cursorLastEventTimestamp;
},
syncLagInSeconds() {
return this.node.cursorLastEventTimestamp - this.node.lastEventTimestamp;
},
syncStatusEventInfo() {
const timeAgoStr = timeIntervalInWords(this.syncLagInSeconds);
const pendingEvents = this.node.lastEventId - this.node.cursorLastEventId;
return sprintf(this.$options.i18n.pendingEvents, {
timeAgoStr,
pendingEvents,
});
},
},
};
</script>
<template>
<div class="gl-display-flex gl-align-items-center">
<span class="gl-font-weight-bold" data-testid="sync-type">{{ syncType }}</span>
<span
v-if="!eventTimestampEmpty"
class="gl-ml-3 gl-text-gray-500 gl-font-sm"
data-testid="sync-status-event-info"
>
{{ syncStatusEventInfo }}
</span>
</div>
</template>
import { helpPagePath } from '~/helpers/help_page_helper'; import { helpPagePath } from '~/helpers/help_page_helper';
import { __ } from '~/locale';
export const GEO_INFO_URL = helpPagePath('administration/geo/index.md'); export const GEO_INFO_URL = helpPagePath('administration/geo/index.md');
...@@ -17,6 +18,10 @@ export const HELP_INFO_URL = helpPagePath( ...@@ -17,6 +18,10 @@ export const HELP_INFO_URL = helpPagePath(
{ anchor: 'repository-verification' }, { anchor: 'repository-verification' },
); );
export const REPLICATION_PAUSE_URL = helpPagePath('administration/geo/index.html', {
anchor: 'pausing-and-resuming-replication',
});
export const HEALTH_STATUS_UI = { export const HEALTH_STATUS_UI = {
healthy: { healthy: {
icon: 'status_success', icon: 'status_success',
...@@ -40,4 +45,17 @@ export const HEALTH_STATUS_UI = { ...@@ -40,4 +45,17 @@ export const HEALTH_STATUS_UI = {
}, },
}; };
export const REPLICATION_STATUS_UI = {
enabled: {
icon: 'play',
color: 'gl-text-green-600',
text: __('Enabled'),
},
disabled: {
icon: 'pause',
color: 'gl-text-orange-600',
text: __('Paused'),
},
};
export const STATUS_DELAY_THRESHOLD_MS = 600000; export const STATUS_DELAY_THRESHOLD_MS = 600000;
import { GlPopover, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import GeoNodeReplicationStatus from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_status.vue';
import { REPLICATION_STATUS_UI, REPLICATION_PAUSE_URL } from 'ee/geo_nodes_beta/constants';
import { MOCK_NODES } from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('GeoNodeReplicationStatus', () => {
let wrapper;
const defaultProps = {
node: MOCK_NODES[1],
};
const createComponent = (props) => {
wrapper = extendedWrapper(
shallowMount(GeoNodeReplicationStatus, {
propsData: {
...defaultProps,
...props,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
const findReplicationStatusIcon = () => wrapper.findByTestId('replication-status-icon');
const findReplicationStatusText = () => wrapper.findByTestId('replication-status-text');
const findQuestionIcon = () => wrapper.find({ ref: 'replicationStatus' });
const findGlPopover = () => wrapper.findComponent(GlPopover);
const findGlPopoverLink = () => findGlPopover().findComponent(GlLink);
describe('template', () => {
describe('always', () => {
beforeEach(() => {
createComponent();
});
it('renders the replication status icon', () => {
expect(findReplicationStatusIcon().exists()).toBe(true);
});
it('renders the replication status text', () => {
expect(findReplicationStatusText().exists()).toBe(true);
});
it('renders the question icon correctly', () => {
expect(findQuestionIcon().exists()).toBe(true);
expect(findQuestionIcon().attributes('name')).toBe('question');
});
it('renders the GlPopover always', () => {
expect(findGlPopover().exists()).toBe(true);
});
it('renders the popover link correctly', () => {
expect(findGlPopoverLink().exists()).toBe(true);
expect(findGlPopoverLink().attributes('href')).toBe(REPLICATION_PAUSE_URL);
});
});
describe.each`
enabled | uiData
${true} | ${REPLICATION_STATUS_UI.enabled}
${false} | ${REPLICATION_STATUS_UI.disabled}
`(`conditionally`, ({ enabled, uiData }) => {
beforeEach(() => {
createComponent({ node: { enabled } });
});
describe(`when enabled is ${enabled}`, () => {
it(`renders the replication status icon correctly`, () => {
expect(findReplicationStatusIcon().classes(uiData.color)).toBe(true);
expect(findReplicationStatusIcon().attributes('name')).toBe(uiData.icon);
});
it(`renders the replication status text correctly`, () => {
expect(findReplicationStatusText().classes(uiData.color)).toBe(true);
expect(findReplicationStatusText().text()).toBe(uiData.text);
});
});
});
});
});
import { GlButton } from '@gitlab/ui'; import { GlButton, GlCard } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import GeoNodeReplicationStatus from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_status.vue';
import GeoNodeReplicationSummary from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_summary.vue'; import GeoNodeReplicationSummary from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_summary.vue';
import GeoNodeSyncSettings from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_sync_settings.vue';
import { MOCK_NODES } from 'ee_jest/geo_nodes_beta/mock_data'; import { MOCK_NODES } from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
...@@ -11,13 +13,14 @@ describe('GeoNodeReplicationSummary', () => { ...@@ -11,13 +13,14 @@ describe('GeoNodeReplicationSummary', () => {
node: MOCK_NODES[1], node: MOCK_NODES[1],
}; };
const createComponent = (initialState, props) => { const createComponent = (props) => {
wrapper = extendedWrapper( wrapper = extendedWrapper(
mount(GeoNodeReplicationSummary, { shallowMount(GeoNodeReplicationSummary, {
propsData: { propsData: {
...defaultProps, ...defaultProps,
...props, ...props,
}, },
stubs: { GlCard },
}), }),
); );
}; };
...@@ -27,9 +30,9 @@ describe('GeoNodeReplicationSummary', () => { ...@@ -27,9 +30,9 @@ describe('GeoNodeReplicationSummary', () => {
}); });
const findGlButton = () => wrapper.findComponent(GlButton); const findGlButton = () => wrapper.findComponent(GlButton);
const findGeoNodeReplicationStatus = () => wrapper.findByTestId('replication-status'); const findGeoNodeReplicationStatus = () => wrapper.findComponent(GeoNodeReplicationStatus);
const findGeoNodeReplicationCounts = () => wrapper.findByTestId('replication-counts'); const findGeoNodeReplicationCounts = () => wrapper.findByTestId('replication-counts');
const findGeoNodeSyncSettings = () => wrapper.findByTestId('sync-settings'); const findGeoNodeSyncSettings = () => wrapper.findComponent(GeoNodeSyncSettings);
describe('template', () => { describe('template', () => {
beforeEach(() => { beforeEach(() => {
......
import { shallowMount } from '@vue/test-utils';
import GeoNodeSyncSettings from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_sync_settings.vue';
import { MOCK_NODES } from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('GeoNodeSyncSettings', () => {
let wrapper;
const defaultProps = {
node: MOCK_NODES[1],
};
const createComponent = (props) => {
wrapper = extendedWrapper(
shallowMount(GeoNodeSyncSettings, {
propsData: {
...defaultProps,
...props,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
const findSyncType = () => wrapper.findByTestId('sync-type');
const findSyncStatusEventInfo = () => wrapper.findByTestId('sync-status-event-info');
describe('template', () => {
describe('always', () => {
beforeEach(() => {
createComponent();
});
it('renders the sync type', () => {
expect(findSyncType().exists()).toBe(true);
});
});
describe('conditionally', () => {
describe.each`
selectiveSyncType | text
${null} | ${'Full'}
${'namespaces'} | ${'Selective (groups)'}
${'shards'} | ${'Selective (shards)'}
`(`sync type`, ({ selectiveSyncType, text }) => {
beforeEach(() => {
createComponent({ node: { selectiveSyncType } });
});
it(`renders correctly when selectiveSyncType is ${selectiveSyncType}`, () => {
expect(findSyncType().text()).toBe(text);
});
});
describe('with no timestamp info', () => {
beforeEach(() => {
createComponent({ node: { lastEventTimestamp: null, cursorLastEventTimestamp: null } });
});
it('does not render the sync status event info', () => {
expect(findSyncStatusEventInfo().exists()).toBe(false);
});
});
describe('with timestamp info', () => {
beforeEach(() => {
createComponent({
node: {
lastEventTimestamp: 1511255300,
lastEventId: 10,
cursorLastEventTimestamp: 1511255200,
cursorLastEventId: 9,
},
});
});
it('does render the sync status event info', () => {
expect(findSyncStatusEventInfo().exists()).toBe(true);
expect(findSyncStatusEventInfo().text()).toBe('20 seconds (1 events)');
});
});
});
});
});
...@@ -8948,6 +8948,9 @@ msgstr "" ...@@ -8948,6 +8948,9 @@ msgstr ""
msgid "Create blank project" msgid "Create blank project"
msgstr "" msgstr ""
msgid "Create blank project/repository"
msgstr ""
msgid "Create branch" msgid "Create branch"
msgstr "" msgstr ""
...@@ -13805,6 +13808,9 @@ msgstr "" ...@@ -13805,6 +13808,9 @@ msgstr ""
msgid "From the Kubernetes cluster details view, applications list, install GitLab Runner." msgid "From the Kubernetes cluster details view, applications list, install GitLab Runner."
msgstr "" msgstr ""
msgid "Full"
msgstr ""
msgid "Full name" msgid "Full name"
msgstr "" msgstr ""
...@@ -14012,6 +14018,9 @@ msgstr "" ...@@ -14012,6 +14018,9 @@ msgstr ""
msgid "Geo|%{name} is scheduled for re-verify" msgid "Geo|%{name} is scheduled for re-verify"
msgstr "" msgstr ""
msgid "Geo|%{timeAgoStr} (%{pendingEvents} events)"
msgstr ""
msgid "Geo|%{title} checksum progress" msgid "Geo|%{title} checksum progress"
msgstr "" msgstr ""
...@@ -14075,6 +14084,9 @@ msgstr "" ...@@ -14075,6 +14084,9 @@ msgstr ""
msgid "Geo|Geo Status" msgid "Geo|Geo Status"
msgstr "" msgstr ""
msgid "Geo|Geo nodes are paused using a command run on the node"
msgstr ""
msgid "Geo|Geo sites" msgid "Geo|Geo sites"
msgstr "" msgstr ""
...@@ -14228,6 +14240,9 @@ msgstr "" ...@@ -14228,6 +14240,9 @@ msgstr ""
msgid "Geo|Secondary site" msgid "Geo|Secondary site"
msgstr "" msgstr ""
msgid "Geo|Selective (%{syncLabel})"
msgstr ""
msgid "Geo|Status" msgid "Geo|Status"
msgstr "" msgstr ""
...@@ -15982,6 +15997,9 @@ msgstr "" ...@@ -15982,6 +15997,9 @@ msgstr ""
msgid "Import project members" msgid "Import project members"
msgstr "" msgstr ""
msgid "Import project/repository"
msgstr ""
msgid "Import projects from Bitbucket" msgid "Import projects from Bitbucket"
msgstr "" msgstr ""
...@@ -20920,6 +20938,9 @@ msgstr "" ...@@ -20920,6 +20938,9 @@ msgstr ""
msgid "New project" msgid "New project"
msgstr "" msgstr ""
msgid "New project/repository"
msgstr ""
msgid "New release" msgid "New release"
msgstr "" msgstr ""
...@@ -22580,6 +22601,9 @@ msgstr "" ...@@ -22580,6 +22601,9 @@ msgstr ""
msgid "Pause replication" msgid "Pause replication"
msgstr "" msgstr ""
msgid "Paused"
msgstr ""
msgid "Paused runners don't accept new jobs" msgid "Paused runners don't accept new jobs"
msgstr "" msgstr ""
...@@ -24794,6 +24818,9 @@ msgstr "" ...@@ -24794,6 +24818,9 @@ msgstr ""
msgid "ProjectsNew|Create blank project" msgid "ProjectsNew|Create blank project"
msgstr "" msgstr ""
msgid "ProjectsNew|Create blank project/repository"
msgstr ""
msgid "ProjectsNew|Create from template" msgid "ProjectsNew|Create from template"
msgstr "" msgstr ""
...@@ -24812,6 +24839,9 @@ msgstr "" ...@@ -24812,6 +24839,9 @@ msgstr ""
msgid "ProjectsNew|Import project" msgid "ProjectsNew|Import project"
msgstr "" msgstr ""
msgid "ProjectsNew|Import project/repository"
msgstr ""
msgid "ProjectsNew|Initialize repository with a README" msgid "ProjectsNew|Initialize repository with a README"
msgstr "" msgstr ""
......
...@@ -5,7 +5,7 @@ module QA ...@@ -5,7 +5,7 @@ module QA
module Dashboard module Dashboard
module Snippet module Snippet
class Index < Page::Base class Index < Page::Base
view 'app/views/layouts/header/_new_dropdown.haml' do view 'app/views/layouts/header/_new_dropdown.html.haml' do
element :new_menu_toggle element :new_menu_toggle
element :global_new_snippet_link element :global_new_snippet_link
end end
......
...@@ -22,7 +22,7 @@ module QA ...@@ -22,7 +22,7 @@ module QA
element :file_tree_table element :file_tree_table
end end
view 'app/views/layouts/header/_new_dropdown.haml' do view 'app/views/layouts/header/_new_dropdown.html.haml' do
element :new_menu_toggle element :new_menu_toggle
element :new_issue_link, "link_to _('New issue'), new_project_issue_path(@project)" # rubocop:disable QA/ElementWithPattern element :new_issue_link, "link_to _('New issue'), new_project_issue_path(@project)" # rubocop:disable QA/ElementWithPattern
end end
......
...@@ -448,6 +448,12 @@ RSpec.describe ProjectsController do ...@@ -448,6 +448,12 @@ RSpec.describe ProjectsController do
post :create, params: { project: project_params } post :create, params: { project: project_params }
end end
it 'tracks a created event for the new_repo experiment', :experiment do
expect(experiment(:new_repo, :candidate)).to track(:project_created).on_next_instance
post :create, params: { project: project_params }
end
end end
describe 'POST #archive' do describe 'POST #archive' do
......
...@@ -12,6 +12,72 @@ RSpec.describe 'New project', :js do ...@@ -12,6 +12,72 @@ RSpec.describe 'New project', :js do
sign_in(user) sign_in(user)
end end
context 'new repo experiment', :experiment do
it 'when in control renders "project"' do
stub_experiments(new_repo: :control)
visit new_project_path
find('li.header-new.dropdown').click
page.within('li.header-new.dropdown') do
expect(page).to have_selector('a', text: 'New project')
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')
end
it 'when in candidate renders "project/repository"' do
stub_experiments(new_repo: :candidate)
visit new_project_path
find('li.header-new.dropdown').click
page.within('li.header-new.dropdown') 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')
end
context 'with combined_menu feature disabled' do
before do
stub_feature_flags(combined_menu: false)
end
it 'when in control it renders "project" in the new projects dropdown' do
stub_experiments(new_repo: :control)
visit new_project_path
find('#nav-projects-dropdown').click
page.within('#nav-projects-dropdown') do
expect(page).to have_selector('a', text: 'Create blank project')
expect(page).to have_selector('a', text: 'Import project')
expect(page).to have_no_selector('a', text: 'Create blank project/repository')
expect(page).to have_no_selector('a', text: 'Import project/repository')
end
end
it 'when in candidate it renders "project/repository" in the new projects dropdown' do
stub_experiments(new_repo: :candidate)
visit new_project_path
find('#nav-projects-dropdown').click
page.within('#nav-projects-dropdown') do
expect(page).to have_selector('a', text: 'Create blank project/repository')
expect(page).to have_selector('a', text: 'Import project/repository')
end
end
end
end
it 'shows a message if multiple levels are restricted' do it 'shows a message if multiple levels are restricted' do
Gitlab::CurrentSettings.update!( Gitlab::CurrentSettings.update!(
restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL] restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL]
......
...@@ -1507,19 +1507,42 @@ describe('DiffsStoreActions', () => { ...@@ -1507,19 +1507,42 @@ describe('DiffsStoreActions', () => {
}); });
describe('setFileByFile', () => { describe('setFileByFile', () => {
const updateUserEndpoint = 'user/prefs';
let putSpy;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
putSpy = jest.spyOn(axios, 'put');
mock.onPut(updateUserEndpoint).reply(200, {});
});
afterEach(() => {
mock.restore();
});
it.each` it.each`
value value
${true} ${true}
${false} ${false}
`('commits SET_FILE_BY_FILE with the new value $value', ({ value }) => { `(
return testAction( 'commits SET_FILE_BY_FILE and persists the File-by-File user preference with the new value $value',
setFileByFile, async ({ value }) => {
{ fileByFile: value }, await testAction(
{ viewDiffsFileByFile: null }, setFileByFile,
[{ type: types.SET_FILE_BY_FILE, payload: value }], { fileByFile: value },
[], {
); viewDiffsFileByFile: null,
}); endpointUpdateUser: updateUserEndpoint,
},
[{ type: types.SET_FILE_BY_FILE, payload: value }],
[],
);
expect(putSpy).toHaveBeenCalledWith(updateUserEndpoint, { view_diffs_file_by_file: value });
},
);
}); });
describe('reviewFile', () => { describe('reviewFile', () => {
......
import { GlBreadcrumb } from '@gitlab/ui'; import { GlBreadcrumb } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { assignGitlabExperiment } from 'helpers/experimentation_helper';
import App from '~/projects/experiment_new_project_creation/components/app.vue'; import App from '~/projects/experiment_new_project_creation/components/app.vue';
import LegacyContainer from '~/projects/experiment_new_project_creation/components/legacy_container.vue'; import LegacyContainer from '~/projects/experiment_new_project_creation/components/legacy_container.vue';
import WelcomePage from '~/projects/experiment_new_project_creation/components/welcome.vue'; import WelcomePage from '~/projects/experiment_new_project_creation/components/welcome.vue';
...@@ -17,6 +18,34 @@ describe('Experimental new project creation app', () => { ...@@ -17,6 +18,34 @@ describe('Experimental new project creation app', () => {
wrapper = null; wrapper = null;
}); });
const findWelcomePage = () => wrapper.findComponent(WelcomePage);
const findPanel = (panelName) =>
findWelcomePage()
.props()
.panels.find((p) => p.name === panelName);
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 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('with empty hash', () => { describe('with empty hash', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { mockTracking } from 'helpers/tracking_helper'; 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 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 '~/projects/experiment_new_project_creation/components/welcome.vue';
jest.mock('~/experimentation/utils', () => ({ getExperimentData: jest.fn() }));
describe('Welcome page', () => { describe('Welcome page', () => {
let wrapper; let wrapper;
let trackingSpy; let trackingSpy;
...@@ -14,6 +19,7 @@ describe('Welcome page', () => { ...@@ -14,6 +19,7 @@ describe('Welcome page', () => {
beforeEach(() => { beforeEach(() => {
trackingSpy = mockTracking('_category_', document, jest.spyOn); trackingSpy = mockTracking('_category_', document, jest.spyOn);
trackingSpy.mockImplementation(() => {}); trackingSpy.mockImplementation(() => {});
getExperimentData.mockReturnValue(undefined);
}); });
afterEach(() => { afterEach(() => {
...@@ -22,14 +28,35 @@ describe('Welcome page', () => { ...@@ -22,14 +28,35 @@ describe('Welcome page', () => {
wrapper = null; wrapper = null;
}); });
it('tracks link clicks', () => { it('tracks link clicks', async () => {
createComponent({ panels: [{ name: 'test', href: '#' }] }); createComponent({ panels: [{ name: 'test', href: '#' }] });
wrapper.find('a').trigger('click'); const link = wrapper.find('a');
link.trigger('click');
await nextTick();
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', { label: 'test' }); expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', { label: 'test' });
}); });
}); });
it('adds new_repo experiment data if in experiment', async () => {
const mockExperimentData = 'data';
getExperimentData.mockReturnValue(mockExperimentData);
createComponent({ panels: [{ name: 'test', href: '#' }] });
const link = wrapper.find('a');
link.trigger('click');
await nextTick();
return wrapper.vm.$nextTick().then(() => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', {
label: 'test',
context: {
data: mockExperimentData,
schema: TRACKING_CONTEXT_SCHEMA,
},
});
});
});
it('renders new project push tip popover', () => { it('renders new project push tip popover', () => {
createComponent({ panels: [{ name: 'test', href: '#' }] }); createComponent({ panels: [{ name: 'test', href: '#' }] });
......
...@@ -163,6 +163,7 @@ RSpec.describe 'layouts/header/_new_dropdown' do ...@@ -163,6 +163,7 @@ RSpec.describe 'layouts/header/_new_dropdown' do
end end
it 'has a "New project" link' do it 'has a "New project" link' do
render('layouts/header/new_repo_experiment')
render render
expect(rendered).to have_link('New project', href: new_project_path) expect(rendered).to have_link('New project', href: new_project_path)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment