Add latest changes from gitlab-org/gitlab@master
|
@ -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
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -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
|
||||||
|
|
|
@ -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') => {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 527 KiB After Width: | Height: | Size: 78 KiB |
|
@ -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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 10 KiB |
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 35 KiB |
|
@ -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") }
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
};
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
|
@ -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
|
|
|
@ -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)),
|
||||||
|
|
||||||
|
|
|
@ -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},
|
||||||
|
|