diff options
Diffstat (limited to 'app')
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 |