Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/pages')
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/integrations/index.js5
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue6
-rw-r--r--app/assets/javascripts/pages/admin/runners/index/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js71
-rw-r--r--app/assets/javascripts/pages/groups/new/components/app.vue55
-rw-r--r--app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue44
-rw-r--r--app/assets/javascripts/pages/groups/new/index.js33
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue6
-rw-r--r--app/assets/javascripts/pages/profiles/show/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js19
-rw-r--r--app/assets/javascripts/pages/projects/branches/index/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/feature_flags_user_lists/index/index.js25
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/app.vue5
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue93
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue8
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js4
-rw-r--r--app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue6
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js4
-rw-r--r--app/assets/javascripts/pages/projects/packages/infrastructure_registry/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue46
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js2
-rw-r--r--app/assets/javascripts/pages/projects/project.js2
-rw-r--r--app/assets/javascripts/pages/projects/project_members/index.js73
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/settings/integrations/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue2
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue2
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js4
-rw-r--r--app/assets/javascripts/pages/shared/mount_runner_aws_deployments.js17
-rw-r--r--app/assets/javascripts/pages/shared/nav/sidebar_tracking.js44
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue170
-rw-r--r--app/assets/javascripts/pages/shared/wikis/constants.js4
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js67
36 files changed, 606 insertions, 247 deletions
diff --git a/app/assets/javascripts/pages/admin/application_settings/integrations/index.js b/app/assets/javascripts/pages/admin/application_settings/integrations/index.js
index f318b6f62d5..53068f72d3f 100644
--- a/app/assets/javascripts/pages/admin/application_settings/integrations/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/integrations/index.js
@@ -1,8 +1,3 @@
import initIntegrationsList from '~/integrations/index';
-import PersistentUserCallout from '~/persistent_user_callout';
-
-const callout = document.querySelector('.js-admin-integrations-moved');
-
-PersistentUserCallout.factory(callout);
initIntegrationsList();
diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
index 798eeee48bf..ffccc1419a6 100644
--- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
+++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
@@ -1,6 +1,6 @@
<script>
import { GlModal } from '@gitlab/ui';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
@@ -31,7 +31,9 @@ export default {
redirectTo(response.request.responseURL);
})
.catch((error) => {
- createFlash(s__('AdminArea|Stopping jobs failed'));
+ createFlash({
+ message: s__('AdminArea|Stopping jobs failed'),
+ });
throw error;
});
},
diff --git a/app/assets/javascripts/pages/admin/runners/index/index.js b/app/assets/javascripts/pages/admin/runners/index/index.js
index 45ed3ac6bd8..d5563470394 100644
--- a/app/assets/javascripts/pages/admin/runners/index/index.js
+++ b/app/assets/javascripts/pages/admin/runners/index/index.js
@@ -2,6 +2,7 @@ import AdminRunnersFilteredSearchTokenKeys from '~/filtered_search/admin_runners
import { FILTERED_SEARCH } from '~/pages/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
+import { initRunnerList } from '~/runner/runner_list';
initFilteredSearch({
page: FILTERED_SEARCH.ADMIN_RUNNERS,
@@ -10,3 +11,7 @@ initFilteredSearch({
});
initInstallRunner();
+
+if (gon.features?.runnerListViewVueUi) {
+ initRunnerList();
+}
diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js
index b0a70055835..13656ee9b16 100644
--- a/app/assets/javascripts/pages/groups/group_members/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index.js
@@ -29,46 +29,43 @@ function mountRemoveMemberModal() {
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
-initMembersApp(document.querySelector('.js-group-members-list'), {
- namespace: MEMBER_TYPES.user,
- tableFields: SHARED_FIELDS.concat(['source', 'granted']),
- tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
- tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
- requestFormatter: groupMemberRequestFormatter,
- filteredSearchBar: {
- show: true,
- tokens: ['two_factor', 'with_inherited_permissions'],
- searchParam: 'search',
- placeholder: s__('Members|Filter members'),
- recentSearchesStorageKey: 'group_members',
+initMembersApp(document.querySelector('.js-group-members-list-app'), {
+ [MEMBER_TYPES.user]: {
+ tableFields: SHARED_FIELDS.concat(['source', 'granted']),
+ tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
+ tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
+ requestFormatter: groupMemberRequestFormatter,
+ filteredSearchBar: {
+ show: true,
+ tokens: ['two_factor', 'with_inherited_permissions'],
+ searchParam: 'search',
+ placeholder: s__('Members|Filter members'),
+ recentSearchesStorageKey: 'group_members',
+ },
},
-});
-
-initMembersApp(document.querySelector('.js-group-group-links-list'), {
- namespace: MEMBER_TYPES.group,
- tableFields: SHARED_FIELDS.concat('granted'),
- tableAttrs: {
- table: { 'data-qa-selector': 'groups_list' },
- tr: { 'data-qa-selector': 'group_row' },
+ [MEMBER_TYPES.group]: {
+ tableFields: SHARED_FIELDS.concat('granted'),
+ tableAttrs: {
+ table: { 'data-qa-selector': 'groups_list' },
+ tr: { 'data-qa-selector': 'group_row' },
+ },
+ requestFormatter: groupLinkRequestFormatter,
},
- requestFormatter: groupLinkRequestFormatter,
-});
-initMembersApp(document.querySelector('.js-group-invited-members-list'), {
- namespace: MEMBER_TYPES.invite,
- tableFields: SHARED_FIELDS.concat('invited'),
- requestFormatter: groupMemberRequestFormatter,
- filteredSearchBar: {
- show: true,
- tokens: [],
- searchParam: 'search_invited',
- placeholder: s__('Members|Search invited'),
- recentSearchesStorageKey: 'group_invited_members',
+ [MEMBER_TYPES.invite]: {
+ tableFields: SHARED_FIELDS.concat('invited'),
+ requestFormatter: groupMemberRequestFormatter,
+ filteredSearchBar: {
+ show: true,
+ tokens: [],
+ searchParam: 'search_invited',
+ placeholder: s__('Members|Search invited'),
+ recentSearchesStorageKey: 'group_invited_members',
+ },
+ },
+ [MEMBER_TYPES.accessRequest]: {
+ tableFields: SHARED_FIELDS.concat('requested'),
+ requestFormatter: groupMemberRequestFormatter,
},
-});
-initMembersApp(document.querySelector('.js-group-access-requests-list'), {
- namespace: MEMBER_TYPES.accessRequest,
- tableFields: SHARED_FIELDS.concat('requested'),
- requestFormatter: groupMemberRequestFormatter,
});
groupsSelect();
diff --git a/app/assets/javascripts/pages/groups/new/components/app.vue b/app/assets/javascripts/pages/groups/new/components/app.vue
new file mode 100644
index 00000000000..9aac364d20e
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/new/components/app.vue
@@ -0,0 +1,55 @@
+<script>
+import importGroupIllustration from '@gitlab/svgs/dist/illustrations/group-import.svg';
+import newGroupIllustration from '@gitlab/svgs/dist/illustrations/group-new.svg';
+
+import { s__ } from '~/locale';
+import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
+import createGroupDescriptionDetails from './create_group_description_details.vue';
+
+const PANELS = [
+ {
+ name: 'create-group-pane',
+ selector: '#create-group-pane',
+ title: s__('GroupsNew|Create group'),
+ description: s__(
+ 'GroupsNew|Assemble related projects together and grant members access to several projects at once.',
+ ),
+ illustration: newGroupIllustration,
+ details: createGroupDescriptionDetails,
+ },
+ {
+ name: 'import-group-pane',
+ selector: '#import-group-pane',
+ title: s__('GroupsNew|Import group'),
+ description: s__(
+ 'GroupsNew|Export groups with all their related data and move to a new GitLab instance.',
+ ),
+ illustration: importGroupIllustration,
+ details: 'Migrate your existing groups from another instance of GitLab.',
+ },
+];
+
+export default {
+ components: {
+ NewNamespacePage,
+ },
+ props: {
+ hasErrors: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ PANELS,
+};
+</script>
+
+<template>
+ <new-namespace-page
+ :jump-to-last-persisted-panel="hasErrors"
+ :initial-breadcrumb="s__('New group')"
+ :panels="$options.PANELS"
+ :title="s__('GroupsNew|Create new group')"
+ persistence-key="new_group_last_active_tab"
+ />
+</template>
diff --git a/app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue b/app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue
new file mode 100644
index 00000000000..ea08a0821a8
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue
@@ -0,0 +1,44 @@
+<script>
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+export default {
+ components: {
+ GlLink,
+ GlSprintf,
+ },
+ paths: {
+ groupsHelpPath: helpPagePath('user/group/index'),
+ subgroupsHelpPath: helpPagePath('user/group/subgroups/index'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <p>
+ <gl-sprintf
+ :message="
+ s__(
+ 'GroupsNew|%{linkStart}Groups%{linkEnd} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects. Groups can also be nested by creating subgroups.',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link :href="$options.paths.groupsHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <p>
+ <gl-sprintf
+ :message="
+ s__('GroupsNew|Groups can also be nested by creating %{linkStart}subgroups%{linkEnd}.')
+ "
+ >
+ <template #link="{ content }">
+ <gl-link :href="$options.paths.subgroupsHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js
index 569b5afd676..7557edb1b49 100644
--- a/app/assets/javascripts/pages/groups/new/index.js
+++ b/app/assets/javascripts/pages/groups/new/index.js
@@ -1,8 +1,9 @@
-import $ from 'jquery';
+import Vue from 'vue';
import BindInOut from '~/behaviors/bind_in_out';
import initFilePickers from '~/file_pickers';
import Group from '~/group';
-import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import NewGroupCreationApp from './components/app.vue';
import GroupPathValidator from './group_path_validator';
new GroupPathValidator(); // eslint-disable-line no-new
@@ -12,15 +13,21 @@ initFilePickers();
new Group(); // eslint-disable-line no-new
-const CONTAINER_SELECTOR = '.group-edit-container .nav-tabs';
-const DEFAULT_ACTION = '#create-group-pane';
-// eslint-disable-next-line no-new
-new LinkedTabs({
- defaultAction: DEFAULT_ACTION,
- parentEl: CONTAINER_SELECTOR,
- hashedTabs: true,
-});
-
-if (window.location.hash) {
- $(CONTAINER_SELECTOR).find(`a[href="${window.location.hash}"]`).tab('show');
+function initNewGroupCreation(el) {
+ const { hasErrors } = el.dataset;
+
+ const props = {
+ hasErrors: parseBoolean(hasErrors),
+ };
+
+ return new Vue({
+ el,
+ render(h) {
+ return h(NewGroupCreationApp, { props });
+ },
+ });
}
+
+const el = document.querySelector('.js-new-group-creation');
+
+initNewGroupCreation(el);
diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
index 636eea5d7ac..a8d7a83cdd6 100644
--- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
@@ -3,6 +3,7 @@ import GroupRunnersFilteredSearchTokenKeys from '~/filtered_search/group_runners
import initSharedRunnersForm from '~/group_settings/mount_shared_runners';
import { FILTERED_SEARCH } from '~/pages/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
+import { initRunnerAwsDeployments } from '~/pages/shared/mount_runner_aws_deployments';
import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
import initSettingsPanels from '~/settings_panels';
@@ -20,3 +21,4 @@ initSharedRunnersForm();
initVariableList();
initInstallRunner();
+initRunnerAwsDeployments();
diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
index 8d4997586dd..e42e89ce021 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
@@ -1,6 +1,6 @@
<script>
import { GlModal } from '@gitlab/ui';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__, sprintf } from '~/locale';
@@ -63,7 +63,9 @@ export default {
visitUrl(response.data.url);
})
.catch((error) => {
- createFlash(error);
+ createFlash({
+ message: error,
+ });
})
.finally(() => {
this.visible = false;
diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js
index b5441127797..226ef4c4e23 100644
--- a/app/assets/javascripts/pages/profiles/show/index.js
+++ b/app/assets/javascripts/pages/profiles/show/index.js
@@ -2,7 +2,7 @@ import emojiRegex from 'emoji-regex';
import $ from 'jquery';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import * as Emoji from '~/emoji';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import EmojiMenu from './emoji_menu';
@@ -81,4 +81,8 @@ Emoji.initEmojiMap()
}
});
})
- .catch(() => createFlash(__('Failed to load emoji list.')));
+ .catch(() =>
+ createFlash({
+ message: __('Failed to load emoji list.'),
+ }),
+ );
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index 8a8ce70e998..6cc0095f5a5 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import TableOfContents from '~/blob/components/table_contents.vue';
import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
import BlobViewer from '~/blob/viewer/index';
import GpgBadges from '~/gpg_badges';
@@ -19,12 +20,16 @@ const apolloProvider = new VueApollo({
const viewBlobEl = document.querySelector('#js-view-blob-app');
if (viewBlobEl) {
- const { blobPath, projectPath } = viewBlobEl.dataset;
+ const { blobPath, projectPath, targetBranch, originalBranch } = viewBlobEl.dataset;
// eslint-disable-next-line no-new
new Vue({
el: viewBlobEl,
apolloProvider,
+ provide: {
+ targetBranch,
+ originalBranch,
+ },
render(createElement) {
return createElement(BlobContentViewer, {
props: {
@@ -92,3 +97,15 @@ if (successPipelineEl) {
},
});
}
+
+const tableContentsEl = document.querySelector('.js-table-contents');
+
+if (tableContentsEl) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: tableContentsEl,
+ render(h) {
+ return h(TableOfContents);
+ },
+ });
+}
diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js
index 27ec746ad02..97dc76908af 100644
--- a/app/assets/javascripts/pages/projects/branches/index/index.js
+++ b/app/assets/javascripts/pages/projects/branches/index/index.js
@@ -3,6 +3,8 @@ import AjaxLoadingSpinner from '~/branches/ajax_loading_spinner';
import BranchSortDropdown from '~/branches/branch_sort_dropdown';
import DeleteModal from '~/branches/branches_delete_modal';
import initDiverganceGraph from '~/branches/divergence_graph';
+import initDeleteBranchButton from '~/branches/init_delete_branch_button';
+import initDeleteBranchModal from '~/branches/init_delete_branch_modal';
AjaxLoadingSpinner.init();
new DeleteModal(); // eslint-disable-line no-new
@@ -14,3 +16,9 @@ const { divergingCountsEndpoint, defaultBranch } = document.querySelector(
initDiverganceGraph(divergingCountsEndpoint, defaultBranch);
BranchSortDropdown();
initDeprecatedRemoveRowBehavior();
+
+document
+ .querySelectorAll('.js-delete-branch-button')
+ .forEach((elem) => initDeleteBranchButton(elem));
+
+initDeleteBranchModal();
diff --git a/app/assets/javascripts/pages/projects/feature_flags_user_lists/index/index.js b/app/assets/javascripts/pages/projects/feature_flags_user_lists/index/index.js
new file mode 100644
index 00000000000..519e04e14fb
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/feature_flags_user_lists/index/index.js
@@ -0,0 +1,25 @@
+/* eslint-disable no-new */
+
+import Vue from 'vue';
+import Vuex from 'vuex';
+import UserLists from '~/user_lists/components/user_lists.vue';
+import createStore from '~/user_lists/store/index';
+
+Vue.use(Vuex);
+
+const el = document.querySelector('#js-user-lists');
+
+const { featureFlagsHelpPagePath, errorStateSvgPath, projectId, newUserListPath } = el.dataset;
+
+new Vue({
+ el,
+ store: createStore({ projectId }),
+ provide: {
+ featureFlagsHelpPagePath,
+ errorStateSvgPath,
+ newUserListPath,
+ },
+ render(createElement) {
+ return createElement(UserLists);
+ },
+});
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/app.vue b/app/assets/javascripts/pages/projects/forks/new/components/app.vue
index 02b357d389b..7fb41c6e7b7 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/app.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/app.vue
@@ -38,6 +38,10 @@ export default {
type: String,
required: true,
},
+ restrictedVisibilityLevels: {
+ type: Array,
+ required: true,
+ },
},
};
</script>
@@ -66,6 +70,7 @@ export default {
:project-path="projectPath"
:project-description="projectDescription"
:project-visibility="projectVisibility"
+ :restricted-visibility-levels="restrictedVisibilityLevels"
/>
</div>
</div>
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
index 07cc0ce46bc..75c3b6d564c 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
@@ -26,10 +26,10 @@ const PRIVATE_VISIBILITY = 'private';
const INTERNAL_VISIBILITY = 'internal';
const PUBLIC_VISIBILITY = 'public';
-const ALLOWED_VISIBILITY = {
- private: [PRIVATE_VISIBILITY],
- internal: [INTERNAL_VISIBILITY, PRIVATE_VISIBILITY],
- public: [INTERNAL_VISIBILITY, PRIVATE_VISIBILITY, PUBLIC_VISIBILITY],
+const VISIBILITY_LEVEL = {
+ [PRIVATE_VISIBILITY]: 0,
+ [INTERNAL_VISIBILITY]: 10,
+ [PUBLIC_VISIBILITY]: 20,
};
const initFormField = ({ value, required = true, skipValidation = false }) => ({
@@ -95,6 +95,10 @@ export default {
type: String,
required: true,
},
+ restrictedVisibilityLevels: {
+ type: Array,
+ required: true,
+ },
},
data() {
const form = {
@@ -111,10 +115,7 @@ export default {
required: false,
skipValidation: true,
}),
- visibility: initFormField({
- value: this.projectVisibility,
- skipValidation: true,
- }),
+ visibility: initFormField({ value: this.getInitialVisibilityValue() }),
},
};
return {
@@ -127,14 +128,38 @@ export default {
projectUrl() {
return `${gon.gitlab_url}/`;
},
- projectAllowedVisibility() {
- return ALLOWED_VISIBILITY[this.projectVisibility];
+ projectVisibilityLevel() {
+ return VISIBILITY_LEVEL[this.projectVisibility];
+ },
+ namespaceVisibilityLevel() {
+ const visibility = this.form.fields.namespace.value?.visibility || PUBLIC_VISIBILITY;
+ return VISIBILITY_LEVEL[visibility];
+ },
+ visibilityLevelCap() {
+ return Math.min(this.projectVisibilityLevel, this.namespaceVisibilityLevel);
+ },
+ restrictedVisibilityLevelsSet() {
+ return new Set(this.restrictedVisibilityLevels);
},
- namespaceAllowedVisibility() {
- return (
- ALLOWED_VISIBILITY[this.form.fields.namespace.value?.visibility] ||
- ALLOWED_VISIBILITY[PUBLIC_VISIBILITY]
+ allowedVisibilityLevels() {
+ const allowedLevels = Object.entries(VISIBILITY_LEVEL).reduce(
+ (levels, [levelName, levelValue]) => {
+ if (
+ !this.restrictedVisibilityLevelsSet.has(levelValue) &&
+ levelValue <= this.visibilityLevelCap
+ ) {
+ levels.push(levelName);
+ }
+ return levels;
+ },
+ [],
);
+
+ if (!allowedLevels.length) {
+ return [PRIVATE_VISIBILITY];
+ }
+
+ return allowedLevels;
},
visibilityLevels() {
return [
@@ -142,7 +167,9 @@ export default {
text: s__('ForkProject|Private'),
value: PRIVATE_VISIBILITY,
icon: 'lock',
- help: s__('ForkProject|The project can be accessed without any authentication.'),
+ help: s__(
+ 'ForkProject|Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.',
+ ),
disabled: this.isVisibilityLevelDisabled(PRIVATE_VISIBILITY),
},
{
@@ -156,9 +183,7 @@ export default {
text: s__('ForkProject|Public'),
value: PUBLIC_VISIBILITY,
icon: 'earth',
- help: s__(
- 'ForkProject|Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.',
- ),
+ help: s__('ForkProject|The project can be accessed without any authentication.'),
disabled: this.isVisibilityLevelDisabled(PUBLIC_VISIBILITY),
},
];
@@ -166,12 +191,9 @@ export default {
},
watch: {
// eslint-disable-next-line func-names
- 'form.fields.namespace.value': function (newVal) {
- const { visibility } = newVal;
-
- if (this.projectAllowedVisibility.includes(visibility)) {
- this.form.fields.visibility.value = visibility;
- }
+ 'form.fields.namespace.value': function () {
+ this.form.fields.visibility.value =
+ this.restrictedVisibilityLevels.length !== 0 ? null : PRIVATE_VISIBILITY;
},
// eslint-disable-next-line func-names
'form.fields.name.value': function (newVal) {
@@ -186,11 +208,11 @@ export default {
const { data } = await axios.get(this.endpoint);
this.namespaces = data.namespaces;
},
- isVisibilityLevelDisabled(visibilityLevel) {
- return !(
- this.projectAllowedVisibility.includes(visibilityLevel) &&
- this.namespaceAllowedVisibility.includes(visibilityLevel)
- );
+ isVisibilityLevelDisabled(visibility) {
+ return !this.allowedVisibilityLevels.includes(visibility);
+ },
+ getInitialVisibilityValue() {
+ return this.restrictedVisibilityLevels.length !== 0 ? null : this.projectVisibility;
},
async onSubmit() {
this.form.showValidation = true;
@@ -222,7 +244,11 @@ export default {
redirectTo(data.web_url);
return;
} catch (error) {
- createFlash({ message: error });
+ createFlash({
+ message: s__(
+ 'ForkProject|An error occurred while forking the project. Please try again.',
+ ),
+ });
}
},
},
@@ -322,7 +348,11 @@ export default {
/>
</gl-form-group>
- <gl-form-group>
+ <gl-form-group
+ v-validation:[form.showValidation]
+ :invalid-feedback="s__('ForkProject|Please select a visibility level')"
+ :state="form.fields.visibility.state"
+ >
<label>
{{ s__('ForkProject|Visibility level') }}
<gl-link :href="visibilityHelpPath" target="_blank">
@@ -333,6 +363,7 @@ export default {
v-model="form.fields.visibility.value"
data-testid="fork-visibility-radio-group"
name="visibility"
+ :aria-label="__('visibility')"
required
>
<gl-form-radio
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue
index bc47b124f8b..10753de6cd0 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue
@@ -1,6 +1,6 @@
<script>
import { GlTabs, GlTab, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import ForkGroupsListItem from './fork_groups_list_item.vue';
@@ -44,7 +44,11 @@ export default {
.then((response) => {
this.namespaces = response.data.namespaces;
})
- .catch(() => createFlash(__('There was a problem fetching groups.')));
+ .catch(() =>
+ createFlash({
+ message: __('There was a problem fetching groups.'),
+ }),
+ );
},
},
diff --git a/app/assets/javascripts/pages/projects/forks/new/index.js b/app/assets/javascripts/pages/projects/forks/new/index.js
index 372967c8a1e..1a171252048 100644
--- a/app/assets/javascripts/pages/projects/forks/new/index.js
+++ b/app/assets/javascripts/pages/projects/forks/new/index.js
@@ -16,6 +16,7 @@ if (gon.features.forkProjectForm) {
projectPath,
projectDescription,
projectVisibility,
+ restrictedVisibilityLevels,
} = mountElement.dataset;
// eslint-disable-next-line no-new
@@ -38,6 +39,7 @@ if (gon.features.forkProjectForm) {
projectPath,
projectDescription,
projectVisibility,
+ restrictedVisibilityLevels: JSON.parse(restrictedVisibilityLevels),
},
});
},
diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js
index 45e9643b3f3..1eab3becbc3 100644
--- a/app/assets/javascripts/pages/projects/index.js
+++ b/app/assets/javascripts/pages/projects/index.js
@@ -1,5 +1,7 @@
import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation';
+import { initSidebarTracking } from '../shared/nav/sidebar_tracking';
import Project from './project';
new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
+initSidebarTracking();
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index 3143ff5adac..3cea61262ea 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -1,8 +1,6 @@
import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initIssuableSidebar from '~/init_issuable_sidebar';
-import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
-import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { IssuableType } from '~/issuable_show/constants';
import Issue from '~/issue';
import '~/notes/index';
@@ -34,8 +32,6 @@ export default function initShowIssue() {
initIssueHeaderActions(store);
initSentryErrorStackTraceApp();
initRelatedMergeRequestsApp();
- initInviteMembersModal();
- initInviteMembersTrigger();
import(/* webpackChunkName: 'design_management' */ '~/design_management')
.then((module) => module.default())
diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
index 81ffaa6f7a3..aaa9bb906b2 100644
--- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
@@ -1,6 +1,6 @@
<script>
import { GlSprintf, GlModal } from '@gitlab/ui';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__, __, sprintf } from '~/locale';
@@ -70,7 +70,9 @@ export default {
labelUrl: this.url,
successful: false,
});
- createFlash(error);
+ createFlash({
+ message: error,
+ });
});
},
},
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index 6cd3202815b..d6b6c9fe06a 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -4,8 +4,6 @@ import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initIssuableSidebar from '~/init_issuable_sidebar';
-import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
-import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import StatusBox from '~/issuable/components/status_box.vue';
import createDefaultClient from '~/lib/graphql';
import { handleLocationHash } from '~/lib/utils/common_utils';
@@ -29,8 +27,6 @@ export default function initMergeRequestShow() {
} else {
loadAwardsHandler();
}
- initInviteMembersModal();
- initInviteMembersTrigger();
const el = document.querySelector('.js-mr-status-box');
const apolloProvider = new VueApollo({ defaultClient: createDefaultClient() });
diff --git a/app/assets/javascripts/pages/projects/packages/infrastructure_registry/show/index.js b/app/assets/javascripts/pages/projects/packages/infrastructure_registry/show/index.js
new file mode 100644
index 00000000000..44d9e2ffb6e
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/packages/infrastructure_registry/show/index.js
@@ -0,0 +1,3 @@
+import initDetails from '~/packages_and_registries/infrastructure_registry/details_app_bundle';
+
+initDetails();
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index 159c619e16c..d0ec5668d21 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -1,7 +1,15 @@
<script>
-import { GlFormRadio, GlFormRadioGroup, GlLink, GlSprintf } from '@gitlab/ui';
+import {
+ GlFormRadio,
+ GlFormRadioGroup,
+ GlIcon,
+ GlLink,
+ GlSprintf,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import { getWeekdayNames } from '~/lib/utils/datetime_utility';
-import { s__, sprintf } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const KEY_EVERY_DAY = 'everyDay';
const KEY_EVERY_WEEK = 'everyWeek';
@@ -12,15 +20,25 @@ export default {
components: {
GlFormRadio,
GlFormRadioGroup,
+ GlIcon,
GlLink,
GlSprintf,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ mixins: [glFeatureFlagMixin()],
props: {
initialCronInterval: {
type: String,
required: false,
default: '',
},
+ dailyLimit: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -80,6 +98,17 @@ export default {
weekday() {
return getWeekdayNames()[this.randomWeekDayIndex];
},
+ parsedDailyLimit() {
+ return this.dailyLimit ? (24 * 60) / this.dailyLimit : null;
+ },
+ scheduleDailyLimitMsg() {
+ return sprintf(
+ __(
+ 'Scheduled pipelines cannot run more frequently than once per %{limit} minutes. A pipeline configured to run more frequently only starts after %{limit} minutes have elapsed since the last time it ran.',
+ ),
+ { limit: this.parsedDailyLimit },
+ );
+ },
},
watch: {
cronInterval() {
@@ -111,6 +140,11 @@ export default {
generateRandomDay() {
return Math.floor(Math.random() * 28);
},
+ showDailyLimitMessage({ value }) {
+ return (
+ value === KEY_CUSTOM && this.glFeatures.ciDailyLimitForPipelineSchedules && this.dailyLimit
+ );
+ },
},
};
</script>
@@ -131,7 +165,15 @@ export default {
</gl-link>
</template>
</gl-sprintf>
+
<template v-else>{{ option.text }}</template>
+
+ <gl-icon
+ v-if="showDailyLimitMessage(option)"
+ v-gl-tooltip.hover
+ name="question"
+ :title="scheduleDailyLimitMsg"
+ />
</gl-form-radio>
</gl-form-radio-group>
<input
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
index ce0e573fed2..9056c76d6ca 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
@@ -12,6 +12,7 @@ Vue.use(Translate);
function initIntervalPatternInput() {
const intervalPatternMount = document.getElementById('interval-pattern-input');
const initialCronInterval = intervalPatternMount?.dataset?.initialInterval;
+ const dailyLimit = intervalPatternMount.dataset?.dailyLimit;
return new Vue({
el: intervalPatternMount,
@@ -22,6 +23,7 @@ function initIntervalPatternInput() {
return createElement('interval-pattern-input', {
props: {
initialCronInterval,
+ dailyLimit,
},
});
},
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index 91f376060f8..3b24c2c128b 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -129,7 +129,7 @@ export default class Project {
const currentRef = $dropdown.data('ref');
// The split and startWith is to ensure an exact word match
// and avoid partial match ie. currentRef is "dev" and loc is "development"
- const splitPathAfterRefPortion = loc.split(currentRef)[1];
+ const splitPathAfterRefPortion = loc.split('/-/')[1].split(currentRef)[1];
const doesPathContainRef = splitPathAfterRefPortion?.startsWith('/');
if (doesPathContainRef) {
diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js
index 471798d2931..177dc346c60 100644
--- a/app/assets/javascripts/pages/projects/project_members/index.js
+++ b/app/assets/javascripts/pages/projects/project_members/index.js
@@ -42,46 +42,41 @@ initInviteMembersForm();
new UsersSelect(); // eslint-disable-line no-new
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
-initMembersApp(document.querySelector('.js-project-members-list'), {
- namespace: MEMBER_TYPES.user,
- tableFields: SHARED_FIELDS.concat(['source', 'granted']),
- tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
- tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
- requestFormatter: projectMemberRequestFormatter,
- filteredSearchBar: {
- show: true,
- tokens: ['with_inherited_permissions'],
- searchParam: 'search',
- placeholder: s__('Members|Filter members'),
- recentSearchesStorageKey: 'project_members',
+initMembersApp(document.querySelector('.js-project-members-list-app'), {
+ [MEMBER_TYPES.user]: {
+ tableFields: SHARED_FIELDS.concat(['source', 'granted']),
+ tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
+ tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
+ requestFormatter: projectMemberRequestFormatter,
+ filteredSearchBar: {
+ show: true,
+ tokens: ['with_inherited_permissions'],
+ searchParam: 'search',
+ placeholder: s__('Members|Filter members'),
+ recentSearchesStorageKey: 'project_members',
+ },
},
-});
-
-initMembersApp(document.querySelector('.js-project-group-links-list'), {
- namespace: MEMBER_TYPES.group,
- tableFields: SHARED_FIELDS.concat('granted'),
- tableAttrs: {
- table: { 'data-qa-selector': 'groups_list' },
- tr: { 'data-qa-selector': 'group_row' },
+ [MEMBER_TYPES.group]: {
+ tableFields: SHARED_FIELDS.concat('granted'),
+ tableAttrs: {
+ table: { 'data-qa-selector': 'groups_list' },
+ tr: { 'data-qa-selector': 'group_row' },
+ },
+ requestFormatter: groupLinkRequestFormatter,
+ filteredSearchBar: {
+ show: true,
+ tokens: [],
+ searchParam: 'search_groups',
+ placeholder: s__('Members|Search groups'),
+ recentSearchesStorageKey: 'project_group_links',
+ },
},
- requestFormatter: groupLinkRequestFormatter,
- filteredSearchBar: {
- show: true,
- tokens: [],
- searchParam: 'search_groups',
- placeholder: s__('Members|Search groups'),
- recentSearchesStorageKey: 'project_group_links',
+ [MEMBER_TYPES.invite]: {
+ tableFields: SHARED_FIELDS.concat('invited'),
+ requestFormatter: projectMemberRequestFormatter,
+ },
+ [MEMBER_TYPES.accessRequest]: {
+ tableFields: SHARED_FIELDS.concat('requested'),
+ requestFormatter: projectMemberRequestFormatter,
},
-});
-
-initMembersApp(document.querySelector('.js-project-invited-members-list'), {
- namespace: MEMBER_TYPES.invite,
- tableFields: SHARED_FIELDS.concat('invited'),
- requestFormatter: projectMemberRequestFormatter,
-});
-
-initMembersApp(document.querySelector('.js-project-access-requests-list'), {
- namespace: MEMBER_TYPES.accessRequest,
- tableFields: SHARED_FIELDS.concat('requested'),
- requestFormatter: projectMemberRequestFormatter,
});
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 10105af3561..db7b3bad6ed 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -4,6 +4,7 @@ import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers';
import initVariableList from '~/ci_variable_list';
import initDeployFreeze from '~/deploy_freeze';
import registrySettingsApp from '~/packages_and_registries/settings/project/registry_settings_bundle';
+import { initRunnerAwsDeployments } from '~/pages/shared/mount_runner_aws_deployments';
import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_toggle';
import initSettingsPanels from '~/settings_panels';
@@ -38,4 +39,5 @@ document.addEventListener('DOMContentLoaded', () => {
initArtifactsSettings();
initSharedRunnersToggle();
initInstallRunner();
+ initRunnerAwsDeployments();
});
diff --git a/app/assets/javascripts/pages/projects/settings/integrations/show/index.js b/app/assets/javascripts/pages/projects/settings/integrations/show/index.js
index 01ad87160c5..53068f72d3f 100644
--- a/app/assets/javascripts/pages/projects/settings/integrations/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/integrations/show/index.js
@@ -1,7 +1,3 @@
import initIntegrationsList from '~/integrations/index';
-import PersistentUserCallout from '~/persistent_user_callout';
-
-const callout = document.querySelector('.js-webhooks-moved-alert');
-PersistentUserCallout.factory(callout);
initIntegrationsList();
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
index c110c1d4d62..9fb8be3fdb9 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
@@ -91,7 +91,7 @@ export default {
label-position="hidden"
@change="toggleFeature"
/>
- <div class="select-wrapper gl-flex-fill-1">
+ <div class="select-wrapper gl-flex-grow-1">
<select
:disabled="displaySelectInput"
class="form-control project-repo-select select-control"
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 0b7b4c0ded1..11e6b4577e0 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -381,7 +381,7 @@ export default {
:label="s__('ProjectSettings|Project visibility')"
>
<div class="project-feature-controls gl-display-flex gl-align-items-center gl-my-3 gl-mx-0">
- <div class="select-wrapper gl-flex-fill-1">
+ <div class="select-wrapper gl-flex-grow-1">
<select
v-model="visibilityLevel"
:disabled="!canChangeVisibilityLevel"
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index 83e43d7ac48..26f8018a968 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -3,6 +3,8 @@ import Activities from '~/activities';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import BlobViewer from '~/blob/viewer/index';
import { initUploadForm } from '~/blob_edit/blob_bundle';
+import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
+import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import leaveByUrl from '~/namespaces/leave_by_url';
import initVueNotificationsDropdown from '~/notifications';
import { initUploadFileTrigger } from '~/projects/upload_file_experiment';
@@ -44,3 +46,5 @@ initVueNotificationsDropdown();
new ShortcutsNavigation(); // eslint-disable-line no-new
initUploadFileTrigger();
+initInviteMembersModal();
+initInviteMembersTrigger();
diff --git a/app/assets/javascripts/pages/shared/mount_runner_aws_deployments.js b/app/assets/javascripts/pages/shared/mount_runner_aws_deployments.js
new file mode 100644
index 00000000000..f3807a33a2b
--- /dev/null
+++ b/app/assets/javascripts/pages/shared/mount_runner_aws_deployments.js
@@ -0,0 +1,17 @@
+import Vue from 'vue';
+import RunnerAwsDeployments from '~/vue_shared/components/runner_aws_deployments/runner_aws_deployments.vue';
+
+export function initRunnerAwsDeployments(componentId = 'js-runner-aws-deployments') {
+ const el = document.getElementById(componentId);
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(RunnerAwsDeployments);
+ },
+ });
+}
diff --git a/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js b/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js
new file mode 100644
index 00000000000..79ce1a37d21
--- /dev/null
+++ b/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js
@@ -0,0 +1,44 @@
+function onSidebarLinkClick() {
+ const setDataTrackAction = (element, action) => {
+ element.setAttribute('data-track-action', action);
+ };
+
+ const setDataTrackExtra = (element, value) => {
+ const SIDEBAR_COLLAPSED = 'Collapsed';
+ const SIDEBAR_EXPANDED = 'Expanded';
+ const sidebarCollapsed = document
+ .querySelector('.nav-sidebar')
+ .classList.contains('js-sidebar-collapsed')
+ ? SIDEBAR_COLLAPSED
+ : SIDEBAR_EXPANDED;
+
+ element.setAttribute(
+ 'data-track-extra',
+ JSON.stringify({ sidebar_display: sidebarCollapsed, menu_display: value }),
+ );
+ };
+
+ const EXPANDED = 'Expanded';
+ const FLY_OUT = 'Fly out';
+ const CLICK_MENU_ACTION = 'click_menu';
+ const CLICK_MENU_ITEM_ACTION = 'click_menu_item';
+ const parentElement = this.parentNode;
+ const subMenuList = parentElement.closest('.sidebar-sub-level-items');
+
+ if (subMenuList) {
+ const isFlyOut = subMenuList.classList.contains('fly-out-list') ? FLY_OUT : EXPANDED;
+
+ setDataTrackExtra(parentElement, isFlyOut);
+ setDataTrackAction(parentElement, CLICK_MENU_ITEM_ACTION);
+ } else {
+ const isFlyOut = parentElement.classList.contains('is-showing-fly-out') ? FLY_OUT : EXPANDED;
+
+ setDataTrackExtra(parentElement, isFlyOut);
+ setDataTrackAction(parentElement, CLICK_MENU_ACTION);
+ }
+}
+export const initSidebarTracking = () => {
+ document.querySelectorAll('.nav-sidebar li[data-track-label] > a').forEach((link) => {
+ link.addEventListener('click', onSidebarLinkClick);
+ });
+};
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
index 43753926039..26f6d1d683a 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -14,8 +14,17 @@ import axios from '~/lib/utils/axios_utils';
import csrf from '~/lib/utils/csrf';
import { setUrlFragment } from '~/lib/utils/url_utility';
import { s__, sprintf } from '~/locale';
+import Tracking from '~/tracking';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import {
+ WIKI_CONTENT_EDITOR_TRACKING_LABEL,
+ CONTENT_EDITOR_LOADED_ACTION,
+ SAVED_USING_CONTENT_EDITOR_ACTION,
+} from '../constants';
+
+const trackingMixin = Tracking.mixin({
+ label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
+});
const MARKDOWN_LINK_TEXT = {
markdown: '[Link Title](page-slug)',
@@ -53,21 +62,30 @@ export default {
),
primaryAction: s__('WikiPage|Retry'),
},
- useNewEditor: s__('WikiPage|Use new editor'),
+ useNewEditor: {
+ primaryLabel: s__('WikiPage|Use the new editor'),
+ secondaryLabel: s__('WikiPage|Try this later'),
+ title: s__('WikiPage|Get a richer editing experience'),
+ text: s__(
+ "WikiPage|Try the new visual Markdown editor. Read the %{linkStart}documentation%{linkEnd} to learn what's currently supported.",
+ ),
+ },
switchToOldEditor: {
- label: s__('WikiPage|Switch to old editor'),
- helpText: s__("WikiPage|Switching will discard any changes you've made in the new editor."),
+ label: s__('WikiPage|Switch me back to the classic editor.'),
+ helpText: s__(
+ "WikiPage|This editor is in beta and may not display the page's contents properly. Switching back to the classic editor will discard changes you've made in the new editor.",
+ ),
modal: {
- title: s__('WikiPage|Are you sure you want to switch to the old editor?'),
- primary: s__('WikiPage|Switch to old editor'),
+ title: s__('WikiPage|Are you sure you want to switch back to the classic editor?'),
+ primary: s__('WikiPage|Switch to classic editor'),
cancel: s__('WikiPage|Keep editing'),
text: s__(
- "WikiPage|Switching to the old editor will discard any changes you've made in the new editor.",
+ "WikiPage|Switching to the classic editor will discard any changes you've made in the new editor.",
),
},
},
- helpText: s__(
- "WikiPage|This editor is in beta and may not display the page's contents properly.",
+ feedbackTip: s__(
+ 'Tell us your experiences with the new Markdown editor %{linkStart}in this feedback issue%{linkEnd}.',
),
},
linksHelpText: s__(
@@ -86,6 +104,7 @@ export default {
},
cancel: s__('WikiPage|Cancel'),
},
+ contentEditorFeedbackIssue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332629',
components: {
GlAlert,
GlForm,
@@ -104,13 +123,14 @@ export default {
directives: {
GlModalDirective,
},
- mixins: [glFeatureFlagMixin()],
+ mixins: [trackingMixin],
inject: ['formatOptions', 'pageInfo'],
data() {
return {
title: this.pageInfo.title?.trim() || '',
format: this.pageInfo.format || 'markdown',
- content: this.pageInfo.content?.trim() || '',
+ content: this.pageInfo.content || '',
+ isContentEditorAlertDismissed: false,
isContentEditorLoading: true,
useContentEditor: false,
commitMessage: '',
@@ -120,6 +140,10 @@ export default {
};
},
computed: {
+ noContent() {
+ if (this.isContentEditorActive) return this.contentEditor?.empty;
+ return !this.content.trim();
+ },
csrfToken() {
return csrf.token;
},
@@ -157,14 +181,17 @@ export default {
wikiSpecificMarkdownHelpPath() {
return setUrlFragment(this.pageInfo.markdownHelpPath, 'wiki-specific-markdown');
},
+ contentEditorHelpPath() {
+ return setUrlFragment(this.pageInfo.helpPath, 'gitlab-flavored-markdown-support');
+ },
isMarkdownFormat() {
return this.format === 'markdown';
},
- showContentEditorButton() {
- return this.isMarkdownFormat && !this.useContentEditor && this.glFeatures.wikiContentEditor;
+ showContentEditorAlert() {
+ return this.isMarkdownFormat && !this.useContentEditor && !this.isContentEditorAlertDismissed;
},
disableSubmitButton() {
- return !this.content || !this.title || this.contentEditorRenderFailed;
+ return this.noContent || !this.title || this.contentEditorRenderFailed;
},
isContentEditorActive() {
return this.isMarkdownFormat && this.useContentEditor;
@@ -188,6 +215,8 @@ export default {
handleFormSubmit() {
if (this.useContentEditor) {
this.content = this.contentEditor.getSerializedContent();
+
+ this.trackFormSubmit();
}
this.isDirty = false;
@@ -236,6 +265,8 @@ export default {
try {
await this.contentEditor.setSerializedContent(this.content);
this.isContentEditorLoading = false;
+
+ this.trackContentEditorLoaded();
} catch (e) {
this.contentEditorRenderFailed = true;
}
@@ -258,6 +289,20 @@ export default {
this.$refs.confirmSwitchToOldEditorModal.show();
}
},
+
+ trackContentEditorLoaded() {
+ this.track(CONTENT_EDITOR_LOADED_ACTION);
+ },
+
+ trackFormSubmit() {
+ if (this.isContentEditorActive) {
+ this.track(SAVED_USING_CONTENT_EDITOR_ACTION);
+ }
+ },
+
+ dismissContentEditorAlert() {
+ this.isContentEditorAlertDismissed = true;
+ },
},
};
</script>
@@ -275,11 +320,9 @@ export default {
:dismissible="false"
variant="danger"
:primary-button-text="$options.i18n.contentEditor.renderFailed.primaryAction"
- @primaryAction="retryInitContentEditor()"
+ @primaryAction="retryInitContentEditor"
>
- <p>
- {{ $options.i18n.contentEditor.renderFailed.message }}
- </p>
+ {{ $options.i18n.contentEditor.renderFailed.message }}
</gl-alert>
<input :value="csrfToken" type="hidden" name="authenticity_token" />
@@ -299,7 +342,7 @@ export default {
<div class="col-sm-10">
<input
id="wiki_title"
- v-model.trim="title"
+ v-model="title"
name="wiki[title]"
type="text"
class="form-control"
@@ -337,46 +380,50 @@ export default {
{{ label }}
</option>
</select>
- <div>
- <gl-button
- v-if="showContentEditorButton"
- category="secondary"
- variant="confirm"
- class="gl-mt-4"
- @click="initContentEditor"
- >{{ $options.i18n.contentEditor.useNewEditor }}</gl-button
- >
- <div v-if="isContentEditorActive" class="gl-mt-4 gl-display-flex">
- <div class="gl-mr-4">
- <gl-button category="secondary" variant="confirm" @click="confirmSwitchToOldEditor">{{
- $options.i18n.contentEditor.switchToOldEditor.label
- }}</gl-button>
- </div>
- <div class="gl-mt-2">
- <gl-icon name="warning" />
- {{ $options.i18n.contentEditor.switchToOldEditor.helpText }}
- </div>
- </div>
- <gl-modal
- ref="confirmSwitchToOldEditorModal"
- modal-id="confirm-switch-to-old-editor"
- :title="$options.i18n.contentEditor.switchToOldEditor.modal.title"
- :action-primary="{ text: $options.i18n.contentEditor.switchToOldEditor.modal.primary }"
- :action-cancel="{ text: $options.i18n.contentEditor.switchToOldEditor.modal.cancel }"
- @primary="switchToOldEditor"
- >
- {{ $options.i18n.contentEditor.switchToOldEditor.modal.text }}
- </gl-modal>
- </div>
</div>
</div>
- <div class="form-group row">
+ <div class="form-group row" data-testid="wiki-form-content-fieldset">
<div class="col-sm-2 col-form-label">
<label class="control-label-full-width" for="wiki_content">{{
$options.i18n.content.label
}}</label>
</div>
<div class="col-sm-10">
+ <gl-alert
+ v-if="showContentEditorAlert"
+ class="gl-mb-6"
+ variant="info"
+ :primary-button-text="$options.i18n.contentEditor.useNewEditor.primaryLabel"
+ :secondary-button-text="$options.i18n.contentEditor.useNewEditor.secondaryLabel"
+ :dismiss-label="$options.i18n.contentEditor.useNewEditor.secondaryLabel"
+ :title="$options.i18n.contentEditor.useNewEditor.title"
+ @primaryAction="initContentEditor"
+ @secondaryAction="dismissContentEditorAlert"
+ @dismiss="dismissContentEditorAlert"
+ >
+ <gl-sprintf :message="$options.i18n.contentEditor.useNewEditor.text">
+ <template
+ #link="// eslint-disable-next-line vue/no-template-shadow
+ { content }"
+ ><gl-link
+ :href="contentEditorHelpPath"
+ target="_blank"
+ data-testid="content-editor-help-link"
+ >{{ content }}</gl-link
+ ></template
+ >
+ </gl-sprintf>
+ </gl-alert>
+ <gl-modal
+ ref="confirmSwitchToOldEditorModal"
+ modal-id="confirm-switch-to-old-editor"
+ :title="$options.i18n.contentEditor.switchToOldEditor.modal.title"
+ :action-primary="{ text: $options.i18n.contentEditor.switchToOldEditor.modal.primary }"
+ :action-cancel="{ text: $options.i18n.contentEditor.switchToOldEditor.modal.cancel }"
+ @primary="switchToOldEditor"
+ >
+ {{ $options.i18n.contentEditor.switchToOldEditor.modal.text }}
+ </gl-modal>
<markdown-field
v-if="!isContentEditorActive"
:markdown-preview-path="pageInfo.markdownPreviewPath"
@@ -391,7 +438,7 @@ export default {
<textarea
id="wiki_content"
ref="textarea"
- v-model.trim="content"
+ v-model="content"
name="wiki[content]"
class="note-textarea js-gfm-input js-autosize markdown-area"
dir="auto"
@@ -407,6 +454,20 @@ export default {
</markdown-field>
<div v-if="isContentEditorActive">
+ <gl-alert class="gl-mb-6" variant="tip" :dismissable="false">
+ <gl-sprintf :message="$options.i18n.contentEditor.feedbackTip">
+ <template
+ #link="// eslint-disable-next-line vue/no-template-shadow
+ { content }"
+ ><gl-link
+ :href="$options.contentEditorFeedbackIssue"
+ target="_blank"
+ data-testid="wiki-markdown-help-link"
+ >{{ content }}</gl-link
+ ></template
+ >
+ </gl-sprintf>
+ </gl-alert>
<gl-loading-icon v-if="isContentEditorLoading" class="bordered-box gl-w-full gl-py-6" />
<content-editor v-else :content-editor="contentEditor" />
<input id="wiki_content" v-model.trim="content" type="hidden" name="wiki[content]" />
@@ -432,7 +493,10 @@ export default {
>
</gl-sprintf>
<span v-else>
- {{ $options.i18n.contentEditor.helpText }}
+ {{ $options.i18n.contentEditor.switchToOldEditor.helpText }}
+ <gl-button variant="link" @click="confirmSwitchToOldEditor">{{
+ $options.i18n.contentEditor.switchToOldEditor.label
+ }}</gl-button>
</span>
</div>
</div>
diff --git a/app/assets/javascripts/pages/shared/wikis/constants.js b/app/assets/javascripts/pages/shared/wikis/constants.js
new file mode 100644
index 00000000000..b358ac9cf52
--- /dev/null
+++ b/app/assets/javascripts/pages/shared/wikis/constants.js
@@ -0,0 +1,4 @@
+export const WIKI_CONTENT_EDITOR_TRACKING_LABEL = 'wiki_content_editor';
+
+export const CONTENT_EDITOR_LOADED_ACTION = 'content_editor_loaded';
+export const SAVED_USING_CONTENT_EDITOR_ACTION = 'saved_using_content_editor';
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index c416106fdd8..03dba699461 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -1,4 +1,3 @@
-import { scaleLinear, scaleThreshold } from 'd3-scale';
import { select } from 'd3-selection';
import dateFormat from 'dateformat';
import $ from 'jquery';
@@ -8,7 +7,7 @@ import axios from '~/lib/utils/axios_utils';
import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
import { n__, s__, __ } from '~/locale';
-const d3 = { select, scaleLinear, scaleThreshold };
+const d3 = { select };
const firstDayOfWeekChoices = Object.freeze({
sunday: 0,
@@ -16,6 +15,14 @@ const firstDayOfWeekChoices = Object.freeze({
saturday: 6,
});
+const CONTRIB_LEGENDS = [
+ { title: __('No contributions'), min: 0 },
+ { title: __('1-9 contributions'), min: 1 },
+ { title: __('10-19 contributions'), min: 10 },
+ { title: __('20-29 contributions'), min: 20 },
+ { title: __('30+ contributions'), min: 30 },
+];
+
const LOADING_HTML = `
<div class="text-center">
<div class="spinner spinner-md"></div>
@@ -42,7 +49,17 @@ function formatTooltipText({ date, count }) {
return `${contribText}<br /><span class="gl-text-gray-300">${dateDayName} ${dateText}</span>`;
}
-const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]);
+// Return the contribution level from the number of contributions
+export const getLevelFromContributions = (count) => {
+ if (count <= 0) {
+ return 0;
+ }
+
+ const nextLevel = CONTRIB_LEGENDS.findIndex(({ min }) => count < min);
+
+ // If there is no higher level, we are at the end
+ return nextLevel >= 0 ? nextLevel - 1 : CONTRIB_LEGENDS.length - 1;
+};
export default class ActivityCalendar {
constructor(
@@ -111,10 +128,6 @@ export default class ActivityCalendar {
innerArray.push({ count, date, day });
}
- // Init color functions
- this.colorKey = initColorKey();
- this.color = this.initColor();
-
// Init the svg element
this.svg = this.renderSvg(container, group);
this.renderDays();
@@ -180,9 +193,7 @@ export default class ActivityCalendar {
.attr('y', (stamp) => this.dayYPos(stamp.day))
.attr('width', this.daySize)
.attr('height', this.daySize)
- .attr('fill', (stamp) =>
- stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed',
- )
+ .attr('data-level', (stamp) => getLevelFromContributions(stamp.count))
.attr('title', (stamp) => formatTooltipText(stamp))
.attr('class', 'user-contrib-cell has-tooltip')
.attr('data-html', true)
@@ -246,50 +257,24 @@ export default class ActivityCalendar {
}
renderKey() {
- const keyValues = [
- __('No contributions'),
- __('1-9 contributions'),
- __('10-19 contributions'),
- __('20-29 contributions'),
- __('30+ contributions'),
- ];
- const keyColors = [
- '#ededed',
- this.colorKey(0),
- this.colorKey(1),
- this.colorKey(2),
- this.colorKey(3),
- ];
-
this.svg
.append('g')
.attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
.selectAll('rect')
- .data(keyColors)
+ .data(CONTRIB_LEGENDS)
.enter()
.append('rect')
.attr('width', this.daySize)
.attr('height', this.daySize)
- .attr('x', (color, i) => this.daySizeWithSpace * i)
+ .attr('x', (_, i) => this.daySizeWithSpace * i)
.attr('y', 0)
- .attr('fill', (color) => color)
- .attr('class', 'has-tooltip')
- .attr('title', (color, i) => keyValues[i])
+ .attr('data-level', (_, i) => i)
+ .attr('class', 'user-contrib-cell has-tooltip contrib-legend')
+ .attr('title', (x) => x.title)
.attr('data-container', 'body')
.attr('data-html', true);
}
- initColor() {
- const colorRange = [
- '#ededed',
- this.colorKey(0),
- this.colorKey(1),
- this.colorKey(2),
- this.colorKey(3),
- ];
- return d3.scaleThreshold().domain([0, 10, 20, 30]).range(colorRange);
- }
-
clickDay(stamp) {
if (this.currentSelectedDate !== stamp.date) {
this.currentSelectedDate = stamp.date;