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
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-11-24 15:12:29 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-11-24 15:12:29 +0300
commit83ffdf48518e121c1ccab9d7913d3f7e79d7766c (patch)
tree478c690873fcd6b448968f4ff3158e7050b04917 /app
parentaab5db568e1090b5a9ae9cec6d788f88ac6faa4c (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue18
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/index.js2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/options.js2
-rw-r--r--app/assets/javascripts/lib/utils/datetime/locale_dateformat.js53
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue27
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue8
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_form.vue6
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/frequent_groups.vue16
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/frequent_item.vue28
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue47
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/frequent_projects.vue16
-rw-r--r--app/assets/javascripts/super_sidebar/constants.js3
-rw-r--r--app/assets/javascripts/super_sidebar/utils.js49
-rw-r--r--app/graphql/resolvers/ci/catalog/resources_resolver.rb18
-rw-r--r--app/graphql/resolvers/users/frecent_groups_resolver.rb6
-rw-r--r--app/graphql/resolvers/users/frecent_projects_resolver.rb4
-rw-r--r--app/graphql/types/ci/catalog/resource_scope_enum.rb1
-rw-r--r--app/graphql/types/query_type.rb6
-rw-r--r--app/helpers/ci/pipeline_editor_helper.rb1
-rw-r--r--app/models/ci/catalog/listing.rb19
21 files changed, 125 insertions, 207 deletions
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
index 204eaf20664..51ca69478b0 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
@@ -14,6 +14,7 @@ import {
export default {
i18n: {
+ browseCatalog: __('Browse CI/CD Catalog'),
browseTemplates: __('Browse templates'),
help: __('Help'),
jobAssistant: s__('JobAssistant|Job assistant'),
@@ -24,7 +25,7 @@ export default {
GlButton,
},
mixins: [glFeatureFlagMixin(), Tracking.mixin()],
- inject: ['aiChatAvailable'],
+ inject: ['aiChatAvailable', 'ciCatalogPath'],
props: {
showHelpDrawer: {
type: Boolean,
@@ -65,6 +66,11 @@ export default {
this.showAiAssistantDrawer ? EDITOR_APP_DRAWER_NONE : EDITOR_APP_DRAWER_AI_ASSISTANT,
);
},
+ trackCatalogBrowsing() {
+ const { label, actions } = pipelineEditorTrackingOptions;
+
+ this.track(actions.browseCatalog, { label });
+ },
trackHelpDrawerClick() {
const { label, actions } = pipelineEditorTrackingOptions;
this.track(actions.openHelpDrawer, { label });
@@ -84,6 +90,16 @@ export default {
>
<slot></slot>
<gl-button
+ :href="ciCatalogPath"
+ size="small"
+ icon="external-link"
+ target="_blank"
+ data-testid="catalog-repo-link"
+ @click="trackCatalogBrowsing"
+ >
+ {{ $options.i18n.browseCatalog }}
+ </gl-button>
+ <gl-button
:href="$options.TEMPLATE_REPOSITORY_URL"
size="small"
icon="external-link"
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
index c7c15cdd76e..9312e1c31ca 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -1,6 +1,5 @@
<script>
import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
-import CiEditorHeader from 'ee_else_ce/ci/pipeline_editor/components/editor/ci_editor_header.vue';
import { s__, __ } from '~/locale';
import PipelineGraph from '~/ci/pipeline_editor/components/graph/pipeline_graph.vue';
import { getParameterValues, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
@@ -20,6 +19,7 @@ import {
} from '../constants';
import getAppStatus from '../graphql/queries/client/app_status.query.graphql';
import CiConfigMergedPreview from './editor/ci_config_merged_preview.vue';
+import CiEditorHeader from './editor/ci_editor_header.vue';
import CiValidate from './validate/ci_validate.vue';
import TextEditor from './editor/text_editor.vue';
import EditorTab from './ui/editor_tab.vue';
diff --git a/app/assets/javascripts/ci/pipeline_editor/index.js b/app/assets/javascripts/ci/pipeline_editor/index.js
index bc20e478876..408e91a4d62 100644
--- a/app/assets/javascripts/ci/pipeline_editor/index.js
+++ b/app/assets/javascripts/ci/pipeline_editor/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import { createAppOptions } from 'ee_else_ce/ci/pipeline_editor/options';
+import { createAppOptions } from '~/ci/pipeline_editor/options';
export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
const el = document.querySelector(selector);
diff --git a/app/assets/javascripts/ci/pipeline_editor/options.js b/app/assets/javascripts/ci/pipeline_editor/options.js
index 340cb6ab979..2a34a2fad42 100644
--- a/app/assets/javascripts/ci/pipeline_editor/options.js
+++ b/app/assets/javascripts/ci/pipeline_editor/options.js
@@ -19,6 +19,7 @@ export const createAppOptions = (el) => {
initialBranchName,
pipelineEtag,
// Add to provide/inject API for static values
+ ciCatalogPath,
ciConfigPath,
ciExamplesHelpPagePath,
ciHelpPagePath,
@@ -110,6 +111,7 @@ export const createAppOptions = (el) => {
apolloProvider,
provide: {
aiChatAvailable: parseBoolean(aiChatAvailable),
+ ciCatalogPath,
ciConfigPath,
ciExamplesHelpPagePath,
ciHelpPagePath,
diff --git a/app/assets/javascripts/lib/utils/datetime/locale_dateformat.js b/app/assets/javascripts/lib/utils/datetime/locale_dateformat.js
index f1446fa5ac4..a4d911a6699 100644
--- a/app/assets/javascripts/lib/utils/datetime/locale_dateformat.js
+++ b/app/assets/javascripts/lib/utils/datetime/locale_dateformat.js
@@ -31,8 +31,24 @@ export const DATE_TIME_FULL_FORMAT = 'asDateTimeFull';
* localeDateFormat[DATE_ONLY_FORMAT].formatRange(date, date) // returns 'Jul 05 - Jul 07, 2023'
*/
export const DATE_ONLY_FORMAT = 'asDate';
+
+/**
+ * Format a Date with the help of {@link DateTimeFormat.asTime}
+ *
+ * Note: In case you can use localeDateFormat.asTime directly, please do that.
+ *
+ * @example
+ * localeDateFormat[TIME_ONLY_FORMAT].format(date) // returns '2:43'
+ * localeDateFormat[TIME_ONLY_FORMAT].formatRange(date, date) // returns '2:43 - 6:27 PM'
+ */
+export const TIME_ONLY_FORMAT = 'asTime';
export const DEFAULT_DATE_TIME_FORMAT = DATE_WITH_TIME_FORMAT;
-export const DATE_TIME_FORMATS = [DATE_WITH_TIME_FORMAT, DATE_TIME_FULL_FORMAT, DATE_ONLY_FORMAT];
+export const DATE_TIME_FORMATS = [
+ DATE_WITH_TIME_FORMAT,
+ DATE_TIME_FULL_FORMAT,
+ DATE_ONLY_FORMAT,
+ TIME_ONLY_FORMAT,
+];
/**
* The DateTimeFormat utilities support formatting a number of types,
@@ -133,6 +149,38 @@ class DateTimeFormat {
}
/**
+ * Locale aware formatter to display only the time.
+ *
+ * Use {@link DateTimeFormat.asDateTime} if you also need to display the date.
+ *
+ *
+ * @example
+ * // en-US: returns something like 2:43 PM
+ * // en-GB: returns something like 14:43
+ * localeDateFormat.asTime.format(date)
+ *
+ * Note: If formatting a _range_ and the dates are not on the same day,
+ * the formatter will do something sensible like:
+ * 7/9/1983, 2:43 PM – 7/12/1983, 12:36 PM
+ *
+ * @example
+ * // en-US: returns something like 2:43 – 6:27 PM
+ * // en-GB: returns something like 14:43 – 18:27
+ * localeDateFormat.asTime.formatRange(date, date2)
+ *
+ * @returns {DateTimeFormatter}
+ */
+ get asTime() {
+ return (
+ this.#formatters[TIME_ONLY_FORMAT] ||
+ this.#createFormatter(TIME_ONLY_FORMAT, {
+ timeStyle: 'short',
+ hourCycle: DateTimeFormat.#hourCycle,
+ })
+ );
+ }
+
+ /**
* Resets the memoized formatters
*
* While this method only seems to be useful for testing right now,
@@ -218,5 +266,8 @@ class DateTimeFormat {
*
* Date (showing date only):
* - {@link DateTimeFormat.asDate localeDateFormat.asDate} - the default format for a date
+ *
+ * Time (showing time only):
+ * - {@link DateTimeFormat.asTime localeDateFormat.asTime} - the default format for a time
*/
export const localeDateFormat = new DateTimeFormat();
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index 3da7daa3eec..912cc4d2b1c 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -81,22 +81,27 @@ export default {
return ['', '/'].indexOf(this.path) === -1;
},
},
- watch: {
- $route: function routeChange() {
- this.$options.totalRowsLoaded = -1;
- },
- },
- totalRowsLoaded: -1,
methods: {
showMore() {
this.$emit('showMore');
},
- generateRowNumber(path, id, index) {
- const key = `${path}-${id}-${index}`;
+ generateRowNumber(entry, index) {
+ const { flatPath, id } = entry;
+ const key = `${flatPath}-${id}-${index}`;
+
+ // We adjust the offset that we request based on the type of entry
+ const numTrees = this.entries?.trees?.length || 0;
+ const numBlobs = this.entries?.blobs?.length || 0;
if (!this.rowNumbers[key] && this.rowNumbers[key] !== 0) {
- this.$options.totalRowsLoaded += 1;
- this.rowNumbers[key] = this.$options.totalRowsLoaded;
+ if (entry.type === 'commit') {
+ // submodules are rendered before blobs but are in the last pages the api response
+ this.rowNumbers[key] = numTrees + numBlobs + index;
+ } else if (entry.type === 'blob') {
+ this.rowNumbers[key] = numTrees + index;
+ } else {
+ this.rowNumbers[key] = index;
+ }
}
return this.rowNumbers[key];
@@ -145,7 +150,7 @@ export default {
:lfs-oid="entry.lfsOid"
:loading-path="loadingPath"
:total-entries="totalEntries"
- :row-number="generateRowNumber(entry.flatPath, entry.id, index)"
+ :row-number="generateRowNumber(entry, index)"
:commit-info="getCommit(entry.name)"
v-on="$listeners"
/>
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
index dd2cfddc94e..49d9e09dde5 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -165,12 +165,8 @@ export default {
return;
}
- // Since a user could scroll either up or down, we want to support lazy loading in both directions
- this.loadCommitData(rowNumber);
-
- if (rowNumber - COMMIT_BATCH_SIZE >= 0) {
- this.loadCommitData(rowNumber - COMMIT_BATCH_SIZE);
- }
+ // Assume we are loading from the top and greedily choose offsets in multiples of COMMIT_BATCH_SIZE to minimize number of requests
+ this.loadCommitData(rowNumber - (rowNumber % COMMIT_BATCH_SIZE));
},
loadCommitData(rowNumber) {
loadCommits(this.projectPath, this.path, this.ref, rowNumber, this.refType)
diff --git a/app/assets/javascripts/set_status_modal/set_status_form.vue b/app/assets/javascripts/set_status_modal/set_status_form.vue
index 60ed0d073fe..b6d609ab1fa 100644
--- a/app/assets/javascripts/set_status_modal/set_status_form.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_form.vue
@@ -14,7 +14,7 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import * as Emoji from '~/emoji';
import { s__ } from '~/locale';
-import { formatDate, newDate, nSecondsAfter, isToday } from '~/lib/utils/datetime_utility';
+import { newDate, nSecondsAfter, isToday, localeDateFormat } from '~/lib/utils/datetime_utility';
import { TIME_RANGES_WITH_NEVER, AVAILABILITY_STATUS, NEVER_TIME_RANGE } from './constants';
export default {
@@ -148,10 +148,10 @@ export default {
},
formatClearStatusAfterDate(date) {
if (isToday(date)) {
- return formatDate(date, 'h:MMtt');
+ return localeDateFormat.asTime.format(date);
}
- return formatDate(date, 'mmm d, yyyy h:MMtt');
+ return localeDateFormat.asDateTime.format(date);
},
},
TIME_RANGES_WITH_NEVER,
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_groups.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_groups.vue
index e7e7e54ce77..252967b33b5 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_groups.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_groups.vue
@@ -1,7 +1,5 @@
<script>
import { s__ } from '~/locale';
-import { MAX_FREQUENT_GROUPS_COUNT } from '~/super_sidebar/constants';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import currentUserFrecentGroupsQuery from '~/super_sidebar/graphql/queries/current_user_frecent_groups.query.graphql';
import FrequentItems from './frequent_items.vue';
@@ -10,29 +8,17 @@ export default {
apollo: {
frecentGroups: {
query: currentUserFrecentGroupsQuery,
- skip() {
- return !this.glFeatures.frecentNamespacesSuggestions;
- },
},
},
components: {
FrequentItems,
},
- mixins: [glFeatureFlagsMixin()],
inject: ['groupsPath'],
- data() {
- const username = gon.current_username;
-
- return {
- storageKey: username ? `${username}/frequent-groups` : null,
- };
- },
i18n: {
groupName: s__('Navigation|Frequently visited groups'),
viewAllText: s__('Navigation|View all my groups'),
emptyStateText: s__('Navigation|Groups you visit often will appear here.'),
},
- MAX_FREQUENT_GROUPS_COUNT,
};
</script>
@@ -41,8 +27,6 @@ export default {
:loading="$apollo.queries.frecentGroups.loading"
:empty-state-text="$options.i18n.emptyStateText"
:group-name="$options.i18n.groupName"
- :max-items="$options.MAX_FREQUENT_GROUPS_COUNT"
- :storage-key="storageKey"
:items="frecentGroups"
view-all-items-icon="group"
:view-all-items-text="$options.i18n.viewAllText"
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_item.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_item.vue
index b0007c21cdc..b76d238c559 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_item.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_item.vue
@@ -1,33 +1,17 @@
<script>
-import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
-import { __ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'FrequentlyVisitedItem',
components: {
- GlButton,
ProjectAvatar,
},
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- mixins: [glFeatureFlagsMixin()],
props: {
item: {
type: Object,
required: true,
},
},
- methods: {
- onRemove() {
- this.$emit('remove', this.item);
- },
- },
- i18n: {
- remove: __('Remove'),
- },
};
</script>
@@ -51,17 +35,5 @@ export default {
{{ item.subtitle }}
</div>
</div>
-
- <gl-button
- v-if="!glFeatures.frecentNamespacesSuggestions"
- v-gl-tooltip.left
- icon="dash"
- category="tertiary"
- :aria-label="$options.i18n.remove"
- :title="$options.i18n.remove"
- class="show-on-focus-or-hover--target"
- @click.stop.prevent="onRemove"
- @keydown.enter.stop.prevent="onRemove"
- />
</div>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue
index 2e431c4f8da..60692361683 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue
@@ -1,9 +1,7 @@
<script>
import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlIcon } from '@gitlab/ui';
import { truncateNamespace } from '~/lib/utils/text_utility';
-import { getItemsFromLocalStorage, removeItemFromLocalStorage } from '~/super_sidebar/utils';
import { TRACKING_UNKNOWN_PANEL } from '~/super_sidebar/constants';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { TRACKING_CLICK_COMMAND_PALETTE_ITEM } from '../command_palette/constants';
import FrequentItem from './frequent_item.vue';
import FrequentItemSkeleton from './frequent_item_skeleton.vue';
@@ -17,7 +15,6 @@ export default {
FrequentItem,
FrequentItemSkeleton,
},
- mixins: [glFeatureFlagsMixin()],
props: {
loading: {
type: Boolean,
@@ -32,15 +29,6 @@ export default {
type: String,
required: true,
},
- maxItems: {
- type: Number,
- required: true,
- },
- storageKey: {
- type: String,
- required: false,
- default: null,
- },
viewAllItemsText: {
type: String,
required: true,
@@ -60,21 +48,12 @@ export default {
default: () => [],
},
},
- data() {
- return {
- localItems: getItemsFromLocalStorage({
- storageKey: this.storageKey,
- maxItems: this.maxItems,
- }),
- };
- },
computed: {
formattedItems() {
- const items = this.glFeatures.frecentNamespacesSuggestions ? this.items : this.localItems;
// Each item needs two different representations. One is for the
// GlDisclosureDropdownItem, and the other is for the FrequentItem
// renderer component inside it.
- return items.map((item) => ({
+ return this.items.map((item) => ({
forDropdown: {
id: item.id,
@@ -107,29 +86,11 @@ export default {
};
},
},
- created() {
- if (!this.glFeatures.frecentNamespacesSuggestions && !this.storageKey) {
- this.$emit('nothing-to-render');
- }
- },
- methods: {
- removeItem(item) {
- if (this.glFeatures.frecentNamespacesSuggestions) {
- return;
- }
- removeItemFromLocalStorage({
- storageKey: this.storageKey,
- item,
- });
-
- this.localItems = this.localItems.filter((i) => i.id !== item.id);
- },
- },
};
</script>
<template>
- <gl-disclosure-dropdown-group v-if="storageKey" v-bind="$attrs">
+ <gl-disclosure-dropdown-group v-bind="$attrs">
<template #group-label>{{ groupName }}</template>
<gl-disclosure-dropdown-item v-if="loading">
@@ -142,9 +103,7 @@ export default {
:item="item.forDropdown"
class="show-on-focus-or-hover--context"
>
- <template #list-item
- ><frequent-item :item="item.forRenderer" @remove="removeItem"
- /></template>
+ <template #list-item><frequent-item :item="item.forRenderer" /></template>
</gl-disclosure-dropdown-item>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_projects.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_projects.vue
index 4a269d1b876..2d13ab3dd4a 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_projects.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_projects.vue
@@ -1,7 +1,5 @@
<script>
import { s__ } from '~/locale';
-import { MAX_FREQUENT_PROJECTS_COUNT } from '~/super_sidebar/constants';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import currentUserFrecentProjectsQuery from '~/super_sidebar/graphql/queries/current_user_frecent_projects.query.graphql';
import FrequentItems from './frequent_items.vue';
@@ -10,29 +8,17 @@ export default {
apollo: {
frecentProjects: {
query: currentUserFrecentProjectsQuery,
- skip() {
- return !this.glFeatures.frecentNamespacesSuggestions;
- },
},
},
components: {
FrequentItems,
},
- mixins: [glFeatureFlagsMixin()],
inject: ['projectsPath'],
- data() {
- const username = gon.current_username;
-
- return {
- storageKey: username ? `${username}/frequent-projects` : null,
- };
- },
i18n: {
groupName: s__('Navigation|Frequently visited projects'),
viewAllText: s__('Navigation|View all my projects'),
emptyStateText: s__('Navigation|Projects you visit often will appear here.'),
},
- MAX_FREQUENT_PROJECTS_COUNT,
};
</script>
@@ -41,8 +27,6 @@ export default {
:loading="$apollo.queries.frecentProjects.loading"
:empty-state-text="$options.i18n.emptyStateText"
:group-name="$options.i18n.groupName"
- :max-items="$options.MAX_FREQUENT_PROJECTS_COUNT"
- :storage-key="storageKey"
:items="frecentProjects"
view-all-items-icon="project"
:view-all-items-text="$options.i18n.viewAllText"
diff --git a/app/assets/javascripts/super_sidebar/constants.js b/app/assets/javascripts/super_sidebar/constants.js
index e96dca3f365..f4da76f4a6e 100644
--- a/app/assets/javascripts/super_sidebar/constants.js
+++ b/app/assets/javascripts/super_sidebar/constants.js
@@ -25,9 +25,6 @@ export const helpCenterState = Vue.observable({
showTanukiBotChatDrawer: false,
});
-export const MAX_FREQUENT_PROJECTS_COUNT = 5;
-export const MAX_FREQUENT_GROUPS_COUNT = 3;
-
export const SUPER_SIDEBAR_PEEK_OPEN_DELAY = 200;
export const SUPER_SIDEBAR_PEEK_CLOSE_DELAY = 500;
export const SUPER_SIDEBAR_PEEK_STATE_CLOSED = 'closed';
diff --git a/app/assets/javascripts/super_sidebar/utils.js b/app/assets/javascripts/super_sidebar/utils.js
index 3d6eef62ad2..dbea306eced 100644
--- a/app/assets/javascripts/super_sidebar/utils.js
+++ b/app/assets/javascripts/super_sidebar/utils.js
@@ -26,24 +26,14 @@ const sortItemsByFrequencyAndLastAccess = (items) =>
return 0;
});
-// This imitates getTopFrequentItems from app/assets/javascripts/frequent_items/utils.js, but
-// adjusts the rules to accommodate for the context switcher's designs.
-export const getTopFrequentItems = (items, maxCount) => {
- if (!Array.isArray(items)) return [];
-
- const frequentItems = items.filter((item) => item.frequency >= FREQUENT_ITEMS.ELIGIBLE_FREQUENCY);
- sortItemsByFrequencyAndLastAccess(frequentItems);
-
- return frequentItems.slice(0, maxCount);
-};
-
/**
* This tracks projects' and groups' visits in order to suggest a list of frequently visited
- * entities to the user. Currently, this track visits in two ways:
- * - The legacy approach uses a simple counting algorithm and stores the data in the local storage.
- * - The above approach is being migrated to a backend-based one, where visits will be stored in the
- * DB, and suggestions will be made through a smarter algorithm. When we are ready to transition
- * to the newer approach, the legacy one will be cleaned up.
+ * entities to the user. The suggestion logic is implemented server-side and computed items can be
+ * retrieved through the GraphQL API.
+ * To persist a visit in the DB, an AJAX request needs to be triggered by the client. To avoid making
+ * the request on every visited page, we also keep track of the visits in the local storage so that
+ * the request is only sent once every 15 minutes per namespace per user.
+ *
* @param {object} item The project/group item being tracked.
* @param {string} namespace A string indicating whether the tracked entity is a project or a group.
* @param {string} trackVisitsPath The API endpoint to track visits server-side.
@@ -115,31 +105,4 @@ export const trackContextAccess = (username, context, trackVisitsPath) => {
return localStorage.setItem(storageKey, JSON.stringify(storedItems));
};
-export const getItemsFromLocalStorage = ({ storageKey, maxItems }) => {
- if (!AccessorUtilities.canUseLocalStorage()) {
- return [];
- }
-
- try {
- const parsedCachedFrequentItems = JSON.parse(localStorage.getItem(storageKey));
- return getTopFrequentItems(parsedCachedFrequentItems, maxItems);
- } catch (e) {
- Sentry.captureException(e);
- return [];
- }
-};
-
-export const removeItemFromLocalStorage = ({ storageKey, item }) => {
- try {
- const parsedCachedFrequentItems = JSON.parse(localStorage.getItem(storageKey));
- const filteredItems = parsedCachedFrequentItems.filter((i) => i.id !== item.id);
- localStorage.setItem(storageKey, JSON.stringify(filteredItems));
-
- return filteredItems;
- } catch (e) {
- Sentry.captureException(e);
- return [];
- }
-};
-
export const ariaCurrent = (isActive) => (isActive ? 'page' : null);
diff --git a/app/graphql/resolvers/ci/catalog/resources_resolver.rb b/app/graphql/resolvers/ci/catalog/resources_resolver.rb
index c6904dcd7f6..ec415cf25c1 100644
--- a/app/graphql/resolvers/ci/catalog/resources_resolver.rb
+++ b/app/graphql/resolvers/ci/catalog/resources_resolver.rb
@@ -27,17 +27,13 @@ module Resolvers
description: 'Project with the namespace catalog.'
def resolve_with_lookahead(scope:, project_path: nil, search: nil, sort: nil)
- if project_path.present?
- project = Project.find_by_full_path(project_path)
-
- apply_lookahead(
- ::Ci::Catalog::Listing
- .new(context[:current_user])
- .resources(namespace: project.root_namespace, sort: sort, search: search)
- )
- elsif scope == :all
- apply_lookahead(::Ci::Catalog::Listing.new(context[:current_user]).resources(sort: sort, search: search))
- end
+ project = Project.find_by_full_path(project_path)
+
+ apply_lookahead(
+ ::Ci::Catalog::Listing
+ .new(context[:current_user])
+ .resources(namespace: project&.root_namespace, sort: sort, search: search, scope: scope)
+ )
end
private
diff --git a/app/graphql/resolvers/users/frecent_groups_resolver.rb b/app/graphql/resolvers/users/frecent_groups_resolver.rb
index 2fc757e31ab..f6b43297898 100644
--- a/app/graphql/resolvers/users/frecent_groups_resolver.rb
+++ b/app/graphql/resolvers/users/frecent_groups_resolver.rb
@@ -10,12 +10,6 @@ module Resolvers
def resolve
return unless current_user.present?
- if Feature.disabled?(:frecent_namespaces_suggestions, current_user)
- raise_resource_not_available_error!("'frecent_namespaces_suggestions' feature flag is disabled")
- end
-
- return unless Feature.enabled?(:frecent_namespaces_suggestions, current_user)
-
::Users::GroupVisit.frecent_groups(user_id: current_user.id)
end
end
diff --git a/app/graphql/resolvers/users/frecent_projects_resolver.rb b/app/graphql/resolvers/users/frecent_projects_resolver.rb
index 397d4ca0cfd..9508195800a 100644
--- a/app/graphql/resolvers/users/frecent_projects_resolver.rb
+++ b/app/graphql/resolvers/users/frecent_projects_resolver.rb
@@ -10,10 +10,6 @@ module Resolvers
def resolve
return unless current_user.present?
- if Feature.disabled?(:frecent_namespaces_suggestions, current_user)
- raise_resource_not_available_error!("'frecent_namespaces_suggestions' feature flag is disabled")
- end
-
::Users::ProjectVisit.frecent_projects(user_id: current_user.id)
end
end
diff --git a/app/graphql/types/ci/catalog/resource_scope_enum.rb b/app/graphql/types/ci/catalog/resource_scope_enum.rb
index b825c3a7925..728670ba913 100644
--- a/app/graphql/types/ci/catalog/resource_scope_enum.rb
+++ b/app/graphql/types/ci/catalog/resource_scope_enum.rb
@@ -8,6 +8,7 @@ module Types
description 'Values for scoping catalog resources'
value 'ALL', 'All catalog resources visible to the current user.', value: :all
+ value 'NAMESPACES', 'Catalog resources belonging to authorized namespaces of the user.', value: :namespaces
end
end
end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 9acff9eb2b1..9c64e056f87 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -57,12 +57,10 @@ module Types
field :echo, resolver: Resolvers::EchoResolver
field :frecent_groups, [Types::GroupType],
resolver: Resolvers::Users::FrecentGroupsResolver,
- description: "A user's frecently visited groups. Requires the `frecent_namespaces_suggestions` feature flag to be enabled.",
- alpha: { milestone: '16.6' }
+ description: "A user's frecently visited groups"
field :frecent_projects, [Types::ProjectType],
resolver: Resolvers::Users::FrecentProjectsResolver,
- description: "A user's frecently visited projects. Requires the `frecent_namespaces_suggestions` feature flag to be enabled.",
- alpha: { milestone: '16.6' }
+ description: "A user's frecently visited projects"
field :gitpod_enabled, GraphQL::Types::Boolean,
null: true,
description: "Whether Gitpod is enabled in application settings."
diff --git a/app/helpers/ci/pipeline_editor_helper.rb b/app/helpers/ci/pipeline_editor_helper.rb
index 4d1bdf5fa7f..b40d633dc69 100644
--- a/app/helpers/ci/pipeline_editor_helper.rb
+++ b/app/helpers/ci/pipeline_editor_helper.rb
@@ -14,6 +14,7 @@ module Ci
total_branches = project.repository_exists? ? project.repository.branch_count : 0
{
+ "ci-catalog-path" => explore_catalog_index_path,
"ci-config-path": project.ci_config_path_or_default,
"ci-examples-help-page-path" => help_page_path('ci/examples/index'),
"ci-help-page-path" => help_page_path('ci/index'),
diff --git a/app/models/ci/catalog/listing.rb b/app/models/ci/catalog/listing.rb
index 14d043ad156..7a50a8ea94a 100644
--- a/app/models/ci/catalog/listing.rb
+++ b/app/models/ci/catalog/listing.rb
@@ -13,8 +13,9 @@ module Ci
@current_user = current_user
end
- def resources(namespace: nil, sort: nil, search: nil)
- relation = all_resources
+ def resources(namespace: nil, sort: nil, search: nil, scope: :all)
+ relation = Ci::Catalog::Resource.published.joins(:project).includes(:project)
+ relation = by_scope(relation, scope)
relation = by_namespace(relation, namespace)
relation = by_search(relation, search)
@@ -43,12 +44,6 @@ module Ci
attr_reader :current_user
- def all_resources
- Ci::Catalog::Resource.published
- .joins(:project).includes(:project)
- .merge(Project.public_or_visible_to_user(current_user))
- end
-
def by_namespace(relation, namespace)
return relation unless namespace
raise ArgumentError, 'Namespace is not a root namespace' unless namespace.root?
@@ -62,6 +57,14 @@ module Ci
relation.search(search)
end
+
+ def by_scope(relation, scope)
+ if scope == :namespaces && Feature.enabled?(:ci_guard_for_catalog_resource_scope, current_user)
+ relation.merge(Project.public_and_internal_only.visible_to_user(current_user))
+ else
+ relation.merge(Project.public_or_visible_to_user(current_user))
+ end
+ end
end
end
end