Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-02-23 00:18:11 +00:00
parent beeaef0da5
commit 6adb784bf2
31 changed files with 265 additions and 166 deletions

View File

@ -19,7 +19,6 @@ Rails/IncludeUrlHelper:
- app/models/integrations/redmine.rb - app/models/integrations/redmine.rb
- app/models/integrations/webex_teams.rb - app/models/integrations/webex_teams.rb
- app/models/integrations/youtrack.rb - app/models/integrations/youtrack.rb
- app/presenters/gitlab/blame_presenter.rb
- ee/app/models/integrations/github.rb - ee/app/models/integrations/github.rb
- ee/spec/helpers/ee/projects/security/configuration_helper_spec.rb - ee/spec/helpers/ee/projects/security/configuration_helper_spec.rb
- ee/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb - ee/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb

View File

@ -1,16 +1,6 @@
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants'; import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants';
export const GRAPHQL_PAGE_SIZE = 30;
export const initialPaginationState = {
currentPage: 1,
prevPageCursor: '',
nextPageCursor: '',
first: GRAPHQL_PAGE_SIZE,
last: null,
};
/* Error constants */ /* Error constants */
export const POST_FAILURE = 'post_failure'; export const POST_FAILURE = 'post_failure';
export const DEFAULT = 'default'; export const DEFAULT = 'default';

View File

@ -0,0 +1,29 @@
import { isEqual } from 'lodash';
export default {
typePolicies: {
Project: {
fields: {
jobs: {
keyArgs: false,
},
},
},
CiJobConnection: {
merge(existing = {}, incoming, { args = {} }) {
let nodes;
if (Object.keys(existing).length !== 0 && isEqual(existing?.statuses, args?.statuses)) {
nodes = [...existing.nodes, ...incoming.nodes];
} else {
nodes = [...incoming.nodes];
}
return {
nodes,
statuses: Array.isArray(args.statuses) ? [...args.statuses] : args.statuses,
};
},
},
},
};

View File

