Commit f8e050cf authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'ensure-pages-metadatum' into 'master'

Ensure that pages metadata exists when updating it

See merge request gitlab-org/gitlab!46669
parents 75f58060 a7537882
...@@ -616,12 +616,12 @@ const Api = { ...@@ -616,12 +616,12 @@ const Api = {
return axios.get(url); return axios.get(url);
}, },
pipelineJobs(projectId, pipelineId) { pipelineJobs(projectId, pipelineId, params) {
const url = Api.buildUrl(this.pipelineJobsPath) const url = Api.buildUrl(this.pipelineJobsPath)
.replace(':id', encodeURIComponent(projectId)) .replace(':id', encodeURIComponent(projectId))
.replace(':pipeline_id', encodeURIComponent(pipelineId)); .replace(':pipeline_id', encodeURIComponent(pipelineId));
return axios.get(url); return axios.get(url, { params });
}, },
// Return all pipelines for a project or filter by query params // Return all pipelines for a project or filter by query params
......
...@@ -230,7 +230,13 @@ export default { ...@@ -230,7 +230,13 @@ export default {
:href="titleLink" :href="titleLink"
@click="handleFileNameClick" @click="handleFileNameClick"
> >
<file-icon :file-name="filePath" :size="18" aria-hidden="true" css-classes="gl-mr-2" /> <file-icon
:file-name="filePath"
:size="18"
aria-hidden="true"
css-classes="gl-mr-2"
:submodule="diffFile.submodule"
/>
<span v-if="isFileRenamed"> <span v-if="isFileRenamed">
<strong <strong
v-gl-tooltip v-gl-tooltip
......
...@@ -664,6 +664,7 @@ export const generateTreeList = files => { ...@@ -664,6 +664,7 @@ export const generateTreeList = files => {
addedLines: file.added_lines, addedLines: file.added_lines,
removedLines: file.removed_lines, removedLines: file.removed_lines,
parentPath: parent ? `${parent.path}/` : '/', parentPath: parent ? `${parent.path}/` : '/',
submodule: file.submodule,
}); });
} else { } else {
Object.assign(entry, { Object.assign(entry, {
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
* Used in environments table. * Used in environments table.
*/ */
import { GlTooltipDirective, GlButton } from '@gitlab/ui'; import { GlTooltipDirective, GlButton, GlModalDirective } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
...@@ -14,6 +14,7 @@ export default { ...@@ -14,6 +14,7 @@ export default {
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
GlModalDirective,
}, },
props: { props: {
environment: { environment: {
...@@ -54,14 +55,13 @@ export default { ...@@ -54,14 +55,13 @@ export default {
<template> <template>
<gl-button <gl-button
v-gl-tooltip="{ id: $options.stopEnvironmentTooltipId }" v-gl-tooltip="{ id: $options.stopEnvironmentTooltipId }"
v-gl-modal-directive="'stop-environment-modal'"
:loading="isLoading" :loading="isLoading"
:title="title" :title="title"
:aria-label="title" :aria-label="title"
icon="stop" icon="stop"
category="primary" category="primary"
variant="danger" variant="danger"
data-toggle="modal"
data-target="#stop-environment-modal"
@click="onClick" @click="onClick"
/> />
</template> </template>
<script> <script>
/* eslint-disable @gitlab/vue-require-i18n-strings */ import { GlSprintf, GlTooltipDirective, GlModal } from '@gitlab/ui';
import { GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { __, s__ } from '~/locale';
export default { export default {
id: 'stop-environment-modal', id: 'stop-environment-modal',
name: 'StopEnvironmentModal', name: 'StopEnvironmentModal',
components: { components: {
GlModal: DeprecatedModal2, GlModal,
GlSprintf, GlSprintf,
}, },
...@@ -24,6 +23,20 @@ export default { ...@@ -24,6 +23,20 @@ export default {
}, },
}, },
computed: {
primaryProps() {
return {
text: s__('Environments|Stop environment'),
attributes: [{ variant: 'danger' }],
};
},
cancelProps() {
return {
text: __('Cancel'),
};
},
},
methods: { methods: {
onSubmit() { onSubmit() {
eventHub.$emit('stopEnvironment', this.environment); eventHub.$emit('stopEnvironment', this.environment);
...@@ -34,18 +47,23 @@ export default { ...@@ -34,18 +47,23 @@ export default {
<template> <template>
<gl-modal <gl-modal
:id="$options.id" :modal-id="$options.id"
:footer-primary-button-text="s__('Environments|Stop environment')" :action-primary="primaryProps"
footer-primary-button-variant="danger" :action-cancel="cancelProps"
@submit="onSubmit" @primary="onSubmit"
>
<template #modal-title>
<gl-sprintf :message="s__('Environments|Stopping %{environmentName}')">
<template #environmentName>
<span
v-gl-tooltip
:title="environment.name"
class="gl-text-truncate gl-ml-2 gl-mr-2 gl-flex-fill"
> >
<template #header>
<h4 class="modal-title d-flex mw-100">
Stopping
<span v-gl-tooltip :title="environment.name" class="text-truncate ml-1 mr-1 flex-fill">
{{ environment.name }}? {{ environment.name }}?
</span> </span>
</h4> </template>
</gl-sprintf>
</template> </template>
<p>{{ s__('Environments|Are you sure you want to stop this environment?') }}</p> <p>{{ s__('Environments|Are you sure you want to stop this environment?') }}</p>
......
...@@ -143,7 +143,7 @@ export default { ...@@ -143,7 +143,7 @@ export default {
<div class="media-body"> <div class="media-body">
<div class="gl-ml-3 float-left"> <div class="gl-ml-3 float-left">
<span class="gl-font-weight-bold"> <span class="gl-font-weight-bold">
{{ __('This merge request is still a work in progress.') }} {{ __('This merge request is still a draft.') }}
</span> </span>
<span class="gl-display-block text-muted">{{ <span class="gl-display-block text-muted">{{
__("Draft merge requests can't be merged.") __("Draft merge requests can't be merged.")
......
...@@ -153,6 +153,7 @@ export default { ...@@ -153,6 +153,7 @@ export default {
:folder="isTree" :folder="isTree"
:opened="file.opened" :opened="file.opened"
:size="16" :size="16"
:submodule="file.submodule"
/> />
<gl-truncate v-if="truncateMiddle" :text="file.name" position="middle" class="gl-pr-7" /> <gl-truncate v-if="truncateMiddle" :text="file.name" position="middle" class="gl-pr-7" />
<template v-else>{{ file.name }}</template> <template v-else>{{ file.name }}</template>
......
...@@ -3,6 +3,7 @@ import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; ...@@ -3,6 +3,7 @@ import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import ReportSection from '~/reports/components/report_section.vue'; import ReportSection from '~/reports/components/report_section.vue';
import { status } from '~/reports/constants'; import { status } from '~/reports/constants';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import Flash from '~/flash'; import Flash from '~/flash';
import Api from '~/api'; import Api from '~/api';
...@@ -52,12 +53,27 @@ export default { ...@@ -52,12 +53,27 @@ export default {
}); });
}, },
methods: { methods: {
checkHasSecurityReports(reportTypes) { async checkHasSecurityReports(reportTypes) {
return Api.pipelineJobs(this.projectId, this.pipelineId).then(({ data: jobs }) => let page = 1;
jobs.some(({ artifacts = [] }) => while (page) {
// eslint-disable-next-line no-await-in-loop
const { data: jobs, headers } = await Api.pipelineJobs(this.projectId, this.pipelineId, {
per_page: 100,
page,
});
const hasSecurityReports = jobs.some(({ artifacts = [] }) =>
artifacts.some(({ file_type }) => reportTypes.includes(file_type)), artifacts.some(({ file_type }) => reportTypes.includes(file_type)),
),
); );
if (hasSecurityReports) {
return true;
}
page = parseIntPagination(normalizeHeaders(headers)).nextPage;
}
return false;
}, },
activatePipelinesTab() { activatePipelinesTab() {
if (window.mrTabs) { if (window.mrTabs) {
......
---
title: Add a /draft alias to the /wip quick action
merge_request: 46277
author:
type: added
---
title: Display submodules in MR tree and file header
merge_request: 46840
author:
type: fixed
---
title: Add usage ping for web users of geo secondaries
merge_request: 46278
author:
type: added
---
title: Ensure security report is displayed correctly in merge requests with a lot of CI jobs
merge_request: 46870
author:
type: fixed
---
name: gitlab_org_sitemap
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46661
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/276915
milestone: '13.6'
type: development
group: group::editor
default_enabled: false
...@@ -275,6 +275,10 @@ Rails.application.routes.draw do ...@@ -275,6 +275,10 @@ Rails.application.routes.draw do
draw :profile draw :profile
end end
Gitlab.ee do
get '/sitemap' => 'sitemap#show', format: :xml
end
root to: "root#index" root to: "root#index"
get '*unmatched_route', to: 'application#route_not_found' get '*unmatched_route', to: 'application#route_not_found'
......
# frozen_string_literal: true
class AddIndexToOauthAccessGrantsResourceOwnerId < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_oauth_access_grants_on_resource_owner_id'
disable_ddl_transaction!
def up
add_concurrent_index :oauth_access_grants, %i[resource_owner_id application_id created_at], name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :oauth_access_grants, INDEX_NAME
end
end
c269a999cabce99d26f3be303656bbb27f2b843b639755b112ad350d4cb5b5c6
\ No newline at end of file
...@@ -21257,6 +21257,8 @@ CREATE INDEX index_notification_settings_on_user_id ON notification_settings USI ...@@ -21257,6 +21257,8 @@ CREATE INDEX index_notification_settings_on_user_id ON notification_settings USI
CREATE UNIQUE INDEX index_notifications_on_user_id_and_source_id_and_source_type ON notification_settings USING btree (user_id, source_id, source_type); CREATE UNIQUE INDEX index_notifications_on_user_id_and_source_id_and_source_type ON notification_settings USING btree (user_id, source_id, source_type);
CREATE INDEX index_oauth_access_grants_on_resource_owner_id ON oauth_access_grants USING btree (resource_owner_id, application_id, created_at);
CREATE UNIQUE INDEX index_oauth_access_grants_on_token ON oauth_access_grants USING btree (token); CREATE UNIQUE INDEX index_oauth_access_grants_on_token ON oauth_access_grants USING btree (token);
CREATE INDEX index_oauth_access_tokens_on_application_id ON oauth_access_tokens USING btree (application_id); CREATE INDEX index_oauth_access_tokens_on_application_id ON oauth_access_tokens USING btree (application_id);
......
--- ---
stage: none stage: none
group: unassigned group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers"
description: 'Learn how to install, configure, update, and maintain your GitLab instance.' description: 'Learn how to install, configure, update, and maintain your GitLab instance.'
--- ---
......
...@@ -349,7 +349,9 @@ PUT /groups/:id/epics/:epic_iid ...@@ -349,7 +349,9 @@ PUT /groups/:id/epics/:epic_iid
| `title` | string | no | The title of an epic | | `title` | string | no | The title of an epic |
| `description` | string | no | The description of an epic. Limited to 1,048,576 characters. | | `description` | string | no | The description of an epic. Limited to 1,048,576 characters. |
| `confidential` | boolean | no | Whether the epic should be confidential | | `confidential` | boolean | no | Whether the epic should be confidential |
| `labels` | string | no | The comma separated list of labels | | `labels` | string | no | Comma-separated label names for an issue. Set to an empty string to unassign all labels. |
| `add_labels` | string | no | Comma-separated label names to add to an issue. |
| `remove_labels` | string | no | Comma-separated label names to remove from an issue. |
| `updated_at` | string | no | When the epic was updated. Date time string, ISO 8601 formatted, for example `2016-03-11T03:45:40Z` . Requires administrator or project/group owner privileges ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/255309) in GitLab 13.5) | | `updated_at` | string | no | When the epic was updated. Date time string, ISO 8601 formatted, for example `2016-03-11T03:45:40Z` . Requires administrator or project/group owner privileges ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/255309) in GitLab 13.5) |
| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) | | `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) | | `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
......
...@@ -411,6 +411,8 @@ job B: ...@@ -411,6 +411,8 @@ job B:
- cat vendor/hello.txt - cat vendor/hello.txt
cache: cache:
key: build-cache key: build-cache
paths:
- vendor/
``` ```
Here's what happens behind the scenes: Here's what happens behind the scenes:
......
---
description: "Internal users documentation."
type: concepts, reference, dev
stage: none
group: Development
info: "See the Technical Writers assigned to Development Guidelines: https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments-to-development-guidelines"
---
# Internal users
GitLab uses internal users (sometimes referred to as "bots") to perform
actions or functions that cannot be attributed to a regular user.
These users are created programatically throughout the codebase itself when
necessary, and do not count towards a license limit.
They are used when a traditional user account would not be applicable, for
example when generating alerts or automatic review feedback.
Technically, an internal user is a type of user, but they have reduced access
and a very specific purpose. They cannot be used for regular user actions,
such as authentication or API requests.
They have email addresses and names which can be attributed to any actions
they perform.
For example, when we [migrated](https://gitlab.com/gitlab-org/gitlab/-/issues/216120)
GitLab Snippets to [Versioned Snippets](../user/snippets.md#versioned-snippets)
in GitLab 13.0, we used an internal user to attribute the authorship of
snippets to itself when a snippet's author wasn't available for creating
repository commits, such as when the user has been disabled, so the Migration
Bot was used instead.
For this bot:
- The name was set to `GitLab Migration Bot`.
- The email was set to `noreply+gitlab-migration-bot@{instance host}`.
Other examples of internal users:
- [Alert Bot](../operations/metrics/alerts.md#trigger-actions-from-alerts)
- [Ghost User](../user/profile/account/delete_account.md#associated-records)
- [Support Bot](../user/project/service_desk.md#support-bot-user)
- Visual Review Bot
...@@ -72,6 +72,17 @@ With GitLab Enterprise Edition, you can also: ...@@ -72,6 +72,17 @@ With GitLab Enterprise Edition, you can also:
You can also [integrate](project/integrations/overview.md) GitLab with numerous third-party applications, such as Mattermost, Microsoft Teams, HipChat, Trello, Slack, Bamboo CI, Jira, and a lot more. You can also [integrate](project/integrations/overview.md) GitLab with numerous third-party applications, such as Mattermost, Microsoft Teams, HipChat, Trello, Slack, Bamboo CI, Jira, and a lot more.
## User types
There are several types of users in GitLab:
- Regular users and GitLab.com users. <!-- Note: further description TBA -->
- [Groups](group/index.md) of users.
- GitLab [admin area](admin_area/index.md) user.
- [GitLab Administrator](../administration/index.md) with full access to
self-managed instances' features and settings.
- [Internal users](../development/internal_users.md).
## Projects ## Projects
In GitLab, you can create [projects](project/index.md) to host In GitLab, you can create [projects](project/index.md) to host
......
...@@ -27,7 +27,7 @@ There are several ways to flag a merge request as a Draft: ...@@ -27,7 +27,7 @@ There are several ways to flag a merge request as a Draft:
description will have the same effect. description will have the same effect.
- **Deprecated** Add `[WIP]` or `WIP:` to the start of the merge request's title. - **Deprecated** Add `[WIP]` or `WIP:` to the start of the merge request's title.
**WIP** still works but was deprecated in favor of **Draft**. It will be removed in the next major version (GitLab 14.0). **WIP** still works but was deprecated in favor of **Draft**. It will be removed in the next major version (GitLab 14.0).
- Add the `/wip` [quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics) - Add the `/draft` (or `/wip`) [quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics)
in a comment in the merge request. This is a toggle, and can be repeated in a comment in the merge request. This is a toggle, and can be repeated
to change the status back. Note that any other text in the comment will be discarded. to change the status back. Note that any other text in the comment will be discarded.
- Add `draft:`, `Draft:`, `fixup!`, or `Fixup!` to the beginning of a commit message targeting the - Add `draft:`, `Draft:`, `fixup!`, or `Fixup!` to the beginning of a commit message targeting the
...@@ -43,7 +43,7 @@ Similar to above, when a Merge Request is ready to be merged, you can remove the ...@@ -43,7 +43,7 @@ Similar to above, when a Merge Request is ready to be merged, you can remove the
- Remove `[Draft]`, `Draft:` or `(Draft)` from the start of the merge request's title. Clicking on - Remove `[Draft]`, `Draft:` or `(Draft)` from the start of the merge request's title. Clicking on
**Remove the Draft: prefix from the title**, under the title box, when editing the merge **Remove the Draft: prefix from the title**, under the title box, when editing the merge
request's description, will have the same effect. request's description, will have the same effect.
- Add the `/wip` [quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics) - Add the `/draft` (or `/wip`) [quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics)
in a comment in the merge request. This is a toggle, and can be repeated in a comment in the merge request. This is a toggle, and can be repeated
to change the status back. Note that any other text in the comment will be discarded. to change the status back. Note that any other text in the comment will be discarded.
- Click on the **Resolve Draft status** button near the bottom of the merge request description, - Click on the **Resolve Draft status** button near the bottom of the merge request description,
......
...@@ -40,6 +40,7 @@ The following quick actions are applicable to descriptions, discussions and thre ...@@ -40,6 +40,7 @@ The following quick actions are applicable to descriptions, discussions and thre
| `/copy_metadata <#issue>` | ✓ | ✓ | | Copy labels and milestone from another issue in the project. | | `/copy_metadata <#issue>` | ✓ | ✓ | | Copy labels and milestone from another issue in the project. |
| `/create_merge_request <branch name>` | ✓ | | | Create a new merge request starting from the current issue. | | `/create_merge_request <branch name>` | ✓ | | | Create a new merge request starting from the current issue. |
| `/done` | ✓ | ✓ | ✓ | Mark to do as done. | | `/done` | ✓ | ✓ | ✓ | Mark to do as done. |
| `/draft` | | ✓ | | Toggle the draft status. |
| `/due <date>` | ✓ | | | Set due date. Examples of valid `<date>` include `in 2 days`, `this Friday` and `December 31st`. | | `/due <date>` | ✓ | | | Set due date. Examples of valid `<date>` include `in 2 days`, `this Friday` and `December 31st`. |
| `/duplicate <#issue>` | ✓ | | | Close this issue and mark as a duplicate of another issue. **(CORE)** Also, mark both as related. **(STARTER)** | | `/duplicate <#issue>` | ✓ | | | Close this issue and mark as a duplicate of another issue. **(CORE)** Also, mark both as related. **(STARTER)** |
| `/epic <epic>` | ✓ | | | Add to epic `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic. **(PREMIUM)** | | `/epic <epic>` | ✓ | | | Add to epic `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic. **(PREMIUM)** |
...@@ -82,7 +83,7 @@ The following quick actions are applicable to descriptions, discussions and thre ...@@ -82,7 +83,7 @@ The following quick actions are applicable to descriptions, discussions and thre
| `/unlock` | ✓ | ✓ | | Unlock the discussions. | | `/unlock` | ✓ | ✓ | | Unlock the discussions. |
| `/unsubscribe` | ✓ | ✓ | ✓ | Unsubscribe from notifications. | | `/unsubscribe` | ✓ | ✓ | ✓ | Unsubscribe from notifications. |
| `/weight <value>` | ✓ | | | Set weight. Valid options for `<value>` include `0`, `1`, `2`, and so on. **(STARTER)** | | `/weight <value>` | ✓ | | | Set weight. Valid options for `<value>` include `0`, `1`, `2`, and so on. **(STARTER)** |
| `/wip` | | ✓ | | Toggle the Work In Progress status. | | `/wip` | | ✓ | | Toggle the draft status. |
| `/zoom <Zoom URL>` | ✓ | | | Add Zoom meeting to this issue ([introduced in GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16609)). | | `/zoom <Zoom URL>` | ✓ | | | Add Zoom meeting to this issue ([introduced in GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16609)). |
## Autocomplete characters ## Autocomplete characters
......
...@@ -59,13 +59,13 @@ export default { ...@@ -59,13 +59,13 @@ export default {
<span class="gl-font-weight-bold gl-mr-3">{{ $options.i18n.lastUpdated }}</span> <span class="gl-font-weight-bold gl-mr-3">{{ $options.i18n.lastUpdated }}</span>
<span class="gl-white-space-nowrap"> <span class="gl-white-space-nowrap">
<time-ago-tooltip class="gl-pr-3" :time="pipeline.createdAt" /> <time-ago-tooltip class="gl-pr-3" :time="pipeline.createdAt" />
<gl-link :href="pipeline.path" target="_blank">#{{ pipeline.id }}</gl-link> <gl-link :href="pipeline.path">#{{ pipeline.id }}</gl-link>
<pipeline-status-badge :pipeline="pipeline" class="gl-ml-3" /> <pipeline-status-badge :pipeline="pipeline" class="gl-ml-3" />
</span> </span>
</div> </div>
<div v-if="autoFixMrsCount" data-testid="auto-fix-mrs-link"> <div v-if="autoFixMrsCount" data-testid="auto-fix-mrs-link">
<span class="gl-font-weight-bold gl-mr-3">{{ $options.i18n.autoFixSolutions }}</span> <span class="gl-font-weight-bold gl-mr-3">{{ $options.i18n.autoFixSolutions }}</span>
<gl-link :href="autoFixMrsPath" target="_blank" class="gl-white-space-nowrap">{{ <gl-link :href="autoFixMrsPath" class="gl-white-space-nowrap">{{
sprintf($options.i18n.autoFixMrsLink, { mrsCount: autoFixMrsCount }) sprintf($options.i18n.autoFixMrsLink, { mrsCount: autoFixMrsCount })
}}</gl-link> }}</gl-link>
</div> </div>
......
# frozen_string_literal: true
class SitemapController < ApplicationController
skip_before_action :authenticate_user!
feature_category :metrics
def show
return render_404 unless Gitlab.com?
return render_404 unless Feature.enabled?(:gitlab_org_sitemap)
respond_to do |format|
format.xml do
response = Sitemap::CreateService.new.execute
xml_data = if response.success?
response.payload[:sitemap]
else
xml_error(response.message)
end
render inline: xml_data
end
end
end
private
def xml_error(message)
xml_builder = Builder::XmlMarkup.new(indent: 2)
xml_builder.instruct!
xml_builder.error message
end
end
# frozen_string_literal: true
module Sitemap
class CreateService
def execute
result = Gitlab::Sitemaps::Generator.execute
if result.is_a?(String)
error_response(result)
else
success_response(result)
end
end
private
def success_response(file)
Gitlab::AppLogger.info("Sitemap generated successfully")
ServiceResponse.success(payload: { sitemap: file.render } )
end
def error_response(message)
Gitlab::AppLogger.error("Sitemap error creating sitemap: #{message}")
ServiceResponse.error(
message: message
)
end
end
end
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- if message.present? && subscribable.present? - if message.present? && subscribable.present?
.container-fluid.container-limited.pt-3 .container-fluid.container-limited.pt-3
.gl-alert.alert-dismissible.gitlab-ee-license-banner.hidden.js-gitlab-ee-license-banner.pb-5.border-width-1px.border-style-solid.border-color-default.border-radius-default{ role: 'alert', data: { license_expiry: subscribable.expires_at } } .gl-alert.alert-dismissible.gitlab-ee-license-banner.hidden.js-gitlab-ee-license-banner.gl-pb-7.gl-border-1.gl-border-solid.gl-border-gray-100.gl-rounded-base{ role: 'alert', data: { license_expiry: subscribable.expires_at } }
%button.close.p-2{ type: 'button', 'aria-label' => 'Dismiss banner', data: { dismiss: 'alert', track_event: 'click_button', track_label: 'dismiss_subscribable_banner' } } %button.close.p-2{ type: 'button', 'aria-label' => 'Dismiss banner', data: { dismiss: 'alert', track_event: 'click_button', track_label: 'dismiss_subscribable_banner' } }
%span{ 'aria-hidden' => 'true' } %span{ 'aria-hidden' => 'true' }
= sprite_icon('merge-request-close-m', size: 24) = sprite_icon('merge-request-close-m', size: 24)
......
---
title: Ooen pipeline status widget links in the same tab
merge_request: 46893
author:
type: changed
---
title: Add add/remove label helpers to Epic API
merge_request: 40465
author:
type: added
---
title: Generate dynamically sitemap through controller
merge_request: 46661
author:
type: changed
...@@ -108,8 +108,10 @@ module API ...@@ -108,8 +108,10 @@ module API
optional :end_date, as: :due_date_fixed, type: String, desc: 'The due date of an epic' optional :end_date, as: :due_date_fixed, type: String, desc: 'The due date of an epic'
optional :due_date_is_fixed, type: Boolean, desc: 'Indicates due date should be sourced from due_date_fixed field not the issue milestones' optional :due_date_is_fixed, type: Boolean, desc: 'Indicates due date should be sourced from due_date_fixed field not the issue milestones'
optional :labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' optional :labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
optional :add_labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
optional :remove_labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
optional :state_event, type: String, values: %w[reopen close], desc: 'State event for an epic' optional :state_event, type: String, values: %w[reopen close], desc: 'State event for an epic'
at_least_one_of :title, :description, :start_date_fixed, :start_date_is_fixed, :due_date_fixed, :due_date_is_fixed, :labels, :state_event, :confidential at_least_one_of :title, :description, :start_date_fixed, :start_date_is_fixed, :due_date_fixed, :due_date_is_fixed, :labels, :add_labels, :remove_labels, :state_event, :confidential
end end
put ':id/(-/)epics/:epic_iid' do put ':id/(-/)epics/:epic_iid' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/issues/194104') Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/issues/194104')
......
...@@ -274,6 +274,22 @@ module EE ...@@ -274,6 +274,22 @@ module EE
}, approval_rules_counts) }, approval_rules_counts)
end end
override :usage_activity_by_stage_enablement
def usage_activity_by_stage_enablement(time_period)
return super unless ::Gitlab::Geo.enabled?
super.merge({
geo_secondary_web_oauth_users: distinct_count(
OauthAccessGrant
.where(time_period)
.where(
application_id: GeoNode.secondary_nodes.select(:oauth_application_id)
),
:resource_owner_id
)
})
end
# Omitted because no user, creator or author associated: `campaigns_imported_from_github`, `ldap_group_links` # Omitted because no user, creator or author associated: `campaigns_imported_from_github`, `ldap_group_links`
override :usage_activity_by_stage_manage override :usage_activity_by_stage_manage
def usage_activity_by_stage_manage(time_period) def usage_activity_by_stage_manage(time_period)
......
...@@ -10,19 +10,22 @@ module Gitlab ...@@ -10,19 +10,22 @@ module Gitlab
def execute def execute
unless Gitlab.com? unless Gitlab.com?
return "The sitemap can only be generated for Gitlab.com" return 'The sitemap can only be generated for Gitlab.com'
end end
file = Sitemaps::SitemapFile.new file = Sitemaps::SitemapFile.new
if gitlab_org_group return "The group '#{GITLAB_ORG_NAMESPACE}' was not found" unless gitlab_org_group
file.add_elements(generic_urls) file.add_elements(generic_urls)
file.add_elements(gitlab_org_group) file.add_elements(gitlab_org_group)
file.add_elements(gitlab_org_subgroups) file.add_elements(gitlab_org_subgroups)
file.add_elements(gitlab_org_projects) file.add_elements(gitlab_org_projects)
file.save
if file.empty?
'No urls found to generate the sitemap'
else else
"The group '#{GITLAB_ORG_NAMESPACE}' was not found" file
end end
end end
...@@ -37,7 +40,7 @@ module Gitlab ...@@ -37,7 +40,7 @@ module Gitlab
end end
def gitlab_org_group def gitlab_org_group
@gitlab_org_group ||= GroupFinder.new(nil).execute(path: 'gitlab-org', parent_id: nil, visibility_level: Gitlab::VisibilityLevel::PUBLIC) @gitlab_org_group ||= GroupFinder.new(nil).execute(path: GITLAB_ORG_NAMESPACE, parent_id: nil, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
end end
def gitlab_org_subgroups def gitlab_org_subgroups
......
...@@ -20,17 +20,23 @@ module Gitlab ...@@ -20,17 +20,23 @@ module Gitlab
end end
def save def save
return if urls.empty? return if empty?
File.write(SITEMAP_FILE_PATH, render) File.write(SITEMAP_FILE_PATH, render)
end end
def render def render
return if empty?
fragment = File.read(File.expand_path("fragments/sitemap_file.xml.builder", __dir__)) fragment = File.read(File.expand_path("fragments/sitemap_file.xml.builder", __dir__))
instance_eval fragment instance_eval fragment
end end
def empty?
urls.empty?
end
private private
def xml_builder def xml_builder
......
# frozen_string_literal: true # frozen_string_literal: true
# Generating the urls for the project and groups is the most
# expensive part of the sitemap generation because we need
# to call the Rails route helpers.
#
# We could hardcode them but if a route changes the sitemap
# urls will be invalid.
module Gitlab module Gitlab
module Sitemaps module Sitemaps
class UrlExtractor class UrlExtractor
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe SitemapController do
describe '#show' do
subject { get :show, format: :xml }
before do
allow(Gitlab).to receive(:com?).and_return(dot_com)
end
context 'when not Gitlab.com?' do
let(:dot_com) { false }
it 'returns :not_found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when Gitlab.com?' do
let(:dot_com) { true }
context 'with an authenticated user' do
let(:flag_value) { true }
before do
stub_feature_flags(gitlab_org_sitemap: flag_value)
allow(Sitemap::CreateService).to receive_message_chain(:new, :execute).and_return(result)
subject
end
shared_examples 'gitlab_org_sitemap flag is disabled' do
let(:flag_value) { false }
it 'returns :not_found' do
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when the sitemap generation raises an error' do
let(:result) { ServiceResponse.error(message: 'foo') }
it 'returns an xml error' do
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to include('<error>foo</error>')
end
it_behaves_like 'gitlab_org_sitemap flag is disabled'
end
context 'when the sitemap was created suscessfully' do
let(:result) { ServiceResponse.success(payload: { sitemap: 'foo' }) }
it 'returns sitemap' do
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to eq('foo')
end
it_behaves_like 'gitlab_org_sitemap flag is disabled'
end
end
end
end
end
...@@ -374,6 +374,22 @@ RSpec.describe Gitlab::UsageData do ...@@ -374,6 +374,22 @@ RSpec.describe Gitlab::UsageData do
end end
end end
describe 'usage_data_by_stage_enablement' do
it 'returns empty hash if geo is not enabled' do
expect(described_class.usage_activity_by_stage_enablement({})).to eq({})
end
it 'excludes data outside of the date range' do
create_list(:geo_node, 2).each do |node|
for_defined_days_back do
create(:oauth_access_grant, application: node.oauth_application)
end
end
expect(described_class.usage_activity_by_stage_enablement(described_class.last_28_days_time_period)).to eq(geo_secondary_web_oauth_users: 2)
end
end
describe 'usage_activity_by_stage_manage' do describe 'usage_activity_by_stage_manage' do
it 'includes accurate usage_activity_by_stage data' do it 'includes accurate usage_activity_by_stage data' do
stub_config( stub_config(
......
...@@ -42,12 +42,7 @@ RSpec.describe Gitlab::Sitemaps::Generator do ...@@ -42,12 +42,7 @@ RSpec.describe Gitlab::Sitemaps::Generator do
let_it_be(:internal_subgroup_internal_project) { create(:project, :internal, namespace: internal_subgroup) } let_it_be(:internal_subgroup_internal_project) { create(:project, :internal, namespace: internal_subgroup) }
it 'includes default explore routes and gitlab-org group routes' do it 'includes default explore routes and gitlab-org group routes' do
new_path = Rails.root.join('tmp/tests/sitemap.xml') content = subject.render
stub_const('Gitlab::Sitemaps::SitemapFile::SITEMAP_FILE_PATH', new_path)
subject
content = File.read(new_path)
expect(content).to include('/explore/projects') expect(content).to include('/explore/projects')
expect(content).to include('/explore/groups') expect(content).to include('/explore/groups')
...@@ -63,8 +58,6 @@ RSpec.describe Gitlab::Sitemaps::Generator do ...@@ -63,8 +58,6 @@ RSpec.describe Gitlab::Sitemaps::Generator do
expect(content).not_to include(public_subgroup_internal_project.full_path) expect(content).not_to include(public_subgroup_internal_project.full_path)
expect(content).not_to include(internal_subgroup_private_project.full_path) expect(content).not_to include(internal_subgroup_private_project.full_path)
expect(content).not_to include(internal_subgroup_internal_project.full_path) expect(content).not_to include(internal_subgroup_internal_project.full_path)
File.delete(new_path)
end end
end end
end end
......
...@@ -10,6 +10,12 @@ RSpec.describe Gitlab::Sitemaps::SitemapFile do ...@@ -10,6 +10,12 @@ RSpec.describe Gitlab::Sitemaps::SitemapFile do
end end
describe '#render' do describe '#render' do
it 'returns if no elements has been provided' do
expect(File).not_to receive(:read)
described_class.new.save # rubocop: disable Rails/SaveBang
end
it 'generates a valid sitemap file' do it 'generates a valid sitemap file' do
freeze_time do freeze_time do
content = subject.render content = subject.render
......
...@@ -7,6 +7,7 @@ RSpec.describe API::Epics do ...@@ -7,6 +7,7 @@ RSpec.describe API::Epics do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:project) { create(:project, :public, group: group) } let(:project) { create(:project, :public, group: group) }
let(:label) { create(:group_label, group: group) } let(:label) { create(:group_label, group: group) }
let(:label2) { create(:group_label, group: group, title: 'label-2') }
let!(:epic) { create(:labeled_epic, group: group, labels: [label]) } let!(:epic) { create(:labeled_epic, group: group, labels: [label]) }
let(:params) { nil } let(:params) { nil }
...@@ -61,6 +62,13 @@ RSpec.describe API::Epics do ...@@ -61,6 +62,13 @@ RSpec.describe API::Epics do
end end
end end
shared_context 'with labels' do
before do
create(:label_link, label: label, target: epic)
create(:label_link, label: label2, target: epic)
end
end
describe 'GET /groups/:id/epics' do describe 'GET /groups/:id/epics' do
let(:url) { "/groups/#{group.path}/epics" } let(:url) { "/groups/#{group.path}/epics" }
let(:params) { { include_descendant_groups: true } } let(:params) { { include_descendant_groups: true } }
...@@ -762,6 +770,9 @@ RSpec.describe API::Epics do ...@@ -762,6 +770,9 @@ RSpec.describe API::Epics do
expect(json_response['labels']).to be_empty expect(json_response['labels']).to be_empty
end end
context 'with labels' do
include_context 'with labels'
it 'updates the epic with labels param as array' do it 'updates the epic with labels param as array' do
stub_const("Gitlab::QueryLimiting::Transaction::THRESHOLD", 110) stub_const("Gitlab::QueryLimiting::Transaction::THRESHOLD", 110)
...@@ -780,6 +791,28 @@ RSpec.describe API::Epics do ...@@ -780,6 +791,28 @@ RSpec.describe API::Epics do
expect(json_response['labels']).to include '?' expect(json_response['labels']).to include '?'
end end
it 'when adding labels, keeps existing labels and adds new' do
put api(url, user), params: { add_labels: '1, 2' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to contain_exactly(label.title, label2.title, '1', '2')
end
it 'when removing labels, only removes those specified' do
put api(url, user), params: { remove_labels: label.title }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to eq([label2.title])
end
it 'when removing all labels, keeps no labels' do
put api(url, user), params: { remove_labels: "#{label.title}, #{label2.title}" }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to be_empty
end
end
context 'when state_event is close' do context 'when state_event is close' do
it 'allows epic to be closed' do it 'allows epic to be closed' do
put api(url, user), params: { state_event: 'close' } put api(url, user), params: { state_event: 'close' }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sitemap::CreateService do
describe '#execute' do
subject { described_class.new.execute}
it 'returns the successful service response with the sitemap content' do
sitemap_file = Gitlab::Sitemaps::SitemapFile.new
allow(sitemap_file).to receive(:render).and_return('foo')
allow(Gitlab::Sitemaps::Generator).to receive(:execute).and_return(sitemap_file)
expect(subject).to be_success
expect(subject.payload[:sitemap]).to eq 'foo'
end
context 'when the sitemap generator returns an error' do
it 'returns an error service response' do
allow(Gitlab).to receive(:com?).and_return(false)
expect(subject).to be_error
expect(subject.message).to eq 'The sitemap can only be generated for Gitlab.com'
end
end
end
end
...@@ -20,6 +20,7 @@ module Atlassian ...@@ -20,6 +20,7 @@ module Atlassian
commits: commits, commits: commits,
branches: branches, branches: branches,
merge_requests: merge_requests, merge_requests: merge_requests,
user_notes_count: user_notes_count(merge_requests),
update_sequence_id: update_sequence_id update_sequence_id: update_sequence_id
) )
] ]
...@@ -37,6 +38,14 @@ module Atlassian ...@@ -37,6 +38,14 @@ module Atlassian
private private
def user_notes_count(merge_requests)
return unless merge_requests
Note.count_for_collection(merge_requests.map(&:id), 'MergeRequest').map do |count_group|
[count_group.noteable_id, count_group.count]
end.to_h
end
def jwt_token(http_method, uri) def jwt_token(http_method, uri)
claims = Atlassian::Jwt.build_claims( claims = Atlassian::Jwt.build_claims(
Atlassian::JiraConnect.app_key, Atlassian::JiraConnect.app_key,
......
...@@ -20,7 +20,13 @@ module Atlassian ...@@ -20,7 +20,13 @@ module Atlassian
end end
expose :title expose :title
expose :author, using: JiraConnect::Serializers::AuthorEntity expose :author, using: JiraConnect::Serializers::AuthorEntity
expose :user_notes_count, as: :commentCount expose :commentCount do |mr|
if options[:user_notes_count]
options[:user_notes_count].fetch(mr.id, 0)
else
mr.user_notes_count
end
end
expose :source_branch, as: :sourceBranch expose :source_branch, as: :sourceBranch
expose :target_branch, as: :destinationBranch expose :target_branch, as: :destinationBranch
expose :lastUpdate do |mr| expose :lastUpdate do |mr|
......
...@@ -21,7 +21,11 @@ module Atlassian ...@@ -21,7 +21,11 @@ module Atlassian
JiraConnect::Serializers::BranchEntity.represent options[:branches], project: project, update_sequence_id: options[:update_sequence_id] JiraConnect::Serializers::BranchEntity.represent options[:branches], project: project, update_sequence_id: options[:update_sequence_id]
end end
expose :pullRequests do |project, options| expose :pullRequests do |project, options|
JiraConnect::Serializers::PullRequestEntity.represent options[:merge_requests], project: project, update_sequence_id: options[:update_sequence_id] JiraConnect::Serializers::PullRequestEntity.represent(
options[:merge_requests],
update_sequence_id: options[:update_sequence_id],
user_notes_count: options[:user_notes_count]
)
end end
end end
end end
......
...@@ -56,21 +56,21 @@ module Gitlab ...@@ -56,21 +56,21 @@ module Gitlab
@updates[:merge] = params[:merge_request_diff_head_sha] @updates[:merge] = params[:merge_request_diff_head_sha]
end end
desc 'Toggle the Work In Progress status' desc 'Toggle the Draft status'
explanation do explanation do
noun = quick_action_target.to_ability_name.humanize(capitalize: false) noun = quick_action_target.to_ability_name.humanize(capitalize: false)
if quick_action_target.work_in_progress? if quick_action_target.work_in_progress?
_("Unmarks this %{noun} as Work In Progress.") _("Unmarks this %{noun} as a draft.")
else else
_("Marks this %{noun} as Work In Progress.") _("Marks this %{noun} as a draft.")
end % { noun: noun } end % { noun: noun }
end end
execution_message do execution_message do
noun = quick_action_target.to_ability_name.humanize(capitalize: false) noun = quick_action_target.to_ability_name.humanize(capitalize: false)
if quick_action_target.work_in_progress? if quick_action_target.work_in_progress?
_("Unmarked this %{noun} as Work In Progress.") _("Unmarked this %{noun} as a draft.")
else else
_("Marked this %{noun} as Work In Progress.") _("Marked this %{noun} as a draft.")
end % { noun: noun } end % { noun: noun }
end end
...@@ -80,7 +80,7 @@ module Gitlab ...@@ -80,7 +80,7 @@ module Gitlab
# Allow it to mark as WIP on MR creation page _or_ through MR notes. # Allow it to mark as WIP on MR creation page _or_ through MR notes.
(quick_action_target.new_record? || current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target)) (quick_action_target.new_record? || current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target))
end end
command :wip do command :draft, :wip do
@updates[:wip_event] = quick_action_target.work_in_progress? ? 'unwip' : 'wip' @updates[:wip_event] = quick_action_target.work_in_progress? ? 'unwip' : 'wip'
end end
......
...@@ -527,6 +527,7 @@ module Gitlab ...@@ -527,6 +527,7 @@ module Gitlab
key => { key => {
configure: usage_activity_by_stage_configure(time_period), configure: usage_activity_by_stage_configure(time_period),
create: usage_activity_by_stage_create(time_period), create: usage_activity_by_stage_create(time_period),
enablement: usage_activity_by_stage_enablement(time_period),
manage: usage_activity_by_stage_manage(time_period), manage: usage_activity_by_stage_manage(time_period),
monitor: usage_activity_by_stage_monitor(time_period), monitor: usage_activity_by_stage_monitor(time_period),
package: usage_activity_by_stage_package(time_period), package: usage_activity_by_stage_package(time_period),
...@@ -582,6 +583,11 @@ module Gitlab ...@@ -582,6 +583,11 @@ module Gitlab
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
# Empty placeholder allows this to match the pattern used by other sections
def usage_activity_by_stage_enablement(time_period)
{}
end
# Omitted because no user, creator or author associated: `campaigns_imported_from_github`, `ldap_group_links` # Omitted because no user, creator or author associated: `campaigns_imported_from_github`, `ldap_group_links`
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def usage_activity_by_stage_manage(time_period) def usage_activity_by_stage_manage(time_period)
......
...@@ -10451,6 +10451,9 @@ msgstr "" ...@@ -10451,6 +10451,9 @@ msgstr ""
msgid "Environments|Stopping" msgid "Environments|Stopping"
msgstr "" msgstr ""
msgid "Environments|Stopping %{environmentName}"
msgstr ""
msgid "Environments|There was an error fetching the logs. Please try again." msgid "Environments|There was an error fetching the logs. Please try again."
msgstr "" msgstr ""
...@@ -16305,7 +16308,7 @@ msgstr "" ...@@ -16305,7 +16308,7 @@ msgstr ""
msgid "Marked For Deletion At - %{deletion_time}" msgid "Marked For Deletion At - %{deletion_time}"
msgstr "" msgstr ""
msgid "Marked this %{noun} as Work In Progress." msgid "Marked this %{noun} as a draft."
msgstr "" msgstr ""
msgid "Marked this issue as a duplicate of %{duplicate_param}." msgid "Marked this issue as a duplicate of %{duplicate_param}."
...@@ -16317,7 +16320,7 @@ msgstr "" ...@@ -16317,7 +16320,7 @@ msgstr ""
msgid "Marked to do as done." msgid "Marked to do as done."
msgstr "" msgstr ""
msgid "Marks this %{noun} as Work In Progress." msgid "Marks this %{noun} as a draft."
msgstr "" msgstr ""
msgid "Marks this issue as a duplicate of %{duplicate_reference}." msgid "Marks this issue as a duplicate of %{duplicate_reference}."
...@@ -27514,7 +27517,7 @@ msgstr "" ...@@ -27514,7 +27517,7 @@ msgstr ""
msgid "This merge request is locked." msgid "This merge request is locked."
msgstr "" msgstr ""
msgid "This merge request is still a work in progress." msgid "This merge request is still a draft."
msgstr "" msgstr ""
msgid "This merge request was merged. To apply this suggestion, edit this file directly." msgid "This merge request was merged. To apply this suggestion, edit this file directly."
...@@ -28694,10 +28697,10 @@ msgstr "" ...@@ -28694,10 +28697,10 @@ msgstr ""
msgid "Unlocks the discussion." msgid "Unlocks the discussion."
msgstr "" msgstr ""
msgid "Unmarked this %{noun} as Work In Progress." msgid "Unmarked this %{noun} as a draft."
msgstr "" msgstr ""
msgid "Unmarks this %{noun} as Work In Progress." msgid "Unmarks this %{noun} as a draft."
msgstr "" msgstr ""
msgid "Unreachable" msgid "Unreachable"
......
...@@ -50,3 +50,4 @@ UsageData/DistinctCountByLargeForeignKey: ...@@ -50,3 +50,4 @@ UsageData/DistinctCountByLargeForeignKey:
- 'owner_id' - 'owner_id'
- 'project_id' - 'project_id'
- 'user_id' - 'user_id'
- 'resource_owner_id'
...@@ -33,7 +33,7 @@ RSpec.describe 'Merge request > User resolves Work in Progress', :js do ...@@ -33,7 +33,7 @@ RSpec.describe 'Merge request > User resolves Work in Progress', :js do
it 'retains merge request data after clicking Resolve WIP status' do it 'retains merge request data after clicking Resolve WIP status' do
expect(page.find('.ci-widget-content')).to have_content("Pipeline ##{pipeline.id}") expect(page.find('.ci-widget-content')).to have_content("Pipeline ##{pipeline.id}")
expect(page).to have_content "This merge request is still a work in progress." expect(page).to have_content "This merge request is still a draft."
page.within('.mr-state-widget') do page.within('.mr-state-widget') do
click_button('Mark as ready') click_button('Mark as ready')
...@@ -45,7 +45,7 @@ RSpec.describe 'Merge request > User resolves Work in Progress', :js do ...@@ -45,7 +45,7 @@ RSpec.describe 'Merge request > User resolves Work in Progress', :js do
# merge request widget refreshes, which masks missing elements # merge request widget refreshes, which masks missing elements
# that should already be present. # that should already be present.
expect(page.find('.ci-widget-content', wait: 0)).to have_content("Pipeline ##{pipeline.id}") expect(page.find('.ci-widget-content', wait: 0)).to have_content("Pipeline ##{pipeline.id}")
expect(page).not_to have_content('This merge request is still a work in progress.') expect(page).not_to have_content('This merge request is still a draft.')
end end
end end
end end
...@@ -710,7 +710,9 @@ describe('Api', () => { ...@@ -710,7 +710,9 @@ describe('Api', () => {
}); });
describe('pipelineJobs', () => { describe('pipelineJobs', () => {
it('fetches the jobs for a given pipeline', done => { it.each([undefined, {}, { foo: true }])(
'fetches the jobs for a given pipeline given %p params',
async params => {
const projectId = 123; const projectId = 123;
const pipelineId = 456; const pipelineId = 456;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/pipelines/${pipelineId}/jobs`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/pipelines/${pipelineId}/jobs`;
...@@ -719,15 +721,12 @@ describe('Api', () => { ...@@ -719,15 +721,12 @@ describe('Api', () => {
name: 'test', name: 'test',
}, },
]; ];
mock.onGet(expectedUrl).reply(httpStatus.OK, payload); mock.onGet(expectedUrl, { params }).reply(httpStatus.OK, payload);
Api.pipelineJobs(projectId, pipelineId) const { data } = await Api.pipelineJobs(projectId, pipelineId, params);
.then(({ data }) => {
expect(data).toEqual(payload); expect(data).toEqual(payload);
}) },
.then(done) );
.catch(done.fail);
});
}); });
describe('createBranch', () => { describe('createBranch', () => {
......
...@@ -6,6 +6,7 @@ import { mockTracking, triggerEvent } from 'helpers/tracking_helper'; ...@@ -6,6 +6,7 @@ import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import diffDiscussionsMockData from '../mock_data/diff_discussions'; import diffDiscussionsMockData from '../mock_data/diff_discussions';
import { truncateSha } from '~/lib/utils/text_utility'; import { truncateSha } from '~/lib/utils/text_utility';
import { diffViewerModes } from '~/ide/constants'; import { diffViewerModes } from '~/ide/constants';
...@@ -207,6 +208,14 @@ describe('DiffFileHeader component', () => { ...@@ -207,6 +208,14 @@ describe('DiffFileHeader component', () => {
}); });
expect(findFileActions().exists()).toBe(false); expect(findFileActions().exists()).toBe(false);
}); });
it('renders submodule icon', () => {
createComponent({
diffFile: submoduleDiffFile,
});
expect(wrapper.find(FileIcon).props('submodule')).toBe(true);
});
}); });
describe('for any file', () => { describe('for any file', () => {
......
...@@ -84,7 +84,7 @@ describe('Wip', () => { ...@@ -84,7 +84,7 @@ describe('Wip', () => {
it('should have correct elements', () => { it('should have correct elements', () => {
expect(el.classList.contains('mr-widget-body')).toBeTruthy(); expect(el.classList.contains('mr-widget-body')).toBeTruthy();
expect(el.innerText).toContain('This merge request is still a work in progress.'); expect(el.innerText).toContain('This merge request is still a draft.');
expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy(); expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(el.querySelector('button').innerText).toContain('Merge'); expect(el.querySelector('button').innerText).toContain('Merge');
expect(el.querySelector('.js-remove-wip').innerText.replace(/\s\s+/g, ' ')).toContain( expect(el.querySelector('.js-remove-wip').innerText.replace(/\s\s+/g, ' ')).toContain(
......
...@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import FileRow from '~/vue_shared/components/file_row.vue'; import FileRow from '~/vue_shared/components/file_row.vue';
import FileHeader from '~/vue_shared/components/file_row_header.vue'; import FileHeader from '~/vue_shared/components/file_row_header.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import { escapeFileUrl } from '~/lib/utils/url_utility'; import { escapeFileUrl } from '~/lib/utils/url_utility';
describe('File row component', () => { describe('File row component', () => {
...@@ -151,4 +152,18 @@ describe('File row component', () => { ...@@ -151,4 +152,18 @@ describe('File row component', () => {
expect(wrapper.find('.file-row-name').classes()).toContain('font-weight-bold'); expect(wrapper.find('.file-row-name').classes()).toContain('font-weight-bold');
}); });
it('renders submodule icon', () => {
const submodule = true;
createComponent({
file: {
...file(),
submodule,
},
level: 0,
});
expect(wrapper.find(FileIcon).props('submodule')).toBe(submodule);
});
}); });
...@@ -5,7 +5,7 @@ import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_a ...@@ -5,7 +5,7 @@ import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_a
jest.mock('~/flash'); jest.mock('~/flash');
describe('Grouped security reports app', () => { describe('Security reports app', () => {
let wrapper; let wrapper;
let mrTabsMock; let mrTabsMock;
...@@ -21,6 +21,8 @@ describe('Grouped security reports app', () => { ...@@ -21,6 +21,8 @@ describe('Grouped security reports app', () => {
}); });
}; };
const anyParams = expect.any(Object);
const findPipelinesTabAnchor = () => wrapper.find('[data-testid="show-pipelines"]'); const findPipelinesTabAnchor = () => wrapper.find('[data-testid="show-pipelines"]');
const findHelpLink = () => wrapper.find('[data-testid="help"]'); const findHelpLink = () => wrapper.find('[data-testid="help"]');
const setupMrTabsMock = () => { const setupMrTabsMock = () => {
...@@ -43,10 +45,12 @@ describe('Grouped security reports app', () => { ...@@ -43,10 +45,12 @@ describe('Grouped security reports app', () => {
window.mrTabs = { tabShown: jest.fn() }; window.mrTabs = { tabShown: jest.fn() };
setupMockJobArtifact(reportType); setupMockJobArtifact(reportType);
createComponent(); createComponent();
return wrapper.vm.$nextTick();
}); });
it('calls the pipelineJobs API correctly', () => { it('calls the pipelineJobs API correctly', () => {
expect(Api.pipelineJobs).toHaveBeenCalledWith(props.projectId, props.pipelineId); expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
expect(Api.pipelineJobs).toHaveBeenCalledWith(props.projectId, props.pipelineId, anyParams);
}); });
it('renders the expected message', () => { it('renders the expected message', () => {
...@@ -75,10 +79,12 @@ describe('Grouped security reports app', () => { ...@@ -75,10 +79,12 @@ describe('Grouped security reports app', () => {
beforeEach(() => { beforeEach(() => {
setupMockJobArtifact('foo'); setupMockJobArtifact('foo');
createComponent(); createComponent();
return wrapper.vm.$nextTick();
}); });
it('calls the pipelineJobs API correctly', () => { it('calls the pipelineJobs API correctly', () => {
expect(Api.pipelineJobs).toHaveBeenCalledWith(props.projectId, props.pipelineId); expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
expect(Api.pipelineJobs).toHaveBeenCalledWith(props.projectId, props.pipelineId, anyParams);
}); });
it('renders nothing', () => { it('renders nothing', () => {
...@@ -86,6 +92,42 @@ describe('Grouped security reports app', () => { ...@@ -86,6 +92,42 @@ describe('Grouped security reports app', () => {
}); });
}); });
describe('security artifacts on last page of multi-page response', () => {
const numPages = 3;
beforeEach(() => {
jest
.spyOn(Api, 'pipelineJobs')
.mockImplementation(async (projectId, pipelineId, { page }) => {
const requestedPage = parseInt(page, 10);
if (requestedPage < numPages) {
return {
// Some jobs with no relevant artifacts
data: [{}, {}],
headers: { 'x-next-page': String(requestedPage + 1) },
};
} else if (requestedPage === numPages) {
return {
data: [{ artifacts: [{ file_type: SecurityReportsApp.reportTypes[0] }] }],
};
}
throw new Error('Test failed due to request of non-existent jobs page');
});
createComponent();
return wrapper.vm.$nextTick();
});
it('fetches all pages', () => {
expect(Api.pipelineJobs).toHaveBeenCalledTimes(numPages);
});
it('renders the expected message', () => {
expect(wrapper.text()).toMatchInterpolatedText(SecurityReportsApp.i18n.scansHaveRun);
});
});
describe('given an error from the API', () => { describe('given an error from the API', () => {
let error; let error;
...@@ -93,10 +135,12 @@ describe('Grouped security reports app', () => { ...@@ -93,10 +135,12 @@ describe('Grouped security reports app', () => {
error = new Error('an error'); error = new Error('an error');
jest.spyOn(Api, 'pipelineJobs').mockRejectedValue(error); jest.spyOn(Api, 'pipelineJobs').mockRejectedValue(error);
createComponent(); createComponent();
return wrapper.vm.$nextTick();
}); });
it('calls the pipelineJobs API correctly', () => { it('calls the pipelineJobs API correctly', () => {
expect(Api.pipelineJobs).toHaveBeenCalledWith(props.projectId, props.pipelineId); expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
expect(Api.pipelineJobs).toHaveBeenCalledWith(props.projectId, props.pipelineId, anyParams);
}); });
it('renders nothing', () => { it('renders nothing', () => {
......
...@@ -20,8 +20,11 @@ RSpec.describe Atlassian::JiraConnect::Client do ...@@ -20,8 +20,11 @@ RSpec.describe Atlassian::JiraConnect::Client do
end end
describe '#store_dev_info' do describe '#store_dev_info' do
it "calls the API with auth headers" do let_it_be(:project) { create_default(:project, :repository) }
expected_jwt = Atlassian::Jwt.encode( let_it_be(:merge_requests) { create_list(:merge_request, 2, :unique_branches) }
let(:expected_jwt) do
Atlassian::Jwt.encode(
Atlassian::Jwt.build_claims( Atlassian::Jwt.build_claims(
Atlassian::JiraConnect.app_key, Atlassian::JiraConnect.app_key,
'/rest/devinfo/0.10/bulk', '/rest/devinfo/0.10/bulk',
...@@ -29,7 +32,9 @@ RSpec.describe Atlassian::JiraConnect::Client do ...@@ -29,7 +32,9 @@ RSpec.describe Atlassian::JiraConnect::Client do
), ),
'sample_secret' 'sample_secret'
) )
end
before do
stub_full_request('https://gitlab-test.atlassian.net/rest/devinfo/0.10/bulk', method: :post) stub_full_request('https://gitlab-test.atlassian.net/rest/devinfo/0.10/bulk', method: :post)
.with( .with(
headers: { headers: {
...@@ -37,8 +42,18 @@ RSpec.describe Atlassian::JiraConnect::Client do ...@@ -37,8 +42,18 @@ RSpec.describe Atlassian::JiraConnect::Client do
'Content-Type' => 'application/json' 'Content-Type' => 'application/json'
} }
) )
end
it "calls the API with auth headers" do
subject.store_dev_info(project: project)
end
it 'avoids N+1 database queries' do
control_count = ActiveRecord::QueryRecorder.new { subject.store_dev_info(project: project, merge_requests: merge_requests) }.count
merge_requests << create(:merge_request, :unique_branches)
subject.store_dev_info(project: create(:project)) expect { subject.store_dev_info(project: project, merge_requests: merge_requests) }.not_to exceed_query_limit(control_count)
end end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Atlassian::JiraConnect::Serializers::PullRequestEntity do
let_it_be(:project) { create_default(:project, :repository) }
let_it_be(:merge_requests) { create_list(:merge_request, 2, :unique_branches) }
let_it_be(:notes) { create_list(:note, 2, system: false, noteable: merge_requests.first) }
subject { described_class.represent(merge_requests).as_json }
it 'exposes commentCount' do
expect(subject.first[:commentCount]).to eq(2)
end
context 'with user_notes_count option' do
let(:user_notes_count) { merge_requests.map { |merge_request| [merge_request.id, 1] }.to_h }
subject { described_class.represent(merge_requests, user_notes_count: user_notes_count).as_json }
it 'avoids N+1 database queries' do
control_count = ActiveRecord::QueryRecorder.new do
described_class.represent(merge_requests, user_notes_count: user_notes_count)
end.count
merge_requests << create(:merge_request, :unique_branches)
expect { subject }.not_to exceed_query_limit(control_count)
end
it 'uses counts from user_notes_count' do
expect(subject.map { |entity| entity[:commentCount] }).to match_array([1, 1, 1])
end
context 'when count is missing for some MRs' do
let(:user_notes_count) { [[merge_requests.last.id, 1]].to_h }
it 'uses 0 as default when count for the MR is not available' do
expect(subject.map { |entity| entity[:commentCount] }).to match_array([0, 0, 1])
end
end
end
end
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Ci::AppendBuildTraceService do RSpec.describe Ci::AppendBuildTraceService do
let(:project) { create(:project) } let_it_be(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, :running, pipeline: pipeline) } let_it_be(:build) { create(:ci_build, :running, pipeline: pipeline) }
before do before do
stub_feature_flags(ci_enable_live_trace: true) stub_feature_flags(ci_enable_live_trace: true)
......
...@@ -312,8 +312,8 @@ RSpec.describe QuickActions::InterpretService do ...@@ -312,8 +312,8 @@ RSpec.describe QuickActions::InterpretService do
end end
end end
shared_examples 'wip command' do shared_examples 'draft command' do
it 'returns wip_event: "wip" if content contains /wip' do it 'returns wip_event: "wip" if content contains /draft' do
_, updates, _ = service.execute(content, issuable) _, updates, _ = service.execute(content, issuable)
expect(updates).to eq(wip_event: 'wip') expect(updates).to eq(wip_event: 'wip')
...@@ -322,12 +322,12 @@ RSpec.describe QuickActions::InterpretService do ...@@ -322,12 +322,12 @@ RSpec.describe QuickActions::InterpretService do
it 'returns the wip message' do it 'returns the wip message' do
_, _, message = service.execute(content, issuable) _, _, message = service.execute(content, issuable)
expect(message).to eq("Marked this #{issuable.to_ability_name.humanize(capitalize: false)} as Work In Progress.") expect(message).to eq("Marked this #{issuable.to_ability_name.humanize(capitalize: false)} as a draft.")
end end
end end
shared_examples 'unwip command' do shared_examples 'undraft command' do
it 'returns wip_event: "unwip" if content contains /wip' do it 'returns wip_event: "unwip" if content contains /draft' do
issuable.update!(title: issuable.wip_title) issuable.update!(title: issuable.wip_title)
_, updates, _ = service.execute(content, issuable) _, updates, _ = service.execute(content, issuable)
...@@ -338,7 +338,7 @@ RSpec.describe QuickActions::InterpretService do ...@@ -338,7 +338,7 @@ RSpec.describe QuickActions::InterpretService do
issuable.update!(title: issuable.wip_title) issuable.update!(title: issuable.wip_title)
_, _, message = service.execute(content, issuable) _, _, message = service.execute(content, issuable)
expect(message).to eq("Unmarked this #{issuable.to_ability_name.humanize(capitalize: false)} as Work In Progress.") expect(message).to eq("Unmarked this #{issuable.to_ability_name.humanize(capitalize: false)} as a draft.")
end end
end end
...@@ -1026,16 +1026,26 @@ RSpec.describe QuickActions::InterpretService do ...@@ -1026,16 +1026,26 @@ RSpec.describe QuickActions::InterpretService do
let(:issuable) { issue } let(:issuable) { issue }
end end
it_behaves_like 'wip command' do it_behaves_like 'draft command' do
let(:content) { '/wip' } let(:content) { '/wip' }
let(:issuable) { merge_request } let(:issuable) { merge_request }
end end
it_behaves_like 'unwip command' do it_behaves_like 'undraft command' do
let(:content) { '/wip' } let(:content) { '/wip' }
let(:issuable) { merge_request } let(:issuable) { merge_request }
end end
it_behaves_like 'draft command' do
let(:content) { '/draft' }
let(:issuable) { merge_request }
end
it_behaves_like 'undraft command' do
let(:content) { '/draft' }
let(:issuable) { merge_request }
end
it_behaves_like 'empty command' do it_behaves_like 'empty command' do
let(:content) { '/remove_due_date' } let(:content) { '/remove_due_date' }
let(:issuable) { merge_request } let(:issuable) { merge_request }
...@@ -1896,13 +1906,13 @@ RSpec.describe QuickActions::InterpretService do ...@@ -1896,13 +1906,13 @@ RSpec.describe QuickActions::InterpretService do
end end
end end
describe 'wip command' do describe 'draft command' do
let(:content) { '/wip' } let(:content) { '/draft' }
it 'includes the new status' do it 'includes the new status' do
_, explanations = service.explain(content, merge_request) _, explanations = service.explain(content, merge_request)
expect(explanations).to eq(['Marks this merge request as Work In Progress.']) expect(explanations).to eq(['Marks this merge request as a draft.'])
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment