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) => {
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;
commit(types.SET_FILE_BY_FILE, fileByFile);
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 }) {
......
<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';
......@@ -13,8 +14,10 @@ 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'),
......@@ -24,6 +27,7 @@ const PANELS = [
illustration: blankProjectIllustration,
},
{
key: 'template',
name: 'create_from_template',
selector: '#create-from-template-pane',
title: s__('ProjectsNew|Create from template'),
......@@ -33,6 +37,7 @@ const PANELS = [
illustration: createFromTemplateIllustration,
},
{
key: 'import',
name: 'import_project',
selector: '#import-project-pane',
title: s__('ProjectsNew|Import project'),
......@@ -42,6 +47,7 @@ const PANELS = [
illustration: importProjectIllustration,
},
{
key: 'ci',
name: CI_CD_PANEL,
selector: '#ci-cd-project-pane',
title: s__('ProjectsNew|Run CI/CD for external repository'),
......@@ -86,11 +92,27 @@ export default {
computed: {
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) {
return PANELS;
return updatedPanels;
}
return PANELS.filter((p) => p.name !== CI_CD_PANEL);
return updatedPanels.filter((p) => p.name !== CI_CD_PANEL);
},
activePanel() {
......
<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);
const trackingMixin = Tracking.mixin({ ...gon.tracking_data, experiment: NEW_REPO_EXPERIMENT });
export default {
components: {
......
......@@ -74,6 +74,7 @@ class ProjectsController < Projects::ApplicationController
@project = ::Projects::CreateService.new(current_user, project_params(attributes: project_params_create_attributes)).execute
if @project.saved?
experiment(:new_repo, user: current_user).track(:project_created)
experiment(:new_project_readme, actor: current_user).track(
:created,
property: active_new_project_tab,
......
......@@ -52,6 +52,8 @@
= 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)
= 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
= sprite_icon('plus-square')
= sprite_icon('chevron-down', css_class: 'caret-down')
......@@ -37,8 +37,7 @@
= render 'layouts/header/project_invite_members_new_dropdown_item'
%li.divider
%li.dropdown-bold-header GitLab
- if current_user.can_create_project?
%li= link_to _('New project'), new_project_path, class: 'qa-global-new-project-link'
= content_for :new_repo_experiment
- if current_user.can_create_group?
%li= link_to _('New group'), new_group_path
- 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 @@
-# https://gitlab.com/gitlab-org/gitlab-foss/issues/49713 for more information.
%ul.list-unstyled.navbar-sub-nav
- 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" } }
= _('Projects')
= sprite_icon('chevron-down', css_class: 'caret-down')
......
......@@ -11,14 +11,21 @@
= nav_link(path: 'projects#trending') do
= link_to explore_root_path, data: { track_label: "projects_dropdown_explore_projects", track_event: "click_link" } do
= _('Explore projects')
= nav_link(path: 'projects/new#blank_project',
html_options: { class: 'gl-border-0 gl-border-t-1 gl-border-solid gl-border-gray-100' },
data: { track_label: "projects_dropdown_blank_project", track_event: "click_link" }) do
= link_to new_project_path(anchor: 'blank_project') do
= _('Create blank project')
= 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
= _('Import project')
- experiment(:new_repo, user: current_user) do |e|
- e.use 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')
= 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')
- 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
= 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')
......
---
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.
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).
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,
shortcuts that have been taken, features that need additional attention, and all
......
......@@ -1143,6 +1143,22 @@ Profiles:
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
......
<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>
import { GlCard, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
import GeoNodeReplicationStatus from './geo_node_replication_status.vue';
import GeoNodeSyncSettings from './geo_node_sync_settings.vue';
export default {
name: 'GeoNodeReplicationSummary',
......@@ -14,6 +16,8 @@ export default {
components: {
GlCard,
GlButton,
GeoNodeReplicationStatus,
GeoNodeSyncSettings,
},
props: {
node: {
......@@ -38,10 +42,12 @@ export default {
>
</template>
<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 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>
<span data-testid="replication-counts">{{ $options.i18n.replicationCounts }}</span>
</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 { __ } from '~/locale';
export const GEO_INFO_URL = helpPagePath('administration/geo/index.md');
......@@ -17,6 +18,10 @@ export const HELP_INFO_URL = helpPagePath(
{ anchor: 'repository-verification' },
);
export const REPLICATION_PAUSE_URL = helpPagePath('administration/geo/index.html', {
anchor: 'pausing-and-resuming-replication',
});
export const HEALTH_STATUS_UI = {
healthy: {
icon: 'status_success',
......@@ -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;
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 { mount } from '@vue/test-utils';
import { GlButton, GlCard } 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 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 { extendedWrapper } from 'helpers/vue_test_utils_helper';
......@@ -11,13 +13,14 @@ describe('GeoNodeReplicationSummary', () => {
node: MOCK_NODES[1],
};
const createComponent = (initialState, props) => {
const createComponent = (props) => {
wrapper = extendedWrapper(
mount(GeoNodeReplicationSummary, {
shallowMount(GeoNodeReplicationSummary, {
propsData: {
...defaultProps,
...props,
},
stubs: { GlCard },
}),
);
};
......@@ -27,9 +30,9 @@ describe('GeoNodeReplicationSummary', () => {
});
const findGlButton = () => wrapper.findComponent(GlButton);
const findGeoNodeReplicationStatus = () => wrapper.findByTestId('replication-status');
const findGeoNodeReplicationStatus = () => wrapper.findComponent(GeoNodeReplicationStatus);
const findGeoNodeReplicationCounts = () => wrapper.findByTestId('replication-counts');
const findGeoNodeSyncSettings = () => wrapper.findByTestId('sync-settings');
const findGeoNodeSyncSettings = () => wrapper.findComponent(GeoNodeSyncSettings);
describe('template', () => {
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 ""
msgid "Create blank project"
msgstr ""
msgid "Create blank project/repository"
msgstr ""
msgid "Create branch"
msgstr ""
......@@ -13805,6 +13808,9 @@ msgstr ""
msgid "From the Kubernetes cluster details view, applications list, install GitLab Runner."
msgstr ""
msgid "Full"
msgstr ""
msgid "Full name"
msgstr ""
......@@ -14012,6 +14018,9 @@ msgstr ""
msgid "Geo|%{name} is scheduled for re-verify"
msgstr ""
msgid "Geo|%{timeAgoStr} (%{pendingEvents} events)"
msgstr ""
msgid "Geo|%{title} checksum progress"
msgstr ""
......@@ -14075,6 +14084,9 @@ msgstr ""
msgid "Geo|Geo Status"
msgstr ""
msgid "Geo|Geo nodes are paused using a command run on the node"
msgstr ""
msgid "Geo|Geo sites"
msgstr ""
......@@ -14228,6 +14240,9 @@ msgstr ""
msgid "Geo|Secondary site"
msgstr ""
msgid "Geo|Selective (%{syncLabel})"
msgstr ""
msgid "Geo|Status"
msgstr ""
......@@ -15982,6 +15997,9 @@ msgstr ""
msgid "Import project members"
msgstr ""
msgid "Import project/repository"
msgstr ""
msgid "Import projects from Bitbucket"
msgstr ""
......@@ -20920,6 +20938,9 @@ msgstr ""
msgid "New project"
msgstr ""
msgid "New project/repository"
msgstr ""
msgid "New release"
msgstr ""
......@@ -22580,6 +22601,9 @@ msgstr ""
msgid "Pause replication"
msgstr ""
msgid "Paused"
msgstr ""
msgid "Paused runners don't accept new jobs"
msgstr ""
......@@ -24794,6 +24818,9 @@ msgstr ""
msgid "ProjectsNew|Create blank project"
msgstr ""
msgid "ProjectsNew|Create blank project/repository"
msgstr ""
msgid "ProjectsNew|Create from template"
msgstr ""
......@@ -24812,6 +24839,9 @@ msgstr ""
msgid "ProjectsNew|Import project"
msgstr ""
msgid "ProjectsNew|Import project/repository"
msgstr ""
msgid "ProjectsNew|Initialize repository with a README"
msgstr ""
......
......@@ -5,7 +5,7 @@ module QA
module Dashboard
module Snippet
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 :global_new_snippet_link
end
......
......@@ -22,7 +22,7 @@ module QA
element :file_tree_table
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_issue_link, "link_to _('New issue'), new_project_issue_path(@project)" # rubocop:disable QA/ElementWithPattern
end
......
......@@ -448,6 +448,12 @@ RSpec.describe ProjectsController do
post :create, params: { project: project_params }
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
describe 'POST #archive' do
......
......@@ -12,6 +12,72 @@ RSpec.describe 'New project', :js do
sign_in(user)
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
Gitlab::CurrentSettings.update!(
restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL]
......
......@@ -1507,19 +1507,42 @@ describe('DiffsStoreActions', () => {
});
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`
value
${true}
${false}
`('commits SET_FILE_BY_FILE with the new value $value', ({ value }) => {
return testAction(
setFileByFile,
{ fileByFile: value },
{ viewDiffsFileByFile: null },
[{ type: types.SET_FILE_BY_FILE, payload: value }],
[],
);
});
`(
'commits SET_FILE_BY_FILE and persists the File-by-File user preference with the new value $value',
async ({ value }) => {
await testAction(
setFileByFile,
{ 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', () => {
......
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';
......@@ -17,6 +18,34 @@ describe('Experimental new project creation app', () => {
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', () => {
beforeEach(() => {
createComponent();
......
import { shallowMount } from '@vue/test-utils';
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';
jest.mock('~/experimentation/utils', () => ({ getExperimentData: jest.fn() }));
describe('Welcome page', () => {
let wrapper;
let trackingSpy;
......@@ -14,6 +19,7 @@ describe('Welcome page', () => {
beforeEach(() => {
trackingSpy = mockTracking('_category_', document, jest.spyOn);
trackingSpy.mockImplementation(() => {});
getExperimentData.mockReturnValue(undefined);
});
afterEach(() => {
......@@ -22,14 +28,35 @@ describe('Welcome page', () => {
wrapper = null;
});
it('tracks link clicks', () => {
it('tracks link clicks', async () => {
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(() => {
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', () => {
createComponent({ panels: [{ name: 'test', href: '#' }] });
......
......@@ -163,6 +163,7 @@ RSpec.describe 'layouts/header/_new_dropdown' do
end
it 'has a "New project" link' do
render('layouts/header/new_repo_experiment')
render
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