@ -1,25 +1,22 @@
query getJobs( query getJobs($fullPath: ID!, $after: String, $statuses: [CiJobStatus!]) {
$fullPath: ID!
$first: Int
$last: Int
$after: String
$before: String
$statuses: [CiJobStatus!]
) {
project(fullPath: $fullPath) { project(fullPath: $fullPath) {
id id
jobs(after: $after, before: $before, first: $first, last: $last, statuses: $statuses) { __typename
jobs(after: $after, first: 30, statuses: $statuses) {
pageInfo { pageInfo {
endCursor endCursor
hasNextPage hasNextPage
hasPreviousPage hasPreviousPage
startCursor startCursor
__typename
} }
nodes { nodes {
__typename
artifacts { artifacts {
nodes { nodes {
downloadPath downloadPath
fileType fileType
__typename
} }
} }
allowFailure allowFailure

View File

@ -4,12 +4,18 @@ import VueApollo from 'vue-apollo';
import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue'; import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import cacheConfig from './graphql/cache_config';
Vue.use(VueApollo); Vue.use(VueApollo);
Vue.use(GlToast); Vue.use(GlToast);
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(), defaultClient: createDefaultClient(
{},
{
cacheConfig,
},
),
}); });
export default (containerId = 'js-jobs-table') => { export default (containerId = 'js-jobs-table') => {

View File

@ -1,7 +1,6 @@
<script> <script>
import { GlAlert, GlPagination, GlSkeletonLoader } from '@gitlab/ui'; import { GlAlert, GlSkeletonLoader, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { GRAPHQL_PAGE_SIZE, initialPaginationState } from './constants';
import eventHub from './event_hub'; import eventHub from './event_hub';
import GetJobs from './graphql/queries/get_jobs.query.graphql'; import GetJobs from './graphql/queries/get_jobs.query.graphql';
import JobsTable from './jobs_table.vue'; import JobsTable from './jobs_table.vue';
@ -11,14 +10,16 @@ import JobsTableTabs from './jobs_table_tabs.vue';
export default { export default {
i18n: { i18n: {
errorMsg: __('There was an error fetching the jobs for your project.'), errorMsg: __('There was an error fetching the jobs for your project.'),
loadingAriaLabel: __('Loading'),
}, },
components: { components: {
GlAlert, GlAlert,
GlPagination,
GlSkeletonLoader, GlSkeletonLoader,
JobsTable, JobsTable,
JobsTableEmptyState, JobsTableEmptyState,
JobsTableTabs, JobsTableTabs,
GlIntersectionObserver,
GlLoadingIcon,
}, },
inject: { inject: {
fullPath: { fullPath: {
@ -31,10 +32,6 @@ export default {
variables() { variables() {
return { return {
fullPath: this.fullPath, fullPath: this.fullPath,
first: this.pagination.first,
last: this.pagination.last,
after: this.pagination.nextPageCursor,
before: this.pagination.prevPageCursor,
}; };
}, },
update(data) { update(data) {
@ -57,7 +54,7 @@ export default {
hasError: false, hasError: false,
isAlertDismissed: false, isAlertDismissed: false,
scope: null, scope: null,
pagination: initialPaginationState, firstLoad: true,
}; };
}, },
computed: { computed: {
@ -67,14 +64,8 @@ export default {
showEmptyState() { showEmptyState() {
return this.jobs.list.length === 0 && !this.scope; return this.jobs.list.length === 0 && !this.scope;
}, },
prevPage() { hasNextPage() {
return Math.max(this.pagination.currentPage - 1, 0); return this.jobs?.pageInfo?.hasNextPage;
},
nextPage() {
return this.jobs.pageInfo?.hasNextPage ? this.pagination.currentPage + 1 : null;
},
showPaginationControls() {
return Boolean(this.prevPage || this.nextPage) && !this.$apollo.loading;
}, },
}, },
mounted() { mounted() {
@ -88,26 +79,22 @@ export default {
this.$apollo.queries.jobs.refetch({ statuses: this.scope }); this.$apollo.queries.jobs.refetch({ statuses: this.scope });
}, },
fetchJobsByStatus(scope) { fetchJobsByStatus(scope) {
this.firstLoad = true;
this.scope = scope; this.scope = scope;
this.$apollo.queries.jobs.refetch({ statuses: scope }); this.$apollo.queries.jobs.refetch({ statuses: scope });
}, },
handlePageChange(page) { fetchMoreJobs() {
const { startCursor, endCursor } = this.jobs.pageInfo; this.firstLoad = false;
if (page > this.pagination.currentPage) { if (!this.$apollo.queries.jobs.loading) {
this.pagination = { this.$apollo.queries.jobs.fetchMore({
...initialPaginationState, variables: {
nextPageCursor: endCursor, fullPath: this.fullPath,
currentPage: page, after: this.jobs?.pageInfo?.endCursor,
}; },
} else { });
this.pagination = {
last: GRAPHQL_PAGE_SIZE,
first: null,
prevPageCursor: startCursor,
currentPage: page,
};
} }
}, },
}, },
@ -128,7 +115,7 @@ export default {
<jobs-table-tabs @fetchJobsByStatus="fetchJobsByStatus" /> <jobs-table-tabs @fetchJobsByStatus="fetchJobsByStatus" />
<div v-if="$apollo.loading" class="gl-mt-5"> <div v-if="$apollo.loading && firstLoad" class="gl-mt-5">
<gl-skeleton-loader :width="1248" :height="73"> <gl-skeleton-loader :width="1248" :height="73">
<circle cx="748.031" cy="37.7193" r="15.0307" /> <circle cx="748.031" cy="37.7193" r="15.0307" />
<circle cx="787.241" cy="37.7193" r="15.0307" /> <circle cx="787.241" cy="37.7193" r="15.0307" />
@ -149,14 +136,12 @@ export default {
<jobs-table v-else :jobs="jobs.list" /> <jobs-table v-else :jobs="jobs.list" />
<gl-pagination <gl-intersection-observer v-if="hasNextPage" @appear="fetchMoreJobs">
v-if="showPaginationControls" <gl-loading-icon
:value="pagination.currentPage" v-if="$apollo.loading"
:prev-page="prevPage" size="md"
:next-page="nextPage" :aria-label="$options.i18n.loadingAriaLabel"
align="center" />
class="gl-mt-3" </gl-intersection-observer>
@input="handlePageChange"
/>
</div> </div>
</template> </template>

View File

@ -25,6 +25,7 @@ class SearchController < ApplicationController
feature_category :global_search feature_category :global_search
urgency :high, [:opensearch] urgency :high, [:opensearch]
urgency :low, [:count]
def show def show
@project = search_service.project @project = search_service.project

View File

@ -60,8 +60,10 @@ class ApplicationRecord < ActiveRecord::Base
end end
# Start a new transaction with a shorter-than-usual statement timeout. This is # Start a new transaction with a shorter-than-usual statement timeout. This is
# currently one third of the default 15-second timeout # currently one third of the default 15-second timeout with a 500ms buffer
def self.with_fast_read_statement_timeout(timeout_ms = 5000) # to allow callers gracefully handling the errors to still complete within
# the 5s target duration of a low urgency request.
def self.with_fast_read_statement_timeout(timeout_ms = 4500)
::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
connection.exec_query("SET LOCAL statement_timeout = #{timeout_ms}") connection.exec_query("SET LOCAL statement_timeout = #{timeout_ms}")

View File

@ -2,7 +2,6 @@
module Gitlab module Gitlab
class BlamePresenter < Gitlab::View::Presenter::Simple class BlamePresenter < Gitlab::View::Presenter::Simple
include ActionView::Helpers::UrlHelper
include ActionView::Helpers::TranslationHelper include ActionView::Helpers::TranslationHelper
include ActionView::Context include ActionView::Context
include AvatarsHelper include AvatarsHelper
@ -75,5 +74,13 @@ module Gitlab
def project_duration def project_duration
@project_duration ||= age_map_duration(groups, project) @project_duration ||= age_map_duration(groups, project)
end end
def link_to(*args, &block)
ActionController::Base.helpers.link_to(*args, &block)
end
def mail_to(*args, &block)
ActionController::Base.helpers.mail_to(*args, &block)
end
end end
end end

View File

@ -435,7 +435,7 @@ list = arts.order(size: :desc).limit(50).each do |art|
end end
``` ```
To change the number of projects listed, change the number in `limit(50)`. To change the number of job artifacts listed, change the number in `limit(50)`.
#### Delete job artifacts from jobs completed before a specific date #### Delete job artifacts from jobs completed before a specific date

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

@ -11,6 +11,35 @@ This is the documentation of Alert Management Alerts API.
NOTE: NOTE:
This API is limited to metric images. For more API endpoints please refer to the [GraphQL API](graphql/reference/index.md#alertmanagementalert). This API is limited to metric images. For more API endpoints please refer to the [GraphQL API](graphql/reference/index.md#alertmanagementalert).
## Upload metric image
```plaintext
POST /projects/:id/alert_management_alerts/:alert_iid/metric_images
```
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. |
| `alert_iid` | integer | yes | The internal ID of a project's alert. |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form 'file=@/path/to/file.png' \
--form 'url=http://example.com' --form 'url_text=Example website' "https://gitlab.example.com/api/v4/projects/5/alert_management_alerts/93/metric_images"
```
Example response:
```json
{
"id": 17,
"created_at": "2020-11-12T20:07:58.156Z",
"filename": "sample_2054",
"file_path": "/uploads/-/system/alert_metric_image/file/17/sample_2054.png",
"url": "example.com/metric",
"url_text": "An example metric"
}
```
## List metric images ## List metric images
```plaintext ```plaintext
@ -48,3 +77,34 @@ Example response:
} }
] ]
``` ```
## Update metric image
```plaintext
PUT /projects/:id/alert_management_alerts/:alert_iid/metric_image/:image_id
```
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. |
| `alert_iid` | integer | yes | The internal ID of a project's alert. |
| `image_id` | integer | yes | The ID of the image. |
| `url` | string | no | The URL to view more metrics information. |
| `url_text` | string | no | A description of the image or URL. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" --request PUT --form 'url=http://example.com' --form 'url_text=Example website' "https://gitlab.example.com/api/v4/projects/5/alert_management_alerts/93/metric_images/1"
```
Example response:
```json
{
"id": 23,
"created_at": "2020-11-13T00:06:18.084Z",
"filename": "file.png",
"file_path": "/uploads/-/system/alert_metric_image/file/23/file.png",
"url": "http://example.com",
"url_text": "Example website"
}
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -47,7 +47,7 @@ Without mirroring, to work locally you must use `git pull` to update your local
with the upstream project, then push the changes back to your fork to update it. with the upstream project, then push the changes back to your fork to update it.
WARNING: WARNING:
With mirroring, before approving a merge request, you are asked to sync. Because of this, automating it is recommended. With mirroring, before approving a merge request, you are asked to sync. We recommend you automate it.
Read more about [How to keep your fork up to date with its origin](https://about.gitlab.com/blog/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/). Read more about [How to keep your fork up to date with its origin](https://about.gitlab.com/blog/2016/12/01/how-to-keep-your-fork-up-to-date-with-its-origin/).
@ -63,7 +63,7 @@ When creating a merge request, if the forked project's visibility is more restri
![Selecting branches](img/forking_workflow_branch_select.png) ![Selecting branches](img/forking_workflow_branch_select.png)
Then you can add labels, a milestone, and assign the merge request to someone who can review Then you can add labels, a milestone, and assign the merge request to someone who can review
your changes. Then click **Submit merge request** to conclude the process. When successfully merged, your your changes. Then select **Submit merge request** to conclude the process. When successfully merged, your
changes are added to the repository and branch you're merging into. changes are added to the repository and branch you're merging into.
## Removing a fork relationship ## Removing a fork relationship

View File

@ -229,7 +229,7 @@ and the exports between them are compatible.
## Related topics ## Related topics
- [Project import/export API](../../../api/project_import_export.md) - [Project import/export API](../../../api/project_import_export.md)
- [Project import/export administration Rake tasks](../../../administration/raketasks/project_import_export.md) **(FREE SELF)** - [Project import/export administration Rake tasks](../../../administration/raketasks/project_import_export.md)
- [Group import/export](../../group/settings/import_export.md) - [Group import/export](../../group/settings/import_export.md)
- [Group import/export API](../../../api/group_import_export.md) - [Group import/export API](../../../api/group_import_export.md)
@ -351,8 +351,8 @@ Rather than attempting to push all changes at once, this workaround:
git push -u origin ${COMMIT_SHA}:refs/heads/main git push -u origin ${COMMIT_SHA}:refs/heads/main
done done
git push -u origin main git push -u origin main
git push -u origin -all git push -u origin --all
git push -u origin -tags git push -u origin --tags
``` ```
### Manually execute export steps ### Manually execute export steps

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -94,7 +94,7 @@ module Gitlab
if schemas.many? if schemas.many?
message = "Cross-database data modification of '#{schemas.to_a.join(", ")}' were detected within " \ message = "Cross-database data modification of '#{schemas.to_a.join(", ")}' were detected within " \
"a transaction modifying the '#{all_tables.to_a.join(", ")}' tables." \ "a transaction modifying the '#{all_tables.to_a.join(", ")}' tables. " \
"Please refer to https://docs.gitlab.com/ee/development/database/multiple_databases.html#removing-cross-database-transactions for details on how to resolve this exception." "Please refer to https://docs.gitlab.com/ee/development/database/multiple_databases.html#removing-cross-database-transactions for details on how to resolve this exception."
if schemas.any? { |s| s.to_s.start_with?("undefined") } if schemas.any? { |s| s.to_s.start_with?("undefined") }

View File

@ -41693,6 +41693,9 @@ msgstr ""
msgid "You are not authorized to update this scanner profile" msgid "You are not authorized to update this scanner profile"
msgstr "" msgstr ""
msgid "You are not authorized to upload metric images"
msgstr ""
msgid "You are now impersonating %{username}" msgid "You are now impersonating %{username}"
msgstr "" msgstr ""

View File

@ -0,0 +1,51 @@
import cacheConfig from '~/jobs/components/table/graphql/cache_config';
import {
CIJobConnectionExistingCache,
CIJobConnectionIncomingCache,
CIJobConnectionIncomingCacheRunningStatus,
} from '../../../mock_data';
const firstLoadArgs = { first: 3, statuses: 'PENDING' };
const runningArgs = { first: 3, statuses: 'RUNNING' };
describe('jobs/components/table/graphql/cache_config', () => {
describe('when fetching data with the same statuses', () => {
it('should contain cache nodes and a status when merging caches on first load', () => {
const res = cacheConfig.typePolicies.CiJobConnection.merge({}, CIJobConnectionIncomingCache, {
args: firstLoadArgs,
});
expect(res.nodes).toHaveLength(CIJobConnectionIncomingCache.nodes.length);
expect(res.statuses).toBe('PENDING');
});
it('should add to existing caches when merging caches after first load', () => {
const res = cacheConfig.typePolicies.CiJobConnection.merge(
CIJobConnectionExistingCache,
CIJobConnectionIncomingCache,
{
args: firstLoadArgs,
},
);
expect(res.nodes).toHaveLength(
CIJobConnectionIncomingCache.nodes.length + CIJobConnectionExistingCache.nodes.length,
);
});
});
describe('when fetching data with different statuses', () => {
it('should reset cache when a cache already exists', () => {
const res = cacheConfig.typePolicies.CiJobConnection.merge(
CIJobConnectionExistingCache,
CIJobConnectionIncomingCacheRunningStatus,
{
args: runningArgs,
},
);
expect(res.nodes).not.toEqual(CIJobConnectionExistingCache.nodes);
expect(res.nodes).toHaveLength(CIJobConnectionIncomingCacheRunningStatus.nodes.length);
});
});
});

View File

@ -1,4 +1,4 @@
import { GlSkeletonLoader, GlAlert, GlEmptyState, GlPagination } from '@gitlab/ui'; import { GlSkeletonLoader, GlAlert, GlEmptyState, GlIntersectionObserver } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
@ -8,12 +8,7 @@ import getJobsQuery from '~/jobs/components/table/graphql/queries/get_jobs.query
import JobsTable from '~/jobs/components/table/jobs_table.vue'; import JobsTable from '~/jobs/components/table/jobs_table.vue';
import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue'; import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue';
import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue'; import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue';
import { import { mockJobsQueryResponse, mockJobsQueryEmptyResponse } from '../../mock_data';
mockJobsQueryResponse,
mockJobsQueryEmptyResponse,
mockJobsQueryResponseLastPage,
mockJobsQueryResponseFirstPage,
} from '../../mock_data';
const projectPath = 'gitlab-org/gitlab'; const projectPath = 'gitlab-org/gitlab';
Vue.use(VueApollo); Vue.use(VueApollo);
@ -30,10 +25,9 @@ describe('Job table app', () => {
const findTabs = () => wrapper.findComponent(JobsTableTabs); const findTabs = () => wrapper.findComponent(JobsTableTabs);
const findAlert = () => wrapper.findComponent(GlAlert); const findAlert = () => wrapper.findComponent(GlAlert);
const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findPagination = () => wrapper.findComponent(GlPagination);
const findPrevious = () => findPagination().findAll('.page-item').at(0); const triggerInfiniteScroll = () =>
const findNext = () => findPagination().findAll('.page-item').at(1); wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
const createMockApolloProvider = (handler) => { const createMockApolloProvider = (handler) => {
const requestHandlers = [[getJobsQuery, handler]]; const requestHandlers = [[getJobsQuery, handler]];
@ -53,7 +47,7 @@ describe('Job table app', () => {
}; };
}, },
provide: { provide: {
projectPath, fullPath: projectPath,
}, },
apolloProvider: createMockApolloProvider(handler), apolloProvider: createMockApolloProvider(handler),
}); });
@ -69,7 +63,6 @@ describe('Job table app', () => {
expect(findSkeletonLoader().exists()).toBe(true); expect(findSkeletonLoader().exists()).toBe(true);
expect(findTable().exists()).toBe(false); expect(findTable().exists()).toBe(false);
expect(findPagination().exists()).toBe(false);
}); });
}); });
@ -83,7 +76,6 @@ describe('Job table app', () => {
it('should display the jobs table with data', () => { it('should display the jobs table with data', () => {
expect(findTable().exists()).toBe(true); expect(findTable().exists()).toBe(true);
expect(findSkeletonLoader().exists()).toBe(false); expect(findSkeletonLoader().exists()).toBe(false);
expect(findPagination().exists()).toBe(true);
}); });
it('should refetch jobs query on fetchJobsByStatus event', async () => { it('should refetch jobs query on fetchJobsByStatus event', async () => {
@ -95,41 +87,24 @@ describe('Job table app', () => {
expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1); expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1);
}); });
});
describe('pagination', () => { describe('when infinite scrolling is triggered', () => {
it('should disable the next page button on the last page', async () => { beforeEach(() => {
createComponent({ triggerInfiniteScroll();
handler: jest.fn().mockResolvedValue(mockJobsQueryResponseLastPage),
mountFn: mount,
data: {
pagination: { currentPage: 3 },
},
}); });
await waitForPromises(); it('does not display a skeleton loader', () => {
expect(findSkeletonLoader().exists()).toBe(false);
expect(findPrevious().exists()).toBe(true);
expect(findNext().exists()).toBe(true);
expect(findNext().classes('disabled')).toBe(true);
});
it('should disable the previous page button on the first page', async () => {
createComponent({
handler: jest.fn().mockResolvedValue(mockJobsQueryResponseFirstPage),
mountFn: mount,
data: {
pagination: {
currentPage: 1,
},
},
}); });
await waitForPromises(); it('handles infinite scrolling by calling fetch more', async () => {
await waitForPromises();
expect(findPrevious().exists()).toBe(true); expect(successHandler).toHaveBeenCalledWith({
expect(findPrevious().classes('disabled')).toBe(true); after: 'eyJpZCI6IjIzMTcifQ',
expect(findNext().exists()).toBe(true); fullPath: 'gitlab-org/gitlab',
});
});
}); });
}); });

View File

@ -1579,44 +1579,6 @@ export const mockJobsQueryResponse = {
}, },
}; };
export const mockJobsQueryResponseLastPage = {
data: {
project: {
id: '1',
jobs: {
...mockJobsQueryResponse.data.project.jobs,
pageInfo: {
endCursor: 'eyJpZCI6IjIzMTcifQ',
hasNextPage: false,
hasPreviousPage: true,
startCursor: 'eyJpZCI6IjIzMzYifQ',
__typename: 'PageInfo',
},
},
__typename: 'Project',
},
},
};
export const mockJobsQueryResponseFirstPage = {
data: {
project: {
id: '1',
jobs: {
...mockJobsQueryResponse.data.project.jobs,
pageInfo: {
endCursor: 'eyJpZCI6IjIzMTcifQ',
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'eyJpZCI6IjIzMzYifQ',
__typename: 'PageInfo',
},
},
__typename: 'Project',
},
},
};
export const mockJobsQueryEmptyResponse = { export const mockJobsQueryEmptyResponse = {
data: { data: {
project: { project: {
@ -1910,3 +1872,44 @@ export const cannotPlayScheduledJob = {
__typename: 'JobPermissions', __typename: 'JobPermissions',
}, },
}; };
export const CIJobConnectionIncomingCache = {
__typename: 'CiJobConnection',
pageInfo: {
__typename: 'PageInfo',
endCursor: 'eyJpZCI6IjIwNTEifQ',
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'eyJpZCI6IjIxNzMifQ',
},
nodes: [
{ __ref: 'CiJob:gid://gitlab/Ci::Build/2057' },
{ __ref: 'CiJob:gid://gitlab/Ci::Build/2056' },
{ __ref: 'CiJob:gid://gitlab/Ci::Build/2051' },
],
};
export const CIJobConnectionIncomingCacheRunningStatus = {
__typename: 'CiJobConnection',
pageInfo: {
__typename: 'PageInfo',
endCursor: 'eyJpZCI6IjIwNTEifQ',
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'eyJpZCI6IjIxNzMifQ',
},
nodes: [
{ __ref: 'CiJob:gid://gitlab/Ci::Build/2000' },
{ __ref: 'CiJob:gid://gitlab/Ci::Build/2001' },
{ __ref: 'CiJob:gid://gitlab/Ci::Build/2002' },
],
};
export const CIJobConnectionExistingCache = {
nodes: [
{ __ref: 'CiJob:gid://gitlab/Ci::Build/2057' },
{ __ref: 'CiJob:gid://gitlab/Ci::Build/2056' },
{ __ref: 'CiJob:gid://gitlab/Ci::Build/2051' },
],
statuses: 'PENDING',
};

View File

@ -3,8 +3,6 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Groups::DestroyService do RSpec.describe Groups::DestroyService do
include DatabaseConnectionHelpers
let!(:user) { create(:user) } let!(:user) { create(:user) }
let!(:group) { create(:group) } let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) } let!(:nested_group) { create(:group, parent: group) }

View File

@ -1,11 +0,0 @@
# frozen_string_literal: true
module DatabaseConnectionHelpers
def run_with_new_database_connection
pool = ActiveRecord::Base.connection_pool
conn = pool.checkout
yield conn
ensure
pool.checkin(conn)
end
end

View File

@ -318,9 +318,12 @@ func configureRoutes(u *upstream) {
// Group Import via UI upload acceleration // Group Import via UI upload acceleration
u.route("POST", importPattern+`gitlab_group`, upload.Multipart(api, signingProxy, preparers.uploads)), u.route("POST", importPattern+`gitlab_group`, upload.Multipart(api, signingProxy, preparers.uploads)),
// Metric image upload // Issuable Metric image upload
u.route("POST", apiProjectPattern+`issues/[0-9]+/metric_images\z`, upload.Multipart(api, signingProxy, preparers.uploads)), u.route("POST", apiProjectPattern+`issues/[0-9]+/metric_images\z`, upload.Multipart(api, signingProxy, preparers.uploads)),
// Alert Metric image upload
u.route("POST", apiProjectPattern+`alert_management_alerts/[0-9]+/metric_images\z`, upload.Multipart(api, signingProxy, preparers.uploads)),
// Requirements Import via UI upload acceleration // Requirements Import via UI upload acceleration
u.route("POST", projectPattern+`requirements_management/requirements/import_csv`, upload.Multipart(api, signingProxy, preparers.uploads)), u.route("POST", projectPattern+`requirements_management/requirements/import_csv`, upload.Multipart(api, signingProxy, preparers.uploads)),

View File

@ -141,6 +141,7 @@ func TestAcceleratedUpload(t *testing.T) {
{"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/packages/pypi`, true}, {"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/packages/pypi`, true},
{"POST", `/api/v4/projects/9001/issues/30/metric_images`, true}, {"POST", `/api/v4/projects/9001/issues/30/metric_images`, true},
{"POST", `/api/v4/projects/group%2Fproject/issues/30/metric_images`, true}, {"POST", `/api/v4/projects/group%2Fproject/issues/30/metric_images`, true},
{"POST", `/api/v4/projects/9001/alert_management_alerts/30/metric_images`, true},
{"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/issues/30/metric_images`, true}, {"POST", `/api/v4/projects/group%2Fsubgroup%2Fproject/issues/30/metric_images`, true},
{"POST", `/my/project/-/requirements_management/requirements/import_csv`, true}, {"POST", `/my/project/-/requirements_management/requirements/import_csv`, true},
{"POST", `/my/project/-/requirements_management/requirements/import_csv/`, true}, {"POST", `/my/project/-/requirements_management/requirements/import_csv/`, true},