summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-15 12:07:19 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-15 12:07:19 +0000
commite6fed37d941271b897d37820fd3b571feab280b0 (patch)
treec81c9a7d38d6a8b0c1dc5f8ebb784fce68acb288
parent45a8c43afe8a17de19a92708b380b29b6ae04ce6 (diff)
downloadgitlab-ce-e6fed37d941271b897d37820fd3b571feab280b0.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--app/assets/javascripts/google_cloud/aiml/panel.vue63
-rw-r--r--app/assets/javascripts/google_cloud/components/google_cloud_menu.vue21
-rw-r--r--app/assets/javascripts/init_diff_stats_dropdown.js15
-rw-r--r--app/assets/javascripts/lib/utils/sticky.js60
-rw-r--r--app/assets/javascripts/merge_request_tabs.js17
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/compare/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/wikis/diff/index.js4
-rw-r--r--app/assets/javascripts/profile/components/profile_tabs.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue25
-rw-r--r--app/assets/stylesheets/framework/diffs.scss45
-rw-r--r--app/assets/stylesheets/framework/variables.scss6
-rw-r--r--app/assets/stylesheets/pages/projects.scss14
-rw-r--r--app/assets/stylesheets/test_environment.scss6
-rw-r--r--app/controllers/graphql_controller.rb8
-rw-r--r--app/views/admin/application_settings/network.html.haml3
-rw-r--r--app/views/projects/diffs/_diffs.html.haml6
-rw-r--r--app/views/shared/wikis/diff.html.haml2
-rw-r--r--config/feature_flags/development/optimize_scope_projects_with_feature_available.yml (renamed from config/feature_flags/development/rate_limit_for_unauthenticated_projects_api_access.yml)10
-rw-r--r--data/removals/16_0/16-0-jira-github-enterprise-dvcs-connector.yml10
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/architecture/blueprints/clickhouse_ingestion_pipeline/clickhouse_dbwriter.png (renamed from doc/architecture/blueprints/database/clickhouse/clickhouse_dbwriter.png)bin46544 -> 46544 bytes
-rw-r--r--doc/architecture/blueprints/clickhouse_ingestion_pipeline/index.md (renamed from doc/architecture/blueprints/database/clickhouse/ingestion_pipeline.md)2
-rw-r--r--doc/architecture/blueprints/clickhouse_read_abstraction_layer/index.md (renamed from doc/architecture/blueprints/database/clickhouse/read_abstraction_layer.md)19
-rw-r--r--doc/architecture/blueprints/gitlab_ci_events/index.md63
-rw-r--r--doc/architecture/blueprints/gitlab_ci_events/proposal-1-using-the-gitlab-ci-file.md60
-rw-r--r--doc/architecture/blueprints/gitlab_ci_events/proposal-2-using-the-rules-keyword.md38
-rw-r--r--doc/architecture/blueprints/gitlab_ci_events/proposal-3-using-the-gitlab-ci-events-folder.md64
-rw-r--r--doc/architecture/blueprints/gitlab_ci_events/proposal-4-creating-events-via-ci-files.md73
-rw-r--r--doc/development/ai_features.md40
-rw-r--r--doc/update/removals.md9
-rw-r--r--doc/user/admin_area/settings/rate_limit_on_projects_api.md1
-rw-r--r--doc/user/project/integrations/webhook_events.md1
-rw-r--r--lib/api/projects.rb2
-rw-r--r--locale/gitlab.pot9
-rw-r--r--spec/controllers/graphql_controller_spec.rb2
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js46
-rw-r--r--spec/frontend/google_cloud/aiml/panel_spec.js43
-rw-r--r--spec/frontend/google_cloud/components/google_cloud_menu_spec.js7
-rw-r--r--spec/frontend/lib/utils/sticky_spec.js77
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js55
-rw-r--r--spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js30
-rw-r--r--spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js33
-rw-r--r--spec/requests/api/projects_spec.rb24
-rw-r--r--spec/support/capybara.rb2
-rw-r--r--spec/support/helpers/keyset_pagination_helpers.rb7
-rw-r--r--spec/support_specs/helpers/keyset_pagination_helpers_spec.rb88
-rw-r--r--spec/views/admin/application_settings/network.html.haml_spec.rb12
51 files changed, 763 insertions, 381 deletions
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 28c34d2cd88..a3eb5a03fa6 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-14.19.0
+14.20.0
diff --git a/app/assets/javascripts/google_cloud/aiml/panel.vue b/app/assets/javascripts/google_cloud/aiml/panel.vue
new file mode 100644
index 00000000000..f591c47ac40
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/aiml/panel.vue
@@ -0,0 +1,63 @@
+<script>
+import GoogleCloudMenu from '../components/google_cloud_menu.vue';
+import IncubationBanner from '../components/incubation_banner.vue';
+import ServiceTable from './service_table.vue';
+
+export default {
+ components: {
+ IncubationBanner,
+ GoogleCloudMenu,
+ ServiceTable,
+ },
+ props: {
+ configurationUrl: {
+ type: String,
+ required: true,
+ },
+ deploymentsUrl: {
+ type: String,
+ required: true,
+ },
+ databasesUrl: {
+ type: String,
+ required: true,
+ },
+ aimlUrl: {
+ type: String,
+ required: true,
+ },
+ visionAiUrl: {
+ type: String,
+ required: true,
+ },
+ translationAiUrl: {
+ type: String,
+ required: true,
+ },
+ languageAiUrl: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <incubation-banner />
+
+ <google-cloud-menu
+ active="aiml"
+ :configuration-url="configurationUrl"
+ :deployments-url="deploymentsUrl"
+ :databases-url="databasesUrl"
+ :aiml-url="aimlUrl"
+ />
+
+ <service-table
+ :language-ai-url="languageAiUrl"
+ :translation-ai-url="translationAiUrl"
+ :vision-ai-url="visionAiUrl"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/google_cloud/components/google_cloud_menu.vue b/app/assets/javascripts/google_cloud/components/google_cloud_menu.vue
index d6b7c702b54..69b9c6133f1 100644
--- a/app/assets/javascripts/google_cloud/components/google_cloud_menu.vue
+++ b/app/assets/javascripts/google_cloud/components/google_cloud_menu.vue
@@ -4,11 +4,13 @@ import { s__ } from '~/locale';
const CONFIGURATION_KEY = 'configuration';
const DEPLOYMENTS_KEY = 'deployments';
const DATABASES_KEY = 'databases';
+const AIML_KEY = 'aiml';
const i18n = {
configuration: { title: s__('CloudSeed|Configuration') },
deployments: { title: s__('CloudSeed|Deployments') },
databases: { title: s__('CloudSeed|Databases') },
+ aiml: { title: s__('CloudSeed|AI / ML') },
};
export default {
@@ -29,6 +31,11 @@ export default {
type: String,
required: true,
},
+ aimlUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
isConfigurationActive() {
@@ -40,6 +47,9 @@ export default {
isDatabasesActive() {
return this.active === DATABASES_KEY;
},
+ isAimlActive() {
+ return this.active === AIML_KEY;
+ },
},
i18n,
};
@@ -80,6 +90,17 @@ export default {
{{ $options.i18n.databases.title }}
</a>
</li>
+ <li role="presentation" class="nav-item">
+ <a
+ data-testid="aimlLink"
+ role="tab"
+ :href="aimlUrl"
+ class="nav-link gl-tab-nav-item hidden"
+ :class="{ 'gl-tab-nav-item-active': isAimlActive }"
+ >
+ {{ $options.i18n.aiml.title }}
+ </a>
+ </li>
</ul>
</div>
</template>
diff --git a/app/assets/javascripts/init_diff_stats_dropdown.js b/app/assets/javascripts/init_diff_stats_dropdown.js
index 82350a3987e..d48741c794e 100644
--- a/app/assets/javascripts/init_diff_stats_dropdown.js
+++ b/app/assets/javascripts/init_diff_stats_dropdown.js
@@ -1,20 +1,7 @@
import Vue from 'vue';
import DiffStatsDropdown from '~/vue_shared/components/diff_stats_dropdown.vue';
-import { stickyMonitor } from './lib/utils/sticky';
-
-export const initDiffStatsDropdown = (stickyTop) => {
- if (stickyTop) {
- // We spend quite a bit of effort in our CSS to set the correct padding-top on the
- // layout page, so we re-use the padding set there to determine at what height our
- // element should be sticky
- const pageLayout = document.querySelector('.layout-page');
- const pageLayoutTopOffset = pageLayout
- ? parseFloat(window.getComputedStyle(pageLayout).getPropertyValue('padding-top') || 0)
- : 0;
-
- stickyMonitor(document.querySelector('.js-diff-files-changed'), pageLayoutTopOffset, false);
- }
+export const initDiffStatsDropdown = () => {
const el = document.querySelector('.js-diff-stats-dropdown');
if (!el) {
diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js
deleted file mode 100644
index a6d53358cb8..00000000000
--- a/app/assets/javascripts/lib/utils/sticky.js
+++ /dev/null
@@ -1,60 +0,0 @@
-export const createPlaceholder = () => {
- const placeholder = document.createElement('div');
- placeholder.classList.add('sticky-placeholder');
-
- return placeholder;
-};
-
-export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => {
- const top = Math.floor(el.offsetTop - scrollY);
-
- if (top <= stickyTop && !el.classList.contains('is-stuck')) {
- const placeholder = insertPlaceholder ? createPlaceholder() : null;
- const heightBefore = el.offsetHeight;
-
- el.classList.add('is-stuck');
-
- if (insertPlaceholder) {
- el.parentNode.insertBefore(placeholder, el.nextElementSibling);
-
- placeholder.style.height = `${heightBefore - el.offsetHeight}px`;
- }
- } else if (top > stickyTop && el.classList.contains('is-stuck')) {
- el.classList.remove('is-stuck');
-
- if (
- insertPlaceholder &&
- el.nextElementSibling &&
- el.nextElementSibling.classList.contains('sticky-placeholder')
- ) {
- el.nextElementSibling.remove();
- }
- }
-};
-
-/**
- * Create a listener that will toggle a 'is-stuck' class, based on the current scroll position.
- *
- * - If the current environment does not support `position: sticky`, do nothing.
- *
- * @param {HTMLElement} el The `position: sticky` element.
- * @param {Number} stickyTop Used to determine when an element is stuck.
- * @param {Boolean} insertPlaceholder Should a placeholder element be created when element is stuck?
- */
-export const stickyMonitor = (el, stickyTop, insertPlaceholder = true) => {
- if (!el) return;
-
- if (
- typeof CSS === 'undefined' ||
- !CSS.supports('(position: -webkit-sticky) or (position: sticky)')
- )
- return;
-
- document.addEventListener(
- 'scroll',
- () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder),
- {
- passive: true,
- },
- );
-};
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index eb25879205f..cef224d83e2 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -83,18 +83,6 @@ function scrollToContainer(container) {
}
}
-function computeTopOffset(tabs) {
- const navbar = document.querySelector('.navbar-gitlab');
- const peek = document.getElementById('js-peek');
- let stickyTop;
-
- stickyTop = navbar ? navbar.offsetHeight : 0;
- stickyTop = peek ? stickyTop + peek.offsetHeight : stickyTop;
- stickyTop = tabs ? stickyTop + tabs.offsetHeight : stickyTop;
-
- return stickyTop;
-}
-
function mountPipelines() {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
const { mrWidgetData } = gl;
@@ -145,11 +133,11 @@ function destroyPipelines(app) {
return null;
}
-function loadDiffs({ url, sticky, tabs }) {
+function loadDiffs({ url, tabs }) {
return axios.get(url).then(({ data }) => {
const $container = $('#diffs');
$container.html(data.html);
- initDiffStatsDropdown(sticky);
+ initDiffStatsDropdown();
localTimeAgo(document.querySelectorAll('#diffs .js-timeago'));
syntaxHighlight($('#diffs .js-syntax-highlight'));
@@ -537,7 +525,6 @@ export default class MergeRequestTabs {
loadDiffs({
url: diffUrl,
- sticky: computeTopOffset(this.mergeRequestTabs),
tabs: this,
})
.then(() => {
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index 9a47a720709..c9f5895c7a3 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -18,7 +18,7 @@ import '~/sourcegraph/load';
import DiffStats from '~/diffs/components/diff_stats.vue';
import { initReportAbuse } from '~/projects/report_abuse';
-initDiffStatsDropdown(true);
+initDiffStatsDropdown();
new ZenMode();
new ShortcutsNavigation();
diff --git a/app/assets/javascripts/pages/projects/compare/show/index.js b/app/assets/javascripts/pages/projects/compare/show/index.js
index 5bcdd34e258..328b5596e1a 100644
--- a/app/assets/javascripts/pages/projects/compare/show/index.js
+++ b/app/assets/javascripts/pages/projects/compare/show/index.js
@@ -7,7 +7,7 @@ import syntaxHighlight from '~/syntax_highlight';
initCompareSelector();
new Diff(); // eslint-disable-line no-new
-initDiffStatsDropdown(true);
+initDiffStatsDropdown();
GpgBadges.fetch();
syntaxHighlight([document.querySelector('.files')]);
diff --git a/app/assets/javascripts/pages/projects/wikis/diff/index.js b/app/assets/javascripts/pages/projects/wikis/diff/index.js
index 73440db761f..067ffb3dca9 100644
--- a/app/assets/javascripts/pages/projects/wikis/diff/index.js
+++ b/app/assets/javascripts/pages/projects/wikis/diff/index.js
@@ -1,3 +1,7 @@
+import syntaxHighlight from '~/syntax_highlight';
import { initDiffStatsDropdown } from '~/init_diff_stats_dropdown';
+import Diff from '~/diff';
+new Diff(); // eslint-disable-line no-new
initDiffStatsDropdown();
+syntaxHighlight([document.querySelector('.files')]);
diff --git a/app/assets/javascripts/profile/components/profile_tabs.vue b/app/assets/javascripts/profile/components/profile_tabs.vue
index 25b94d7dc7f..8e52a98803d 100644
--- a/app/assets/javascripts/profile/components/profile_tabs.vue
+++ b/app/assets/javascripts/profile/components/profile_tabs.vue
@@ -96,7 +96,7 @@ export default {
:is="component"
v-for="{ key, component } in $options.tabs"
:key="key"
- class="container-fluid container-limited"
+ class="container-fluid container-limited gl-text-left"
:personal-projects="personalProjects"
:personal-projects-loading="personalProjectsLoading"
/>
diff --git a/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue b/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue
index 56e6399a1b7..f62bfb551df 100644
--- a/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue
@@ -147,13 +147,5 @@ export default {
</template>
</gl-sprintf>
</span>
-
- <div
- class="diff-stats-additions-deletions-collapsed gl-float-right gl-display-none"
- data-testid="diff-stats-additions-deletions-collapsed"
- >
- <span class="gl-text-green-600 gl-font-weight-bold">+{{ added }}</span>
- <span class="gl-text-red-500 gl-font-weight-bold">-{{ deleted }}</span>
- </div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue
index 64ce4b66213..11aa7b91745 100644
--- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue
+++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue
@@ -21,7 +21,10 @@ export default {
* openIssuesCount: number;
* permissions: {
* projectAccess: { accessLevel: 50 };
- * }[];
+ * };
+ * descriptionHtml: string;
+ * updatedAt: string;
+ * }[]
*/
projects: {
type: Array,
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
index 714ffd60c25..266cce29e50 100644
--- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
@@ -17,6 +17,8 @@ import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.
import { __ } from '~/locale';
import { numberToMetricPrefix } from '~/lib/utils/number_utils';
import { truncate } from '~/lib/utils/text_utility';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
const MAX_TOPICS_TO_SHOW = 3;
const MAX_TOPIC_TITLE_LENGTH = 15;
@@ -30,6 +32,10 @@ export default {
topics: __('Topics'),
topicsPopoverTargetText: __('+ %{count} more'),
moreTopics: __('More topics'),
+ updated: __('Updated'),
+ },
+ safeHtmlConfig: {
+ ADD_TAGS: ['gl-emoji'],
},
components: {
GlAvatarLabeled,
@@ -39,9 +45,11 @@ export default {
GlBadge,
GlPopover,
GlSprintf,
+ TimeAgoTooltip,
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml,
},
props: {
/**
@@ -62,6 +70,9 @@ export default {
* permissions: {
* projectAccess: { accessLevel: 50 };
* };
+ * descriptionHtml: string;
+ * updatedAt: string;
+ * }
*/
project: {
type: Object,
@@ -138,7 +149,7 @@ export default {
</script>
<template>
- <li class="gl-py-5 gl-md-display-flex gl-align-items-center gl-border-b">
+ <li class="projects-list-item gl-py-5 gl-md-display-flex gl-align-items-center gl-border-b">
<gl-avatar-labeled
class="gl-flex-grow-1"
:entity-id="project.id"
@@ -158,6 +169,12 @@ export default {
accessLevelLabel
}}</user-access-role-badge>
</template>
+ <div
+ v-if="project.descriptionHtml"
+ v-safe-html:[$options.safeHtmlConfig]="project.descriptionHtml"
+ class="gl-font-sm gl-overflow-hidden gl-line-height-20 description"
+ data-testid="project-description"
+ ></div>
<div v-if="hasTopics" class="gl-mt-3" data-testid="project-topics">
<div
class="gl-w-full gl-display-inline-flex gl-flex-wrap gl-font-base gl-font-weight-normal gl-align-items-center gl-mx-n2 gl-my-n2"
@@ -197,7 +214,7 @@ export default {
</div>
</gl-avatar-labeled>
<div
- class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-md-mt-0"
+ class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-pl-10 gl-md-pl-0 gl-md-mt-0"
>
<div class="gl-display-flex gl-align-items-center gl-gap-x-3">
<gl-badge v-if="project.archived" variant="warning">{{ $options.i18n.archived }}</gl-badge>
@@ -231,6 +248,10 @@ export default {
<span>{{ numberToMetricPrefix(project.openIssuesCount) }}</span>
</gl-link>
</div>
+ <div class="gl-font-sm gl-white-space-nowrap gl-text-secondary gl-mt-3">
+ <span>{{ $options.i18n.updated }}</span>
+ <time-ago-tooltip :time="project.updatedAt" />
+ </div>
</div>
</li>
</template>
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index 54a4769f66d..3885863999b 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -51,12 +51,11 @@
pointer-events: none;
}
- &.is-commit {
- top: calc(#{$calc-application-header-height} + #{$commit-stat-summary-height});
- }
-
- &.is-compare {
- top: calc(#{$calc-application-header-height} + #{$compare-branches-sticky-header-height});
+ &.is-commit,
+ &.is-compare,
+ &.is-wiki {
+ top: calc(#{$calc-application-header-height});
+ border-top: 0;
}
}
@@ -682,40 +681,6 @@ table.code {
}
}
-.diff-files-changed {
- background-color: $body-bg;
-
- .inline-parallel-buttons {
- @include gl-relative;
- z-index: 1;
- }
-
- @include media-breakpoint-up(sm) {
- @include gl-sticky;
- top: $calc-application-header-height;
- z-index: 200;
-
- &.is-stuck {
- @include gl-py-0;
- border-top: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
-
- .diff-stats-additions-deletions-expanded,
- .inline-parallel-buttons {
- @include gl-display-none;
- }
- }
- }
-
- @include media-breakpoint-up(lg) {
- &.is-stuck {
- .diff-stats-additions-deletions-collapsed {
- @include gl-display-block;
- }
- }
- }
-}
-
.note-container {
background-color: $gray-light;
border-top: 1px solid $white-normal;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index dc6a5c5479c..3dd6a435a75 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -739,7 +739,6 @@ $calendar-activity-colors: (
*/
$commit-max-width-marker-color: rgba(0, 0, 0, 0);
$commit-message-text-area-bg: rgba(0, 0, 0, 0);
-$commit-stat-summary-height: 32px;
/*
* Files
@@ -916,11 +915,6 @@ Merge requests
$mr-tabs-height: 48px;
/*
-Compare Branches
-*/
-$compare-branches-sticky-header-height: 32px;
-
-/*
Board Swimlanes
*/
$board-swimlanes-headers-height: 64px;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index e6c7e265cdb..ff1987f35b3 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -654,3 +654,17 @@
}
}
}
+
+.projects-list-item {
+ .description {
+ max-height: $gl-spacing-scale-8;
+
+ p {
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ text-overflow: ellipsis;
+ /* stylelint-disable-next-line value-no-vendor-prefix */
+ display: -webkit-box;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/test_environment.scss b/app/assets/stylesheets/test_environment.scss
index e9ba41f9bb7..38fe2795ff4 100644
--- a/app/assets/stylesheets/test_environment.scss
+++ b/app/assets/stylesheets/test_environment.scss
@@ -1,9 +1,3 @@
-// Disable sticky changes bar for tests
-.diff-files-changed {
- position: relative !important;
- top: 0 !important;
-}
-
// Un-hide inputs for @gitlab/ui custom checkboxes and radios so Capybara can target them
.custom-control-input {
z-index: 500;
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index 8630519e028..ff4fce9ad1e 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -109,7 +109,7 @@ class GraphqlController < ApplicationController
private
- def permitted_params
+ def permitted_multiplex_params
params.permit(_json: [:query, :operationName, { variables: {} }])
end
@@ -190,7 +190,7 @@ class GraphqlController < ApplicationController
end
def multiplex_param
- permitted_params[:_json]
+ permitted_multiplex_params[:_json]
end
def multiplex_queries
@@ -221,8 +221,10 @@ class GraphqlController < ApplicationController
Gitlab::Graphql::Variables.new(variable_info).to_h
end
+ # We support Apollo-style query batching where an array of queries will be in the `_json:` key.
+ # https://graphql-ruby.org/queries/multiplex.html#apollo-query-batching
def multiplex?
- multiplex_param.present?
+ params[:_json].is_a?(Array)
end
def authorize_access_api!
diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml
index 1809496bb9f..18ce7c1ceba 100644
--- a/app/views/admin/application_settings/network.html.haml
+++ b/app/views/admin/application_settings/network.html.haml
@@ -146,8 +146,7 @@
.settings-content
= render 'users_api_limits'
-- if Feature.enabled?(:rate_limit_for_unauthenticated_projects_api_access)
- = render 'projects_api_limits'
+= render 'projects_api_limits'
%section.settings.as-import-export-limits.no-animate#js-import-export-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 88354f57c55..982ecbbae51 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -8,16 +8,14 @@
- page = local_assigns.fetch(:page, nil)
- diff_files = conditionally_paginate_diff_files(diffs, paginate: paginate_diffs, page: page, per: paginate_diffs_per_page)
-.files-changed.diff-files-changed.js-diff-files-changed.gl-py-3
+.js-diff-files-changed.gl-py-3
.files-changed-inner
- .inline-parallel-buttons.gl-display-none.gl-md-display-flex
+ .inline-parallel-buttons.gl-display-none.gl-md-display-flex.gl-relative
- if !diffs_expanded? && diff_files.any?(&:collapsed?)
= link_to _('Expand all'), url_for(safe_params.merge(expanded: 1, format: nil)), class: 'gl-button btn btn-default'
- if show_whitespace_toggle
- if current_controller?(:commit)
= commit_diff_whitespace_link(diffs.project, @commit, class: 'd-none d-sm-inline-block')
- - elsif current_controller?('projects/merge_requests/diffs')
- = diff_merge_request_whitespace_link(diffs.project, @merge_request, class: 'd-none d-sm-inline-block')
- elsif current_controller?(:compare)
= diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'd-none d-sm-inline-block')
- elsif current_controller?(:wikis)
diff --git a/app/views/shared/wikis/diff.html.haml b/app/views/shared/wikis/diff.html.haml
index c39739ac422..ee6c7f307a7 100644
--- a/app/views/shared/wikis/diff.html.haml
+++ b/app/views/shared/wikis/diff.html.haml
@@ -28,5 +28,5 @@
%pre.commit-description<
= preserve(markdown_field(commit, :description))
-= render 'projects/diffs/diffs', diffs: @diffs
+= render 'projects/diffs/diffs', diffs: @diffs, diff_page_context: "is-wiki"
= render 'shared/wikis/sidebar'
diff --git a/config/feature_flags/development/rate_limit_for_unauthenticated_projects_api_access.yml b/config/feature_flags/development/optimize_scope_projects_with_feature_available.yml
index ba40608356c..86811a6c534 100644
--- a/config/feature_flags/development/rate_limit_for_unauthenticated_projects_api_access.yml
+++ b/config/feature_flags/development/optimize_scope_projects_with_feature_available.yml
@@ -1,8 +1,8 @@
---
-name: rate_limit_for_unauthenticated_projects_api_access
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112283
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/391922
-milestone: '15.10'
+name: optimize_scope_projects_with_feature_available
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119950/
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/410693
+milestone: '16.0'
type: development
group: group::tenant scale
-default_enabled: true
+default_enabled: false
diff --git a/data/removals/16_0/16-0-jira-github-enterprise-dvcs-connector.yml b/data/removals/16_0/16-0-jira-github-enterprise-dvcs-connector.yml
new file mode 100644
index 00000000000..7538bf77da6
--- /dev/null
+++ b/data/removals/16_0/16-0-jira-github-enterprise-dvcs-connector.yml
@@ -0,0 +1,10 @@
+- title: "Jira DVCS connector for Jira Cloud and Jira 8.13 and earlier"
+ announcement_milestone: "15.1"
+ removal_milestone: "16.0"
+ breaking_change: true
+ reporter: m_frankiewicz
+ stage: Manage
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/362168
+ body: |
+ The [Jira DVCS connector](https://docs.gitlab.com/ee/integration/jira/dvcs/) for Jira Cloud was deprecated in GitLab 15.1 and has been removed in 16.0. Use the [GitLab for Jira Cloud app](https://docs.gitlab.com/ee/integration/jira/connect-app.html) instead. The Jira DVCS connector was also deprecated for Jira 8.13 and earlier. You can only use the Jira DVCS connector with Jira Data Center or Jira Server in Jira 8.14 and later. Upgrade your Jira instance to Jira 8.14 or later, and reconfigure the Jira integration in your GitLab instance.
+ If you cannot upgrade your Jira instance in time and are on GitLab self-managed version, we offer a workaround until GitLab 16.6. This breaking change is deployed in GitLab 16.0 behind a feature flag named `jira_dvcs_end_of_life_amnesty`. The flag is disabled by default, but you can ask an administrator to enable the flag at any time. For questions related to this announcement, see the [feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/408185).
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index e3abed66d77..47e52e6005f 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -18993,6 +18993,7 @@ Represents a product analytics dashboard.
| ---- | ---- | ----------- |
| <a id="productanalyticsdashboarddescription"></a>`description` | [`String`](#string) | Description of the dashboard. |
| <a id="productanalyticsdashboardpanels"></a>`panels` | [`ProductAnalyticsDashboardPanelConnection!`](#productanalyticsdashboardpanelconnection) | Panels shown on the dashboard. (see [Connections](#connections)) |
+| <a id="productanalyticsdashboardslug"></a>`slug` | [`String!`](#string) | Slug of the dashboard. |
| <a id="productanalyticsdashboardtitle"></a>`title` | [`String!`](#string) | Title of the dashboard. |
### `ProductAnalyticsDashboardPanel`
diff --git a/doc/architecture/blueprints/database/clickhouse/clickhouse_dbwriter.png b/doc/architecture/blueprints/clickhouse_ingestion_pipeline/clickhouse_dbwriter.png
index fc65830d3ee..fc65830d3ee 100644
--- a/doc/architecture/blueprints/database/clickhouse/clickhouse_dbwriter.png
+++ b/doc/architecture/blueprints/clickhouse_ingestion_pipeline/clickhouse_dbwriter.png
Binary files differ
diff --git a/doc/architecture/blueprints/database/clickhouse/ingestion_pipeline.md b/doc/architecture/blueprints/clickhouse_ingestion_pipeline/index.md
index 0fafc1bf524..94714e7b245 100644
--- a/doc/architecture/blueprints/database/clickhouse/ingestion_pipeline.md
+++ b/doc/architecture/blueprints/clickhouse_ingestion_pipeline/index.md
@@ -147,7 +147,7 @@ Having addressed the details of the two aformentioned problem-domains, we can mo
The single, biggest challenge around introducing ClickHouse and related systems would be the ability to make it avaiable to our users running GitLab in self-managed environments. The intended goals of this proposal are intentionally kept within those constraints. It is also prudent to establish that what we're *proposing* here be applicable to applications consuming ClickHouse from inside self-managed environments.
-There are ongoing efforts to streamline distribution and deployment of ClickHouse instances for managed environment within the larger scope of [ClickHouse Usage at GitLab](../../clickhouse_usage/index.md). A few other issues tackling parts of the aforementioned problem are:
+There are ongoing efforts to streamline distribution and deployment of ClickHouse instances for managed environment within the larger scope of [ClickHouse Usage at GitLab](../clickhouse_usage/index.md). A few other issues tackling parts of the aforementioned problem are:
- [Research and understand component costs and maintenance requirements of running a ClickHouse instance with GitLab](https://gitlab.com/gitlab-com/www-gitlab-com/-/issues/14384)
- [ClickHouse maintenance and cost research](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116669)
diff --git a/doc/architecture/blueprints/database/clickhouse/read_abstraction_layer.md b/doc/architecture/blueprints/clickhouse_read_abstraction_layer/index.md
index 82470867a30..8290641b7a4 100644
--- a/doc/architecture/blueprints/database/clickhouse/read_abstraction_layer.md
+++ b/doc/architecture/blueprints/clickhouse_read_abstraction_layer/index.md
@@ -10,6 +10,17 @@ participating-stages: []
# Consider an abstraction layer to interact with ClickHouse or alternatives
+## Table of Contents
+
+- [Summary](#summary)
+- [Motivation](#motivation)
+- [Goals](#goals)
+- [Non-goals](#non-goals)
+- [Possible solutions](#possible-solutions)
+ - [Recommended approach](#recommended-approach)
+ - [Overview of open source tools](#overview-of-open-source-tools)
+- [Open Questions](#open-questions)
+
## Summary
Provide a solution standardizing read access to ClickHouse or its alternatives for GitLab installations that will not opt-in to install ClickHouse. After analyzing different [open-source tools](#overview-of-open-source-tools) and weighing them against an option to [build a solution internally](#recommended-approach). The current recommended approach proposes to use dedicated database-level drivers to connect to each data source. Additionally, it proposes the usage of [repository pattern](https://martinfowler.com/eaaCatalog/repository.html) to confine optionally database availability complexity to a single application layer.
@@ -24,7 +35,7 @@ offering a unified interface for interactions with underlying data stores, to a
## Goals
-- Limit the impact of optionally available data stores on the overall GitLab application codebase to [single abstraction layer](../../../../development/reusing_abstractions.md#abstractions)
+- Limit the impact of optionally available data stores on the overall GitLab application codebase to [single abstraction layer](../../../development/reusing_abstractions.md#abstractions)
- Support all data store specific features
- Support communication for satellite services of the main GitLab application
@@ -72,7 +83,7 @@ Following ClickHouse documentation there are the following drivers for Ruby and
To keep the codebase well organized and limit coupling to any specific database engine it is important to encapsulate
interactions, including querying data to a single application layer, that would present its interface to layers above in
-similar vain to [ActiveRecord interface propagation through abstraction layers](../../../../development/reusing_abstractions.md)
+similar vain to [ActiveRecord interface propagation through abstraction layers](../../../development/reusing_abstractions.md)
Keeping underlying database engines encapsulated makes the recommended solution a good two-way door decision that
keeps the opportunity to introduce other tools later on, while giving groups time to explore and understand their use cases.
@@ -81,7 +92,7 @@ At the lowest abstraction layer, it can be expected that there will be a family
following MVC pattern implemented by Rails should be classified as _Models_.
Models-level abstraction builds well into existing patterns and guidelines but unfortunately does not solve the challenge of the optional availability of the ClickHouse database engine for self-managed instances. It is required to design a dedicated entity that will house responsibility of selecting best database to serve business logic request.
-From the already mentioned existing abstraction [guidelines](../../../../development/reusing_abstractions.md) `Finders` seems to be the closest to the given requirements, due to the fact that `Finders` encapsulate database specific interaction behind their own public API, hiding database vendors detail from all layers above them.
+From the already mentioned existing abstraction [guidelines](../../../development/reusing_abstractions.md) `Finders` seems to be the closest to the given requirements, due to the fact that `Finders` encapsulate database specific interaction behind their own public API, hiding database vendors detail from all layers above them.
However, they are closely coupled to `ActiveRecord` ORM framework, and are bound by existing GitLab convention to return `ActiveRecord::Relation` objects, that might be used to compose even more complex queries. That coupling makes `Finders` unfit to deal with the optional availability of ClickHouse because returned data might come from two different databases, and might not be compatible with each other.
@@ -138,7 +149,7 @@ In this section authors provide an overview of existing 3rd party open-source so
1. It focuses on the fact whether the proposed abstraction layer can support both ClickHouse and PostgreSQL (must have)
1. Additional consideration might be if more than the two must-have storages are supported
-1. The solution must support the [minimum required versions](../../../../install/requirements.md#postgresql-requirements) for PostgreSQL
+1. The solution must support the [minimum required versions](../../../install/requirements.md#postgresql-requirements) for PostgreSQL
##### 3. Protocol compatibility
diff --git a/doc/architecture/blueprints/gitlab_ci_events/index.md b/doc/architecture/blueprints/gitlab_ci_events/index.md
new file mode 100644
index 00000000000..7ce8fea9410
--- /dev/null
+++ b/doc/architecture/blueprints/gitlab_ci_events/index.md
@@ -0,0 +1,63 @@
+---
+status: proposed
+creation-date: "2023-03-15"
+authors: [ "@furkanayhan" ]
+coach: "@grzesiek"
+approvers: [ "@jreporter", "@cheryl.li" ]
+owning-stage: "~devops::verify"
+participating-stages: [ "~devops::package", "~devops::deploy" ]
+---
+
+# GitLab CI Events
+
+## Summary
+
+In order to unlock innovation and build more value, GitLab is expected to be
+the center of automation related to DevSecOps processes. We want to transform
+GitLab into a programming environment, that will make it possible for engineers
+to model various workflows on top of CI/CD pipelines. Today, users must create
+custom automation around webhooks or scheduled pipelines to build required
+workflows.
+
+In order to make this automation easier for our users, we want to build a
+powerful CI/CD eventing system, that will make it possible to run pipelines
+whenever something happens inside or outside of GitLab.
+
+A typical use-case is to run a CI/CD job whenever someone creates an issue,
+posts a comment, changes a merge request status from "draft" to "ready for
+review" or adds a new member to a group.
+
+To build that new technology, we should:
+
+1. Emit many hierarchical events from within GitLab in a more advanced way than we do it today.
+1. Make it affordable to run this automation, that will react to GitLab events, at scale.
+1. Provide a set of conventions and libraries to make writing the automation easier.
+
+## Goals
+
+While ["GitLab Events Platform"](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113700)
+aims to build new abstractions around emitting events in GitLab, "GitLab CI
+Events" blueprint is about making it possible to:
+
+1. Define a way in which users will configure when an event emitted will result in a CI pipeline being run.
+1. Describe technology required to match subscriptions with events at GitLab.com scale and beyond.
+1. Describe technology we could use to reduce the cost of running automation jobs significantly.
+
+## Proposals
+
+For now, we have technical 4 proposals;
+
+1. [Proposal 1: Using the `.gitlab-ci.yml` file](proposal-1-using-the-gitlab-ci-file.md)
+ Based on;
+ - [GitLab CI Workflows PoC](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91244)
+ - [PoC NPM CI events](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111693)
+1. [Proposal 2: Using the `rules` keyword](proposal-2-using-the-rules-keyword.md)
+ Highly inefficient way.
+1. [Proposal 3: Using the `.gitlab/ci/events` folder](proposal-3-using-the-gitlab-ci-events-folder.md)
+ Involves file reading for every event.
+1. [Proposal 4: Creating events via CI files](proposal-4-creating-events-via-ci-files.md)
+ Combination of some proposals.
+
+Each of them has its pros and cons. There could be many more proposals and we
+would like to discuss them all. We can combine the best part of those proposals
+and create a new one.
diff --git a/doc/architecture/blueprints/gitlab_ci_events/proposal-1-using-the-gitlab-ci-file.md b/doc/architecture/blueprints/gitlab_ci_events/proposal-1-using-the-gitlab-ci-file.md
new file mode 100644
index 00000000000..7dfc3873ada
--- /dev/null
+++ b/doc/architecture/blueprints/gitlab_ci_events/proposal-1-using-the-gitlab-ci-file.md
@@ -0,0 +1,60 @@
+---
+owning-stage: "~devops::verify"
+description: 'GitLab CI Events Proposal 1: Using the .gitlab-ci.yml file'
+---
+
+# GitLab CI Events Proposal 1: Using the `.gitlab-ci.yml` file
+
+Currently, we have two proof-of-concept (POC) implementations:
+
+- [GitLab CI Workflows PoC](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91244)
+- [PoC NPM CI events](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111693)
+
+They both have similar ideas;
+
+1. Find a new CI Config syntax to define the pipeline events.
+
+ Example 1:
+
+ ```yaml
+ workflow:
+ events:
+ - events/package/published
+
+ # or
+
+ workflow:
+ on:
+ - events/package/published
+ ```
+
+ Example 2:
+
+ ```yaml
+ spec:
+ on:
+ - events/package/published
+ - events/package/removed
+ # on:
+ # package: [published, removed]
+ ---
+ do_something:
+ script: echo "Hello World"
+ ```
+
+1. Upsert an event to the database when creating a pipeline.
+1. Create [EventStore subscriptions](../../../development/event_store.md) to handle the events.
+
+## Problems & Questions
+
+1. The CI config of a project can be anything;
+ - `.gitlab-ci.yml` by default
+ - another file in the project
+ - another file in another project
+ - completely a remote/external file
+
+ How do we handle these cases?
+1. Since we have these problems above, should we keep the events in its own file? (`.gitlab-ci-events.yml`)
+1. Do we only accept the changes in the main branch?
+1. We try to create event subscriptions every time a pipeline is created.
+1. Can we move the existing workflows into the new CI events, for example, `merge_request_event`?
diff --git a/doc/architecture/blueprints/gitlab_ci_events/proposal-2-using-the-rules-keyword.md b/doc/architecture/blueprints/gitlab_ci_events/proposal-2-using-the-rules-keyword.md
new file mode 100644
index 00000000000..6f69a0f11f0
--- /dev/null
+++ b/doc/architecture/blueprints/gitlab_ci_events/proposal-2-using-the-rules-keyword.md
@@ -0,0 +1,38 @@
+---
+owning-stage: "~devops::verify"
+description: 'GitLab CI Events Proposal 2: Using the rules keyword'
+---
+
+# GitLab CI Events Proposal 2: Using the `rules` keyword
+
+Can we do it with our current [`rules`](../../../ci/yaml/index.md#rules) system?
+
+```yaml
+workflow:
+ rules:
+ - events: ["package/*"]
+
+test_package_published:
+ script: echo testing published package
+ rules:
+ - events: ["package/published"]
+
+test_package_removed:
+ script: echo testing removed package
+ rules:
+ - events: ["package/removed"]
+```
+
+1. We don't upsert anything to the database.
+1. We'll have a single worker which subcribes to events
+like `store.subscribe ::Ci::CreatePipelineFromEventWorker, to: ::Issues::CreatedEvent`.
+1. The worker just runs `Ci::CreatePipelineService` with the correct parameters, the rest
+will be handled by the `rules` system. Of course, we'll need modifications to the `rules` system to support `events`.
+
+## Problems & Questions
+
+1. For every defined event run, we need to enqueue a new `Ci::CreatePipelineFromEventWorker` job.
+1. The worker will need to run `Ci::CreatePipelineService` for every event run.
+This may be costly because we go through every cycle of `Ci::CreatePipelineService`.
+1. This would be highly inefficient.
+1. Can we move the existing workflows into the new CI events, for example, `merge_request_event`?
diff --git a/doc/architecture/blueprints/gitlab_ci_events/proposal-3-using-the-gitlab-ci-events-folder.md b/doc/architecture/blueprints/gitlab_ci_events/proposal-3-using-the-gitlab-ci-events-folder.md
new file mode 100644
index 00000000000..ad76b7f8dd4
--- /dev/null
+++ b/doc/architecture/blueprints/gitlab_ci_events/proposal-3-using-the-gitlab-ci-events-folder.md
@@ -0,0 +1,64 @@
+---
+owning-stage: "~devops::verify"
+description: 'GitLab CI Events Proposal 3: Using the .gitlab/ci/events folder'
+---
+
+# GitLab CI Events Proposal 3: Using the `.gitlab/ci/events` folder
+
+We can also approach this problem by creating separate files for events.
+
+Let's say we'll have the `.gitlab/ci/events` folder (or `.gitlab/workflows/ci`).
+
+We can define events in the following format:
+
+```yaml
+# .gitlab/ci/events/package-published.yml
+
+spec:
+ events:
+ - name: package/published
+
+---
+
+include:
+ - local: .gitlab-ci.yml
+ with:
+ event: $[[ gitlab.event.name ]]
+```
+
+And in the `.gitlab-ci.yml` file, we can use the input;
+
+```yaml
+# .gitlab-ci.yml
+
+spec:
+ inputs:
+ event:
+ default: push
+
+---
+
+job1:
+ script: echo "Hello World"
+
+job2:
+ script: echo "Hello World"
+
+job-for-package-published:
+ script: echo "Hello World"
+ rules:
+ - if: $[[ inputs.event ]] == "package/published"
+```
+
+When an event happens;
+
+1. We'll enqueue a new job for the event.
+1. The job will search for the event file in the `.gitlab/ci/events` folder.
+1. The job will run `Ci::CreatePipelineService` for the event file.
+
+## Problems & Questions
+
+1. For every defined event run, we need to enqueue a new job.
+1. Every event-job will need to search for files.
+1. This would be only for the project-scope events.
+1. This can be inefficient because of searching for files for the project for every event.
diff --git a/doc/architecture/blueprints/gitlab_ci_events/proposal-4-creating-events-via-ci-files.md b/doc/architecture/blueprints/gitlab_ci_events/proposal-4-creating-events-via-ci-files.md
new file mode 100644
index 00000000000..5f10ba1fbb2
--- /dev/null
+++ b/doc/architecture/blueprints/gitlab_ci_events/proposal-4-creating-events-via-ci-files.md
@@ -0,0 +1,73 @@
+---
+owning-stage: "~devops::verify"
+description: 'GitLab CI Events Proposal 4: Creating events via CI files'
+---
+
+# GitLab CI Events Proposal 4: Creating events via CI files
+
+Each project can have its own event configuration file. Let's call it `.gitlab-ci-event.yml` for now.
+In this file, we can define events in the following format:
+
+```yaml
+events:
+ - package/published
+ - issue/created
+```
+
+When this file is changed in the project repository, it is parsed and the events are created, updated, or deleted.
+This is highly similar to [Proposal 1](proposal-1-using-the-gitlab-ci-file.md) except that we don't need to
+track pipeline creations every time.
+
+1. Upsert events to the database when `.gitlab-ci-event.yml` is updated.
+1. Create [EventStore subscriptions](../../../development/event_store.md) to handle the events.
+
+## Filtering jobs
+
+We can filter jobs by using the `rules` keyword. For example:
+
+```yaml
+test_package_published:
+ script: echo testing published package
+ rules:
+ - events: ["package/published"]
+
+test_package_removed:
+ script: echo testing removed package
+ rules:
+ - events: ["package/removed"]
+```
+
+Otherwise, we can make it work either a CI variable;
+
+```yaml
+test_package_published:
+ script: echo testing published package
+ rules:
+ - if: $CI_EVENT == "package/published"
+
+test_package_removed:
+ script: echo testing removed package
+ rules:
+ - if: $CI_EVENT == "package/removed"
+```
+
+or an input like in the [Proposal 3](proposal-3-using-the-gitlab-ci-events-folder.md);
+
+```yaml
+spec:
+ inputs:
+ event:
+ default: push
+
+---
+
+test_package_published:
+ script: echo testing published package
+ rules:
+ - if: $[[ inputs.event ]] == "package/published"
+
+test_package_removed:
+ script: echo testing removed package
+ rules:
+ - if: $[[ inputs.event ]] == "package/removed"
+```
diff --git a/doc/development/ai_features.md b/doc/development/ai_features.md
index 8e2746276a1..d53b2027412 100644
--- a/doc/development/ai_features.md
+++ b/doc/development/ai_features.md
@@ -55,6 +55,8 @@ All AI features are experimental.
```ruby
Feature.enable(:ai_related_settings)
Feature.enable(:openai_experimentation)
+ Feature.enable(:tofa_experimentation_main_flag)
+ Feature.enable(:anthropic_experimentation)
```
1. Simulate the GDK to [simulate SaaS](ee_features.md#simulate-a-saas-instance) and ensure the group you want to test has an Ultimate license
@@ -87,31 +89,49 @@ To populate the embedding database for GitLab chat:
1. Open a rails console
1. Run [this script](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/10588#note_1373586079) to populate the embedding database
-### Internal-Only GCP account access
+### Configure GCP Vertex access
In order to obtain a GCP service key for local development, please follow the steps below:
- Create a sandbox GCP environment by visiting [this page](https://about.gitlab.com/handbook/infrastructure-standards/#individual-environment) and following the instructions
- In the GCP console, go to `IAM & Admin` > `Service Accounts` and click on the "Create new service account" button
- Name the service account something specific to what you're using it for. Select Create and Continue. Under `Grant this service account access to project`, select the role `Vertex AI User`. Select `Continue` then `Done`
-- Select your new service account and `Manage keys` > `Add Key` > `Create new key`. This will download the **private** JSON credentials for your service account.
-- In the rails console, you will use this by `Gitlab::CurrentSettings.update(tofa_credentials: File.read('/YOUR_FILE.json'))`
+- Select your new service account and `Manage keys` > `Add Key` > `Create new key`. This will download the **private** JSON credentials for your service account. Your full settings should then be:
+
+```ruby
+Gitlab::CurrentSettings.update(tofa_credentials: File.read('/YOUR_FILE.json'))
+
+# Note: These credential examples will not work locally for all models
+Gitlab::CurrentSettings.update(tofa_host: "<root-domain>") # Example: us-central1-aiplatform.googleapis.com
+Gitlab::CurrentSettings.update(tofa_url: "<full-api-endpoint>") # Example: https://ROOT-DOMAIN/v1/projects/MY-COOL-PROJECT/locations/us-central1/publishers/google/models/MY-SPECIAL-MODEL:predict
+```
+
+Internal team members can [use this snippet](https://gitlab.com/gitlab-com/gl-infra/production/-/snippets/2541742) for help configuring these endpoints.
+
+### Configure OpenAI access
+
+```ruby
+Gitlab::CurrentSettings.update(openai_api_key: "<open-ai-key>")
+```
+
+### Configure Anthropic access
+
+```ruby
+Feature.enable(:anthropic_experimentation)
+Gitlab::CurrentSettings.update!(anthropic_api_key: <insert API key>)
+```
## Experimental REST API
-Use the [experimental REST API endpoints](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/api/ai/experimentation/open_ai.rb) to quickly experiment and prototype AI features.
+Use the [experimental REST API endpoints](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/api/ai/experimentation) to quickly experiment and prototype AI features.
The endpoints are:
- `https://gitlab.example.com/api/v4/ai/experimentation/openai/completions`
- `https://gitlab.example.com/api/v4/ai/experimentation/openai/embeddings`
- `https://gitlab.example.com/api/v4/ai/experimentation/openai/chat/completions`
-
-To use these endpoints locally, set the OpenAI API key in the application settings:
-
-```ruby
-Gitlab::CurrentSettings.update(openai_api_key: "<open-ai-key>")
-```
+- `https://gitlab.example.com/api/v4/ai/experimentation/anthropic/complete`
+- `https://gitlab.example.com/api/v4/ai/experimentation/tofa/chat`
These endpoints are only for prototyping, not for rolling features out to customers.
The experimental endpoint is only available to GitLab team members on production. Use the
diff --git a/doc/update/removals.md b/doc/update/removals.md
index 475e4cc9ca2..3e1fb620fcd 100644
--- a/doc/update/removals.md
+++ b/doc/update/removals.md
@@ -217,6 +217,15 @@ In GitLab 16.0 and later, the GraphQL query for runners will no longer return th
- `PAUSED` has been replaced with the field, `paused: true`.
- `ACTIVE` has been replaced with the field, `paused: false`.
+### Jira DVCS connector for Jira Cloud and Jira 8.13 and earlier
+
+WARNING:
+This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
+Review the details carefully before upgrading.
+
+The [Jira DVCS connector](https://docs.gitlab.com/ee/integration/jira/dvcs/) for Jira Cloud was deprecated in GitLab 15.1 and has been removed in 16.0. Use the [GitLab for Jira Cloud app](https://docs.gitlab.com/ee/integration/jira/connect-app.html) instead. The Jira DVCS connector was also deprecated for Jira 8.13 and earlier. You can only use the Jira DVCS connector with Jira Data Center or Jira Server in Jira 8.14 and later. Upgrade your Jira instance to Jira 8.14 or later, and reconfigure the Jira integration in your GitLab instance.
+If you cannot upgrade your Jira instance in time and are on GitLab self-managed version, we offer a workaround until GitLab 16.6. This breaking change is deployed in GitLab 16.0 behind a feature flag named `jira_dvcs_end_of_life_amnesty`. The flag is disabled by default, but you can ask an administrator to enable the flag at any time. For questions related to this announcement, see the [feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/408185).
+
### License-Check and the Policies tab on the License Compliance page
WARNING:
diff --git a/doc/user/admin_area/settings/rate_limit_on_projects_api.md b/doc/user/admin_area/settings/rate_limit_on_projects_api.md
index e82e682b0bb..29e72daf579 100644
--- a/doc/user/admin_area/settings/rate_limit_on_projects_api.md
+++ b/doc/user/admin_area/settings/rate_limit_on_projects_api.md
@@ -10,6 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112283) in GitLab 15.10 with a [flag](../../../administration/feature_flags.md) named `rate_limit_for_unauthenticated_projects_api_access`. Disabled by default.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/391922) on May 08, 2023.
> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119603) in GitLab 16.0 by default.
+> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120445) in GitLab 16.0. Feature flag `rate_limit_for_unauthenticated_projects_api_access` removed.
You can configure the rate limit per IP address for unauthenticated requests to the [list all projects API](../../../api/projects.md#list-all-projects).
diff --git a/doc/user/project/integrations/webhook_events.md b/doc/user/project/integrations/webhook_events.md
index 5585b0bf3e1..fda778aa167 100644
--- a/doc/user/project/integrations/webhook_events.md
+++ b/doc/user/project/integrations/webhook_events.md
@@ -271,6 +271,7 @@ Payload example:
"human_time_estimate": null,
"human_time_change": null,
"weight": null,
+ "health_status": "at_risk",
"iid": 23,
"url": "http://example.com/diaspora/issues/23",
"state": "opened",
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 697c2a7e214..d6863e4eba4 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -112,8 +112,6 @@ module API
end
def validate_projects_api_rate_limit_for_unauthenticated_users!
- return unless Feature.enabled?(:rate_limit_for_unauthenticated_projects_api_access)
-
check_rate_limit!(:projects_api_rate_limit_unauthenticated, scope: [ip_address]) if current_user.blank?
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 84729dba6ef..99ac0af463e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -18974,6 +18974,9 @@ msgstr ""
msgid "Forbidden"
msgstr ""
+msgid "Forecast horizon must be %{max_horizon} days at the most."
+msgstr ""
+
msgid "Forgot your password?"
msgstr ""
@@ -24210,6 +24213,9 @@ msgstr ""
msgid "Invalid URL: %{url}"
msgstr ""
+msgid "Invalid context. Project is expected."
+msgstr ""
+
msgid "Invalid date"
msgstr ""
@@ -47888,6 +47894,9 @@ msgstr ""
msgid "Unsubscribes from this %{quick_action_target}."
msgstr ""
+msgid "Unsupported forecast type. Supported types: %{types}"
+msgstr ""
+
msgid "Unsupported sort value."
msgstr ""
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
index 4bc7e11ec6b..92b228b6836 100644
--- a/spec/controllers/graphql_controller_spec.rb
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -146,7 +146,7 @@ RSpec.describe GraphqlController, feature_category: :integrations do
])
end
- it 'does not allow string as _json parameter' do
+ it 'does not allow string as _json parameter (a malformed multiplex query)' do
post :execute, params: { _json: 'bad' }
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
index fae3b0c5d1a..1b526e6fbec 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
@@ -1,9 +1,9 @@
import { GlAlert, GlFormInput, GlForm, GlLink } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { createStore } from '~/boards/stores';
@@ -32,6 +32,7 @@ const TEST_ISSUE_B = {
describe('BoardSidebarTitle', () => {
let wrapper;
let store;
+ let storeDispatch;
let mockApollo;
const issueSetTitleMutationHandlerSuccess = jest.fn().mockResolvedValue(updateIssueTitleResponse);
@@ -52,8 +53,9 @@ describe('BoardSidebarTitle', () => {
[issueSetTitleMutation, issueSetTitleMutationHandlerSuccess],
[updateEpicTitleMutation, updateEpicTitleMutationHandlerSuccess],
]);
+ storeDispatch = jest.spyOn(store, 'dispatch');
- wrapper = shallowMount(BoardSidebarTitle, {
+ wrapper = shallowMountExtended(BoardSidebarTitle, {
store,
apolloProvider: mockApollo,
provide: {
@@ -78,9 +80,9 @@ describe('BoardSidebarTitle', () => {
const findFormInput = () => wrapper.findComponent(GlFormInput);
const findGlLink = () => wrapper.findComponent(GlLink);
const findEditableItem = () => wrapper.findComponent(BoardEditableItem);
- const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
- const findTitle = () => wrapper.find('[data-testid="item-title"]');
- const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
+ const findTitle = () => wrapper.findByTestId('item-title');
+ const findCollapsed = () => wrapper.findByTestId('collapsed-content');
it('renders title and reference', () => {
createWrapper();
@@ -105,9 +107,6 @@ describe('BoardSidebarTitle', () => {
beforeEach(async () => {
createWrapper();
- jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
- store.state.boardItems[TEST_ISSUE_A.id].title = TEST_TITLE;
- });
findFormInput().vm.$emit('input', TEST_TITLE);
findForm().vm.$emit('submit', { preventDefault: () => {} });
await nextTick();
@@ -116,29 +115,34 @@ describe('BoardSidebarTitle', () => {
it('collapses sidebar and renders new title', async () => {
await waitForPromises();
expect(findCollapsed().isVisible()).toBe(true);
- expect(findTitle().text()).toContain(TEST_TITLE);
});
it('commits change to the server', () => {
- expect(wrapper.vm.setActiveItemTitle).toHaveBeenCalledWith({
- title: TEST_TITLE,
+ expect(storeDispatch).toHaveBeenCalledWith('setActiveItemTitle', {
projectPath: 'h/b',
+ title: 'New item title',
});
});
+
+ it('renders correct title', async () => {
+ createWrapper({ item: { ...TEST_ISSUE_A, title: TEST_TITLE } });
+ await waitForPromises();
+
+ expect(findTitle().text()).toContain(TEST_TITLE);
+ });
});
describe('when submitting and invalid title', () => {
beforeEach(async () => {
createWrapper();
- jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {});
findFormInput().vm.$emit('input', '');
findForm().vm.$emit('submit', { preventDefault: () => {} });
await nextTick();
});
it('commits change to the server', () => {
- expect(wrapper.vm.setActiveItemTitle).not.toHaveBeenCalled();
+ expect(storeDispatch).not.toHaveBeenCalled();
});
});
@@ -169,7 +173,7 @@ describe('BoardSidebarTitle', () => {
});
it('sets title, expands item and shows alert', () => {
- expect(wrapper.vm.title).toBe(TEST_TITLE);
+ expect(findFormInput().attributes('value')).toBe(TEST_TITLE);
expect(findCollapsed().isVisible()).toBe(false);
expect(findAlert().exists()).toBe(true);
});
@@ -179,16 +183,13 @@ describe('BoardSidebarTitle', () => {
beforeEach(async () => {
createWrapper({ item: TEST_ISSUE_B });
- jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
- store.state.boardItems[TEST_ISSUE_B.id].title = TEST_TITLE;
- });
findFormInput().vm.$emit('input', TEST_TITLE);
findCancelButton().vm.$emit('click');
await nextTick();
});
it('collapses sidebar and render former title', () => {
- expect(wrapper.vm.setActiveItemTitle).not.toHaveBeenCalled();
+ expect(storeDispatch).not.toHaveBeenCalled();
expect(findCollapsed().isVisible()).toBe(true);
expect(findTitle().text()).toBe(TEST_ISSUE_B.title);
});
@@ -198,10 +199,6 @@ describe('BoardSidebarTitle', () => {
beforeEach(async () => {
createWrapper({ item: TEST_ISSUE_B });
- jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
- throw new Error(['failed mutation']);
- });
- jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
findFormInput().vm.$emit('input', 'Invalid title');
findForm().vm.$emit('submit', { preventDefault: () => {} });
await nextTick();
@@ -210,7 +207,10 @@ describe('BoardSidebarTitle', () => {
it('collapses sidebar and renders former item title', () => {
expect(findCollapsed().isVisible()).toBe(true);
expect(findTitle().text()).toContain(TEST_ISSUE_B.title);
- expect(wrapper.vm.setError).toHaveBeenCalled();
+ expect(storeDispatch).toHaveBeenCalledWith(
+ 'setError',
+ expect.objectContaining({ message: 'An error occurred when updating the title' }),
+ );
});
});
diff --git a/spec/frontend/google_cloud/aiml/panel_spec.js b/spec/frontend/google_cloud/aiml/panel_spec.js
new file mode 100644
index 00000000000..374e125c509
--- /dev/null
+++ b/spec/frontend/google_cloud/aiml/panel_spec.js
@@ -0,0 +1,43 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Panel from '~/google_cloud/aiml/panel.vue';
+import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
+import GoogleCloudMenu from '~/google_cloud/components/google_cloud_menu.vue';
+import ServiceTable from '~/google_cloud/aiml/service_table.vue';
+
+describe('google_cloud/databases/panel', () => {
+ let wrapper;
+
+ const props = {
+ configurationUrl: 'configuration-url',
+ deploymentsUrl: 'deployments-url',
+ databasesUrl: 'databases-url',
+ aimlUrl: 'aiml-url',
+ visionAiUrl: 'vision-ai-url',
+ translationAiUrl: 'translation-ai-url',
+ languageAiUrl: 'language-ai-url',
+ };
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(Panel, { propsData: props });
+ });
+
+ it('contains incubation banner', () => {
+ const target = wrapper.findComponent(IncubationBanner);
+ expect(target.exists()).toBe(true);
+ });
+
+ it('contains google cloud menu with `aiml` active', () => {
+ const target = wrapper.findComponent(GoogleCloudMenu);
+ expect(target.exists()).toBe(true);
+ expect(target.props('active')).toBe('aiml');
+ expect(target.props('configurationUrl')).toBe(props.configurationUrl);
+ expect(target.props('deploymentsUrl')).toBe(props.deploymentsUrl);
+ expect(target.props('databasesUrl')).toBe(props.databasesUrl);
+ expect(target.props('aimlUrl')).toBe(props.aimlUrl);
+ });
+
+ it('contains service table', () => {
+ const target = wrapper.findComponent(ServiceTable);
+ expect(target.exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/google_cloud/components/google_cloud_menu_spec.js b/spec/frontend/google_cloud/components/google_cloud_menu_spec.js
index a0c988830ed..f1ee96ff870 100644
--- a/spec/frontend/google_cloud/components/google_cloud_menu_spec.js
+++ b/spec/frontend/google_cloud/components/google_cloud_menu_spec.js
@@ -9,6 +9,7 @@ describe('google_cloud/components/google_cloud_menu', () => {
configurationUrl: 'configuration-url',
deploymentsUrl: 'deployments-url',
databasesUrl: 'databases-url',
+ aimlUrl: 'aiml-url',
};
beforeEach(() => {
@@ -33,4 +34,10 @@ describe('google_cloud/components/google_cloud_menu', () => {
expect(link.text()).toBe(GoogleCloudMenu.i18n.databases.title);
expect(link.attributes('href')).toBe(props.databasesUrl);
});
+
+ it('contains ai/ml link', () => {
+ const link = wrapper.findByTestId('aimlLink');
+ expect(link.text()).toBe(GoogleCloudMenu.i18n.aiml.title);
+ expect(link.attributes('href')).toBe(props.aimlUrl);
+ });
});
diff --git a/spec/frontend/lib/utils/sticky_spec.js b/spec/frontend/lib/utils/sticky_spec.js
deleted file mode 100644
index ec9e746c838..00000000000
--- a/spec/frontend/lib/utils/sticky_spec.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import { setHTMLFixture } from 'helpers/fixtures';
-import { isSticky } from '~/lib/utils/sticky';
-
-const TEST_OFFSET_TOP = 500;
-
-describe('sticky', () => {
- let el;
- let offsetTop;
-
- beforeEach(() => {
- setHTMLFixture(
- `
- <div class="parent">
- <div id="js-sticky"></div>
- </div>
- `,
- );
-
- offsetTop = TEST_OFFSET_TOP;
- el = document.getElementById('js-sticky');
- Object.defineProperty(el, 'offsetTop', {
- get() {
- return offsetTop;
- },
- });
- });
-
- afterEach(() => {
- el = null;
- });
-
- describe('when stuck', () => {
- it('does not remove is-stuck class', () => {
- isSticky(el, 0, el.offsetTop);
- isSticky(el, 0, el.offsetTop);
-
- expect(el.classList.contains('is-stuck')).toBe(true);
- });
-
- it('adds is-stuck class', () => {
- isSticky(el, 0, el.offsetTop);
-
- expect(el.classList.contains('is-stuck')).toBe(true);
- });
-
- it('inserts placeholder element', () => {
- isSticky(el, 0, el.offsetTop, true);
-
- expect(document.querySelector('.sticky-placeholder')).not.toBeNull();
- });
- });
-
- describe('when not stuck', () => {
- it('removes is-stuck class', () => {
- jest.spyOn(el.classList, 'remove');
-
- isSticky(el, 0, el.offsetTop);
- isSticky(el, 0, 0);
-
- expect(el.classList.remove).toHaveBeenCalledWith('is-stuck');
- expect(el.classList.contains('is-stuck')).toBe(false);
- });
-
- it('does not add is-stuck class', () => {
- isSticky(el, 0, 0);
-
- expect(el.classList.contains('is-stuck')).toBe(false);
- });
-
- it('removes placeholder', () => {
- isSticky(el, 0, el.offsetTop, true);
- isSticky(el, 0, 0, true);
-
- expect(document.querySelector('.sticky-placeholder')).toBeNull();
- });
- });
-});
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index 1e7470ace8e..5d81a7a9a0f 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -1,7 +1,7 @@
-import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { GlAvatar } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import DiffsModule from '~/diffs/store/modules';
import NoteActions from '~/notes/components/note_actions.vue';
@@ -37,7 +37,9 @@ describe('issue_note', () => {
const REPORT_ABUSE_PATH = '/abuse_reports/add_category';
- const findMultilineComment = () => wrapper.find('[data-testid="multiline-comment"]');
+ const findNoteBody = () => wrapper.findComponent(NoteBody);
+
+ const findMultilineComment = () => wrapper.findByTestId('multiline-comment');
const createWrapper = (props = {}, storeUpdater = (s) => s) => {
store = new Vuex.Store(
@@ -52,7 +54,7 @@ describe('issue_note', () => {
store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
- wrapper = mount(issueNote, {
+ wrapper = mountExtended(issueNote, {
store,
propsData: {
note,
@@ -250,21 +252,17 @@ describe('issue_note', () => {
});
it('should render issue body', () => {
- const noteBody = wrapper.findComponent(NoteBody);
- const noteBodyProps = noteBody.props();
-
- expect(noteBodyProps.note).toBe(note);
- expect(noteBodyProps.line).toBe(null);
- expect(noteBodyProps.canEdit).toBe(note.current_user.can_edit);
- expect(noteBodyProps.isEditing).toBe(false);
- expect(noteBodyProps.helpPagePath).toBe('');
+ expect(findNoteBody().props().note).toBe(note);
+ expect(findNoteBody().props().line).toBe(null);
+ expect(findNoteBody().props().canEdit).toBe(note.current_user.can_edit);
+ expect(findNoteBody().props().isEditing).toBe(false);
+ expect(findNoteBody().props().helpPagePath).toBe('');
});
it('prevents note preview xss', async () => {
const noteBody =
'<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" onload="alert(1)" />';
const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
- const noteBodyComponent = wrapper.findComponent(NoteBody);
store.hotUpdate({
modules: {
@@ -277,7 +275,7 @@ describe('issue_note', () => {
},
});
- noteBodyComponent.vm.$emit('handleFormUpdate', {
+ findNoteBody().vm.$emit('handleFormUpdate', {
noteText: noteBody,
parentElement: null,
callback: () => {},
@@ -285,7 +283,7 @@ describe('issue_note', () => {
await waitForPromises();
expect(alertSpy).not.toHaveBeenCalled();
- expect(wrapper.vm.note.note_html).toBe(
+ expect(findNoteBody().props().note.note_html).toBe(
'<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7">',
);
});
@@ -321,26 +319,21 @@ describe('issue_note', () => {
},
},
});
- const noteBody = wrapper.findComponent(NoteBody);
- noteBody.vm.resetAutoSave = () => {};
- noteBody.vm.$emit('handleFormUpdate', {
+ findNoteBody().vm.$emit('handleFormUpdate', {
noteText: updatedText,
parentElement: null,
callback: () => {},
});
await nextTick();
- let noteBodyProps = noteBody.props();
- expect(noteBodyProps.note.note_html).toBe(`<p dir="auto">${updatedText}</p>\n`);
+ expect(findNoteBody().props().note.note_html).toBe(`<p dir="auto">${updatedText}</p>\n`);
- noteBody.vm.$emit('cancelForm', {});
+ findNoteBody().vm.$emit('cancelForm', {});
await nextTick();
- noteBodyProps = noteBody.props();
-
- expect(noteBodyProps.note.note_html).toBe(note.note_html);
+ expect(findNoteBody().props().note.note_html).toBe(note.note_html);
});
});
@@ -371,7 +364,7 @@ describe('issue_note', () => {
it('responds to handleFormUpdate', () => {
createWrapper();
updateActions();
- wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', params);
+ findNoteBody().vm.$emit('handleFormUpdate', params);
expect(wrapper.emitted('handleUpdateNote')).toHaveLength(1);
});
@@ -380,16 +373,14 @@ describe('issue_note', () => {
createWrapper();
updateActions();
- wrapper
- .findComponent(NoteBody)
- .vm.$emit('handleFormUpdate', { ...params, noteText: sensitiveMessage });
+ findNoteBody().vm.$emit('handleFormUpdate', { ...params, noteText: sensitiveMessage });
expect(updateNote).not.toHaveBeenCalled();
});
it('does not stringify empty position', () => {
createWrapper();
updateActions();
- wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', params);
+ findNoteBody().vm.$emit('handleFormUpdate', params);
expect(updateNote.mock.calls[0][1].note.note.position).toBeUndefined();
});
@@ -398,7 +389,7 @@ describe('issue_note', () => {
const expectation = JSON.stringify(position);
createWrapper({ note: { ...note, position } });
updateActions();
- wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', params);
+ findNoteBody().vm.$emit('handleFormUpdate', params);
expect(updateNote.mock.calls[0][1].note.note.position).toBe(expectation);
});
});
@@ -423,7 +414,7 @@ describe('issue_note', () => {
createWrapper({ note: noteDef, discussionFile: null }, storeUpdater);
- expect(wrapper.vm.diffFile).toBe(null);
+ expect(findNoteBody().props().file).toBe(null);
},
);
@@ -441,7 +432,7 @@ describe('issue_note', () => {
},
);
- expect(wrapper.vm.diffFile.testId).toBe('diffFileTest');
+ expect(findNoteBody().props().file.testId).toBe('diffFileTest');
});
it('returns the provided diff file if the more robust getters fail', () => {
@@ -457,7 +448,7 @@ describe('issue_note', () => {
},
);
- expect(wrapper.vm.diffFile.testId).toBe('diffFileTest');
+ expect(findNoteBody().props().file.testId).toBe('diffFileTest');
});
});
});
diff --git a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
index 930d2fc8cfe..2a4037d76b7 100644
--- a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
@@ -58,7 +58,6 @@ describe('Diff Stats Dropdown', () => {
const findChangedFiles = () => findChanged().findAllComponents(GlDropdownItem);
const findNoFilesText = () => findChanged().findComponent(GlDropdownText);
const findCollapsed = () => wrapper.findByTestId('diff-stats-additions-deletions-expanded');
- const findExpanded = () => wrapper.findByTestId('diff-stats-additions-deletions-collapsed');
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
describe('file item', () => {
@@ -88,24 +87,17 @@ describe('Diff Stats Dropdown', () => {
});
describe.each`
- changed | added | deleted | expectedDropdownHeader | expectedAddedDeletedExpanded | expectedAddedDeletedCollapsed
- ${0} | ${0} | ${0} | ${'0 changed files'} | ${'+0 -0'} | ${'with 0 additions and 0 deletions'}
- ${2} | ${0} | ${2} | ${'2 changed files'} | ${'+0 -2'} | ${'with 0 additions and 2 deletions'}
- ${2} | ${2} | ${0} | ${'2 changed files'} | ${'+2 -0'} | ${'with 2 additions and 0 deletions'}
- ${2} | ${1} | ${1} | ${'2 changed files'} | ${'+1 -1'} | ${'with 1 addition and 1 deletion'}
- ${1} | ${0} | ${1} | ${'1 changed file'} | ${'+0 -1'} | ${'with 0 additions and 1 deletion'}
- ${1} | ${1} | ${0} | ${'1 changed file'} | ${'+1 -0'} | ${'with 1 addition and 0 deletions'}
- ${4} | ${2} | ${2} | ${'4 changed files'} | ${'+2 -2'} | ${'with 2 additions and 2 deletions'}
+ changed | added | deleted | expectedDropdownHeader | expectedAddedDeletedCollapsed
+ ${0} | ${0} | ${0} | ${'0 changed files'} | ${'with 0 additions and 0 deletions'}
+ ${2} | ${0} | ${2} | ${'2 changed files'} | ${'with 0 additions and 2 deletions'}
+ ${2} | ${2} | ${0} | ${'2 changed files'} | ${'with 2 additions and 0 deletions'}
+ ${2} | ${1} | ${1} | ${'2 changed files'} | ${'with 1 addition and 1 deletion'}
+ ${1} | ${0} | ${1} | ${'1 changed file'} | ${'with 0 additions and 1 deletion'}
+ ${1} | ${1} | ${0} | ${'1 changed file'} | ${'with 1 addition and 0 deletions'}
+ ${4} | ${2} | ${2} | ${'4 changed files'} | ${'with 2 additions and 2 deletions'}
`(
'when there are $changed changed file(s), $added added and $deleted deleted file(s)',
- ({
- changed,
- added,
- deleted,
- expectedDropdownHeader,
- expectedAddedDeletedExpanded,
- expectedAddedDeletedCollapsed,
- }) => {
+ ({ changed, added, deleted, expectedDropdownHeader, expectedAddedDeletedCollapsed }) => {
beforeEach(() => {
createComponent({ changed, added, deleted });
});
@@ -114,10 +106,6 @@ describe('Diff Stats Dropdown', () => {
expect(findChanged().props('text')).toBe(expectedDropdownHeader);
});
- it(`added and deleted count in expanded section should be '${expectedAddedDeletedExpanded}'`, () => {
- expect(findExpanded().text()).toBe(expectedAddedDeletedExpanded);
- });
-
it(`added and deleted count in collapsed section should be '${expectedAddedDeletedCollapsed}'`, () => {
expect(findCollapsed().text()).toBe(expectedAddedDeletedCollapsed);
});
diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
index dc67097d763..3e4d5c558f6 100644
--- a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
@@ -12,6 +12,7 @@ import {
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
import { FEATURABLE_DISABLED, FEATURABLE_ENABLED } from '~/featurable/constants';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
jest.mock('lodash/uniqueId', () => (prefix) => `${prefix}1`);
@@ -36,6 +37,7 @@ describe('ProjectsListItem', () => {
const findForksLink = () => wrapper.findByRole('link', { name: ProjectsListItem.i18n.forks });
const findProjectTopics = () => wrapper.findByTestId('project-topics');
const findPopover = () => findProjectTopics().findComponent(GlPopover);
+ const findProjectDescription = () => wrapper.findByTestId('project-description');
it('renders project avatar', () => {
createComponent();
@@ -105,6 +107,12 @@ describe('ProjectsListItem', () => {
expect(starsLink.findComponent(GlIcon).props('name')).toBe('star-o');
});
+ it('renders updated at', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(TimeAgoTooltip).props('time')).toBe(project.updatedAt);
+ });
+
describe('when issues are enabled', () => {
it('renders issues count', () => {
createComponent();
@@ -230,4 +238,29 @@ describe('ProjectsListItem', () => {
});
});
});
+
+ describe('when project has a description', () => {
+ it('renders description', () => {
+ const descriptionHtml = '<p>Foo bar</p>';
+
+ createComponent({
+ propsData: {
+ project: {
+ ...project,
+ descriptionHtml,
+ },
+ },
+ });
+
+ expect(findProjectDescription().element.innerHTML).toBe(descriptionHtml);
+ });
+ });
+
+ describe('when project does not have a description', () => {
+ it('does not render description', () => {
+ createComponent();
+
+ expect(findProjectDescription().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 17a6eec2a8e..349101a092f 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1181,30 +1181,6 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :projects d
end
end
end
-
- context 'when the feature flag `rate_limit_for_unauthenticated_projects_api_access` is disabled' do
- before do
- stub_feature_flags(rate_limit_for_unauthenticated_projects_api_access: false)
- end
-
- context 'when the user is not signed in' do
- let_it_be(:current_user) { nil }
-
- it_behaves_like 'does not log request and does not block the request' do
- def request
- get api(path, current_user)
- end
- end
- end
-
- context 'when the user is signed in' do
- it_behaves_like 'does not log request and does not block the request' do
- def request
- get api(path, current_user)
- end
- end
- end
- end
end
end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 4b89403359f..0de1300bc50 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -38,7 +38,7 @@ JS_CONSOLE_FILTER = Regexp.union(
CAPYBARA_WINDOW_SIZE = [1366, 768].freeze
-SCREENSHOT_FILENAME_LENGTH = ENV['CI'] || ENV['CI_SERVER'] ? 255 : 99
+SCREENSHOT_FILENAME_LENGTH = ENV['CI'] || ENV['CI_SERVER'] ? 150 : 99
@blackhole_tcp_server = nil
diff --git a/spec/support/helpers/keyset_pagination_helpers.rb b/spec/support/helpers/keyset_pagination_helpers.rb
index 4bc20098e8c..4a476c47fda 100644
--- a/spec/support/helpers/keyset_pagination_helpers.rb
+++ b/spec/support/helpers/keyset_pagination_helpers.rb
@@ -7,14 +7,17 @@ module KeysetPaginationHelpers
link.split(',').filter_map do |link|
match = link.match(/<(?<url>.*)>; rel="(?<rel>\w+)"/)
- break nil unless match
+ next unless match
{ url: match[:url], rel: match[:rel] }
end
end
def pagination_params_from_next_url(response)
- next_url = pagination_links(response).find { |link| link[:rel] == 'next' }[:url]
+ next_link = pagination_links(response).find { |link| link[:rel] == 'next' }
+ next_url = next_link&.fetch(:url)
+ return unless next_url
+
Rack::Utils.parse_query(URI.parse(next_url).query)
end
end
diff --git a/spec/support_specs/helpers/keyset_pagination_helpers_spec.rb b/spec/support_specs/helpers/keyset_pagination_helpers_spec.rb
new file mode 100644
index 00000000000..ec63f33776c
--- /dev/null
+++ b/spec/support_specs/helpers/keyset_pagination_helpers_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe KeysetPaginationHelpers, feature_category: :api do
+ include described_class
+
+ let(:headers) { { 'LINK' => %(<#{url}>; rel="#{rel}") } }
+ let(:response) { instance_double('HTTParty::Response', headers: headers) }
+ let(:rel) { 'next' }
+ let(:url) do
+ 'http://127.0.0.1:3000/api/v4/projects/7/audit_eve' \
+ 'nts?cursor=eyJpZCI6IjYyMjAiLCJfa2QiOiJuIn0%3D&id=7&o' \
+ 'rder_by=id&page=1&pagination=keyset&per_page=2'
+ end
+
+ describe '#pagination_links' do
+ subject { pagination_links(response) }
+
+ let(:expected_result) { [{ url: url, rel: rel }] }
+
+ it { is_expected.to eq expected_result }
+
+ context 'with a partially malformed LINK header' do
+ # malformed as the regxe is expecting the url to be surrounded by `<>`
+ let(:headers) do
+ { 'LINK' => %(<#{url}>; rel="next", GARBAGE, #{url}; rel="prev") }
+ end
+
+ it { is_expected.to eq expected_result }
+ end
+
+ context 'with a malformed LINK header' do
+ # malformed as the regxe is expecting the url to be surrounded by `<>`
+ let(:headers) { { 'LINK' => %(rel="next", GARBAGE, #{url}; rel="prev") } }
+ let(:expected_result) { [] }
+
+ it { is_expected.to eq expected_result }
+ end
+ end
+
+ describe '#pagination_params_from_next_url' do
+ subject { pagination_params_from_next_url(response) }
+
+ let(:expected_result) do
+ {
+ 'cursor' => 'eyJpZCI6IjYyMjAiLCJfa2QiOiJuIn0=',
+ 'id' => '7',
+ 'order_by' => 'id',
+ 'page' => '1',
+ 'pagination' => 'keyset',
+ 'per_page' => '2'
+ }
+ end
+
+ it { is_expected.to eq expected_result }
+
+ context 'with both prev and next rel links' do
+ let(:prev_url) do
+ 'http://127.0.0.1:3000/api/v4/projects/7/audit_eve' \
+ 'nts?cursor=foocursor&id=8&o' \
+ 'rder_by=id&page=0&pagination=keyset&per_page=2'
+ end
+
+ let(:headers) do
+ { 'LINK' => %(<#{url}>; rel="next", <#{prev_url}>; rel="prev") }
+ end
+
+ it { is_expected.to eq expected_result }
+ end
+
+ context 'with a partially malformed LINK header' do
+ # malformed as the regxe is expecting the url to be surrounded by `<>`
+ let(:headers) do
+ { 'LINK' => %(<#{url}>; rel="next", GARBAGE, #{url}; rel="prev") }
+ end
+
+ it { is_expected.to eq expected_result }
+ end
+
+ context 'with a malformed LINK header' do
+ # malformed as the regxe is expecting the url to be surrounded by `<>`
+ let(:headers) { { 'LINK' => %(rel="next", GARBAGE, #{url}; rel="prev") } }
+
+ it { is_expected.to be nil }
+ end
+ end
+end
diff --git a/spec/views/admin/application_settings/network.html.haml_spec.rb b/spec/views/admin/application_settings/network.html.haml_spec.rb
index 3df55be92d5..17515dbcc2c 100644
--- a/spec/views/admin/application_settings/network.html.haml_spec.rb
+++ b/spec/views/admin/application_settings/network.html.haml_spec.rb
@@ -17,17 +17,5 @@ RSpec.describe 'admin/application_settings/network.html.haml', feature_category:
expect(rendered).to have_field('application_setting_projects_api_rate_limit_unauthenticated')
end
-
- context 'when the feature flag `rate_limit_for_unauthenticated_projects_api_access` is turned off' do
- before do
- stub_feature_flags(rate_limit_for_unauthenticated_projects_api_access: false)
- end
-
- it 'does not render the `projects_api_rate_limit_unauthenticated` field' do
- render
-
- expect(rendered).not_to have_field('application_setting_projects_api_rate_limit_unauthenticated')
- end
- end
end
end