Commit c2367afb authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 51a95129
Please view this file on the master branch, on stable branches it's out of date.
## 12.8.1
### Performance (1 change)
- Geo - Fix query to retrieve Job Artifacts when selective sync is disabled. !25388
## 12.8.0
### Removed (1 change)
......
......@@ -55,9 +55,9 @@ export default {
<template>
<section id="grafana" class="settings no-animate js-grafana-integration">
<div class="settings-header">
<h4 class="js-section-header">
<h3 class="js-section-header h4">
{{ s__('GrafanaIntegration|Grafana Authentication') }}
</h4>
</h3>
<gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
<p class="js-section-sub-header">
{{ s__('GrafanaIntegration|Embed Grafana charts in GitLab issues.') }}
......
......@@ -33,9 +33,9 @@ export default {
<template>
<section class="settings no-animate">
<div class="settings-header">
<h4 class="js-section-header">
<h3 class="js-section-header h4">
{{ s__('ExternalMetrics|External Dashboard') }}
</h4>
</h3>
<gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
<p class="js-section-sub-header">
{{
......
......@@ -13,19 +13,21 @@ export default () => {
});
}
const pipelineTabLink = document.querySelector('.js-pipeline-tab-link a');
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
const pipelineStatusUrl = `${document
.querySelector('.js-pipeline-tab-link a')
.getAttribute('href')}/status.json`;
// eslint-disable-next-line no-new
new Pipelines({
initTabs: true,
pipelineStatusUrl,
tabsOptions: {
action: controllerAction,
defaultAction: 'pipelines',
parentEl: '.pipelines-tabs',
},
});
if (pipelineTabLink) {
const pipelineStatusUrl = `${pipelineTabLink.getAttribute('href')}/status.json`;
// eslint-disable-next-line no-new
new Pipelines({
initTabs: true,
pipelineStatusUrl,
tabsOptions: {
action: controllerAction,
defaultAction: 'pipelines',
parentEl: '.pipelines-tabs',
},
});
}
};
import PersistentUserCallout from '~/persistent_user_callout';
document.addEventListener('DOMContentLoaded', () => {
const callout = document.querySelector('.js-webhooks-moved-alert');
PersistentUserCallout.factory(callout);
});
......@@ -95,14 +95,14 @@ export default () => {
},
});
const tabsElement = document.querySelector('.pipelines-tabs');
const testReportsEnabled =
window.gon && window.gon.features && window.gon.features.junitPipelineView;
if (testReportsEnabled) {
if (tabsElement && testReportsEnabled) {
const fetchReportsAction = 'fetchReports';
testReportsStore.dispatch('setEndpoint', dataset.testReportEndpoint);
const tabsElmement = document.querySelector('.pipelines-tabs');
const isTestTabActive = Boolean(
document.querySelector('.pipelines-tabs > li > a.test-tab.active'),
);
......@@ -113,11 +113,11 @@ export default () => {
const tabClickHandler = e => {
if (e.target.className === 'test-tab') {
testReportsStore.dispatch(fetchReportsAction);
tabsElmement.removeEventListener('click', tabClickHandler);
tabsElement.removeEventListener('click', tabClickHandler);
}
};
tabsElmement.addEventListener('click', tabClickHandler);
tabsElement.addEventListener('click', tabClickHandler);
}
// eslint-disable-next-line no-new
......
......@@ -76,7 +76,7 @@ export default {
<div v-else-if="shouldRenderSuccessState" class="js-success-state">
<release-block
v-for="(release, index) in releases"
:key="release.tag_name"
:key="release.tagName"
:release="release"
:class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }"
/>
......
......@@ -25,16 +25,16 @@ export default {
},
computed: {
evidenceTitle() {
return sprintf(__('%{tag}-evidence.json'), { tag: this.release.tag_name });
return sprintf(__('%{tag}-evidence.json'), { tag: this.release.tagName });
},
evidenceUrl() {
return this.release.assets && this.release.assets.evidence_file_path;
return this.release.assets && this.release.assets.evidenceFilePath;
},
shortSha() {
return truncateSha(this.sha);
},
sha() {
return this.release.evidence_sha;
return this.release.evidenceSha;
},
},
};
......
......@@ -38,13 +38,13 @@ export default {
},
computed: {
id() {
return slugify(this.release.tag_name);
return slugify(this.release.tagName);
},
assets() {
return this.release.assets || {};
},
hasEvidence() {
return Boolean(this.release.evidence_sha);
return Boolean(this.release.evidenceSha);
},
milestones() {
return this.release.milestones || [];
......@@ -102,7 +102,7 @@ export default {
<evidence-block v-if="hasEvidence && shouldShowEvidence" :release="release" />
<div ref="gfm-content" class="card-text prepend-top-default">
<div v-html="release.description_html"></div>
<div v-html="release.descriptionHtml"></div>
</div>
</div>
......@@ -110,11 +110,11 @@ export default {
v-if="shouldShowFooter"
class="card-footer"
:commit="release.commit"
:commit-path="release.commit_path"
:tag-name="release.tag_name"
:tag-path="release.tag_path"
:commit-path="release.commitPath"
:tag-name="release.tagName"
:tag-path="release.tagPath"
:author="release.author"
:released-at="release.released_at"
:released-at="release.releasedAt"
/>
</div>
</template>
......@@ -31,8 +31,8 @@ export default {
<template #user>
<user-avatar-link
class="prepend-left-4"
:link-href="author.web_url"
:img-src="author.avatar_url"
:link-href="author.webUrl"
:img-src="author.avatarUrl"
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
/>
......
......@@ -66,9 +66,9 @@ export default {
<icon ref="commitIcon" name="commit" class="mr-1" />
<div v-gl-tooltip.bottom :title="commit.title">
<gl-link v-if="commitPath" :href="commitPath">
{{ commit.short_id }}
{{ commit.shortId }}
</gl-link>
<span v-else>{{ commit.short_id }}</span>
<span v-else>{{ commit.shortId }}</span>
</div>
</div>
......@@ -100,8 +100,8 @@ export default {
<div v-if="author" class="d-flex">
<span class="text-secondary">{{ __('by') }}&nbsp;</span>
<user-avatar-link
:link-href="author.web_url"
:img-src="author.avatar_url"
:link-href="author.webUrl"
:img-src="author.avatarUrl"
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
tooltip-placement="bottom"
......
......@@ -20,10 +20,10 @@ export default {
},
computed: {
editLink() {
return this.release._links?.edit_url;
return this.release.Links?.editUrl;
},
selfLink() {
return this.release._links?.self;
return this.release.Links?.self;
},
},
};
......@@ -36,7 +36,7 @@ export default {
{{ release.name }}
</gl-link>
<template v-else>{{ release.name }}</template>
<gl-badge v-if="release.upcoming_release" variant="warning" class="align-middle">{{
<gl-badge v-if="release.upcomingRelease" variant="warning" class="align-middle">{{
__('Upcoming Release')
}}</gl-badge>
</h2>
......
......@@ -32,21 +32,21 @@ export default {
return this.release.commit || {};
},
commitUrl() {
return this.release.commit_path;
return this.release.commitPath;
},
hasAuthor() {
return Boolean(this.author);
},
releasedTimeAgo() {
return sprintf(__('released %{time}'), {
time: this.timeFormatted(this.release.released_at),
time: this.timeFormatted(this.release.releasedAt),
});
},
shouldRenderMilestones() {
return Boolean(this.release.milestones?.length);
},
tagUrl() {
return this.release.tag_path;
return this.release.tagPath;
},
},
};
......@@ -57,24 +57,24 @@ export default {
<div class="append-right-8">
<icon name="commit" class="align-middle" />
<gl-link v-if="commitUrl" v-gl-tooltip.bottom :title="commit.title" :href="commitUrl">
{{ commit.short_id }}
{{ commit.shortId }}
</gl-link>
<span v-else v-gl-tooltip.bottom :title="commit.title">{{ commit.short_id }}</span>
<span v-else v-gl-tooltip.bottom :title="commit.title">{{ commit.shortId }}</span>
</div>
<div class="append-right-8">
<icon name="tag" class="align-middle" />
<gl-link v-if="tagUrl" v-gl-tooltip.bottom :title="__('Tag')" :href="tagUrl">
{{ release.tag_name }}
{{ release.tagName }}
</gl-link>
<span v-else v-gl-tooltip.bottom :title="__('Tag')">{{ release.tag_name }}</span>
<span v-else v-gl-tooltip.bottom :title="__('Tag')">{{ release.tagName }}</span>
</div>
<release-block-milestones v-if="shouldRenderMilestones" :milestones="release.milestones" />
<div class="append-right-4">
&bull;
<span v-gl-tooltip.bottom :title="tooltipTitle(release.released_at)">
<span v-gl-tooltip.bottom :title="tooltipTitle(release.releasedAt)">
{{ releasedTimeAgo }}
</span>
</div>
......
......@@ -40,7 +40,7 @@ export default {
return Number.isNaN(percent) ? 0 : percent;
},
allIssueStats() {
return this.milestones.map(m => m.issue_stats || {});
return this.milestones.map(m => m.issueStats || {});
},
openIssuesCount() {
return this.allIssueStats.map(stats => stats.opened || 0).reduce(sumReducer);
......@@ -109,7 +109,7 @@ export default {
:key="milestone.id"
v-gl-tooltip
:title="milestone.description"
:href="milestone.web_url"
:href="milestone.webUrl"
class="append-right-4"
>
{{ milestone.title }}
......
......@@ -38,7 +38,7 @@ export default {
:key="milestone.id"
v-gl-tooltip
:title="milestone.description"
:href="milestone.web_url"
:href="milestone.webUrl"
class="mx-1 js-milestone-link"
>
{{ milestone.title }}
......
......@@ -22,8 +22,7 @@ export const fetchRelease = ({ dispatch, state }) => {
return api
.release(state.projectId, state.tagName)
.then(({ data: release }) => {
const camelCasedRelease = convertObjectPropsToCamelCase(release, { deep: true });
dispatch('receiveReleaseSuccess', camelCasedRelease);
dispatch('receiveReleaseSuccess', convertObjectPropsToCamelCase(release, { deep: true }));
})
.catch(error => {
dispatch('receiveReleaseError', error);
......
......@@ -2,7 +2,11 @@ import * as types from './mutation_types';
import createFlash from '~/flash';
import { __ } from '~/locale';
import api from '~/api';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import {
normalizeHeaders,
parseIntPagination,
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
/**
* Commits a mutation to update the state while the main endpoint is being requested.
......@@ -28,7 +32,11 @@ export const fetchReleases = ({ dispatch }, { page = '1', projectId }) => {
export const receiveReleasesSuccess = ({ commit }, { data, headers }) => {
const pageInfo = parseIntPagination(normalizeHeaders(headers));
commit(types.RECEIVE_RELEASES_SUCCESS, { data, pageInfo });
const camelCasedReleases = convertObjectPropsToCamelCase(data, { deep: true });
commit(types.RECEIVE_RELEASES_SUCCESS, {
data: camelCasedReleases,
pageInfo,
});
};
export const receiveReleasesError = ({ commit }) => {
......
......@@ -12,7 +12,8 @@ class Projects::HooksController < Projects::ApplicationController
layout "project_settings"
def index
redirect_to project_settings_integrations_path(@project)
@hooks = @project.hooks
@hook = ProjectHook.new
end
def create
......@@ -24,7 +25,7 @@ class Projects::HooksController < Projects::ApplicationController
flash[:alert] = @hook.errors.full_messages.join.html_safe
end
redirect_to project_settings_integrations_path(@project)
redirect_to action: :index
end
def edit
......@@ -33,7 +34,7 @@ class Projects::HooksController < Projects::ApplicationController
def update
if hook.update(hook_params)
flash[:notice] = _('Hook was successfully updated.')
redirect_to project_settings_integrations_path(@project)
redirect_to action: :index
else
render 'edit'
end
......@@ -44,13 +45,13 @@ class Projects::HooksController < Projects::ApplicationController
set_hook_execution_notice(result)
redirect_back_or_default(default: { action: 'index' })
redirect_back_or_default(default: { action: :index })
end
def destroy
hook.destroy
redirect_to project_settings_integrations_path(@project), status: :found
redirect_to action: :index, status: :found
end
private
......
......@@ -9,10 +9,6 @@ module Projects
layout "project_settings"
def show
@hooks = @project.hooks
@hook = ProjectHook.new
# Services
@services = @project.find_or_initialize_services(exceptions: service_exceptions)
end
......
......@@ -4,7 +4,11 @@ module Resolvers
class BoardsResolver < BaseResolver
type Types::BoardType, null: true
def resolve(**args)
argument :id, GraphQL::ID_TYPE,
required: false,
description: 'Find a board by its ID'
def resolve(id: nil)
# The project or group could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` of the project/group to query for boards, so
# make sure it's loaded and not `nil` before continuing.
......@@ -12,7 +16,17 @@ module Resolvers
return Board.none unless parent
Boards::ListService.new(parent, context[:current_user]).execute(create_default_board: false)
Boards::ListService.new(parent, context[:current_user], board_id: extract_board_id(id)).execute(create_default_board: false)
rescue ActiveRecord::RecordNotFound
Board.none
end
private
def extract_board_id(gid)
return unless gid.present?
GitlabSchema.parse_gid(gid, expected_type: ::Board).model_id
end
end
end
......@@ -52,6 +52,12 @@ module Types
null: true,
description: 'Boards of the group',
resolver: Resolvers::BoardsResolver
field :board,
Types::BoardType,
null: true,
description: 'A single board of the group',
resolver: Resolvers::BoardsResolver.single
end
end
......
......@@ -185,6 +185,12 @@ module Types
null: true,
description: 'Boards of the project',
resolver: Resolvers::BoardsResolver
field :board,
Types::BoardType,
null: true,
description: 'A single board of the project',
resolver: Resolvers::BoardsResolver.single
end
end
......
......@@ -669,6 +669,9 @@ module ProjectsHelper
project_members#index
integrations#show
services#edit
hooks#index
hooks#edit
hook_logs#show
repository#show
ci_cd#show
operations#show
......
......@@ -5,6 +5,7 @@ module UserCalloutsHelper
GCP_SIGNUP_OFFER = 'gcp_signup_offer'
SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'
TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight'
WEBHOOKS_MOVED = 'webhooks_moved'
def show_gke_cluster_integration_callout?(project)
can?(current_user, :create_cluster, project) &&
......@@ -33,6 +34,10 @@ module UserCalloutsHelper
current_user && !user_dismissed?(TABS_POSITION_HIGHLIGHT) && !Rails.env.test?
end
def show_webhooks_moved_alert?
!user_dismissed?(WEBHOOKS_MOVED)
end
private
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil)
......
......@@ -21,7 +21,7 @@ class ProjectHook < WebHook
validates :project, presence: true
def pluralized_name
_('Project Hooks')
_('Webhooks')
end
end
......
......@@ -15,7 +15,8 @@ module UserCalloutEnums
gcp_signup_offer: 2,
cluster_security_warning: 3,
suggest_popover_dismissed: 9,
tabs_position_highlight: 10
tabs_position_highlight: 10,
webhooks_moved: 13
}
end
end
......
......@@ -2,49 +2,35 @@
module ResourceEvents
class ChangeMilestoneService
attr_reader :resource, :user, :event_created_at, :resource_args
attr_reader :resource, :user, :event_created_at, :milestone
def initialize(resource:, user:, created_at: Time.now)
@resource = resource
@user = user
@event_created_at = created_at
@resource_args = {
user_id: user.id,
created_at: event_created_at
}
@milestone = resource&.milestone
end
def execute
args = build_resource_args
action = if milestone.nil?
:remove
else
:add
end
ResourceMilestoneEvent.create(build_resource_args)
record = args.merge(milestone_id: milestone&.id, action: ResourceMilestoneEvent.actions[action])
create_event(record)
resource.expire_note_etag_cache
end
private
def milestone
resource&.milestone
end
def create_event(record)
ResourceMilestoneEvent.create(record)
resource.expire_note_etag_cache
end
def build_resource_args
key = resource.class.name.underscore.foreign_key
action = milestone.blank? ? :remove : :add
key = resource.class.name.foreign_key
resource_args.merge(key => resource.id, state: ResourceMilestoneEvent.states[resource.state])
{
user_id: user.id,
created_at: event_created_at,
milestone_id: milestone&.id,
state: ResourceMilestoneEvent.states[resource.state],
action: ResourceMilestoneEvent.actions[action],
key => resource.id
}
end
end
end
......@@ -366,10 +366,14 @@
%span
= _('Members')
- if can_edit
= nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do
= nav_link(controller: [:integrations, :services]) do
= link_to project_settings_integrations_path(@project), title: _('Integrations'), data: { qa_selector: 'integrations_settings_link' } do
%span
= _('Integrations')
= nav_link(controller: [:hooks, :hook_logs]) do
= link_to project_hooks_path(@project), title: _('Webhooks'), data: { qa_selector: 'webhooks_settings_link' } do
%span
= _('Webhooks')
= nav_link(controller: :repository) do
= link_to project_settings_repository_path(@project), title: _('Repository') do
%span
......
- @content_class = 'limit-container-width' unless fluid_layout
- add_to_breadcrumbs _('Webhook Settings'), namespace_project_hooks_path
- page_title _('Webhook Logs')
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
......
- add_to_breadcrumbs _('ProjectService|Integrations'), namespace_project_settings_integrations_path
- page_title _('Edit Project Hook')
- @content_class = 'limit-container-width' unless fluid_layout
- add_to_breadcrumbs _('Webhook Settings'), namespace_project_hooks_path
- page_title _('Webhook')
.row.prepend-top-default
.col-lg-3
......
- @content_class = 'limit-container-width' unless fluid_layout
- breadcrumb_title _('Webhook Settings')
- page_title _('Webhooks')
.row.prepend-top-default
.col-lg-4
= render 'shared/web_hooks/title_and_docs', hook: @hook
......
.row.prepend-top-default.append-bottom-default
.row.prepend-top-default
.col-lg-4
%h4.prepend-top-0
= s_("ProjectService|Project services")
%p= s_("ProjectService|Project services allow you to integrate GitLab with other applications")
= _('Integrations')
%p= _('Integrations allow you to integrate GitLab with other applications')
.col-lg-8
%table.table
%colgroup
......
- breadcrumb_title @service.title
- add_to_breadcrumbs _('Integration Settings'), project_settings_integrations_path(@project)
- page_title @service.title, s_("ProjectService|Services")
- add_to_breadcrumbs(s_("ProjectService|Settings"), edit_project_path(@project))
- add_to_breadcrumbs(s_("ProjectService|Integrations"), project_settings_integrations_path(@project))
= render 'deprecated_message' if @service.deprecation_message
......
- @content_class = "limit-container-width" unless fluid_layout
- breadcrumb_title _("Integrations Settings")
- breadcrumb_title _('Integration Settings')
- page_title _('Integrations')
= render 'projects/hooks/index'
- if show_webhooks_moved_alert?
.gl-alert.gl-alert-info.js-webhooks-moved-alert.prepend-top-default{ role: 'alert', data: { feature_id: UserCalloutsHelper::WEBHOOKS_MOVED, dismiss_endpoint: user_callouts_path } }
= sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
%button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
= sprite_icon('close', size: 16, css_class: 'gl-icon')
.gl-alert-body
= _('Webhooks have moved. They can now be found under the Settings menu.')
.gl-alert-actions
= link_to _('Go to Webhooks'), project_hooks_path(@project), class: 'btn gl-alert-action btn-info new-gl-button'
= render 'projects/services/index'
......@@ -4,7 +4,7 @@
%section.settings.no-animate.js-error-tracking-settings
.settings-header
%h4
%h3{ :class => "h4" }
= _('Error Tracking')
%button.btn.js-settings-toggle{ type: 'button' }
= _('Expand')
......
......@@ -4,7 +4,7 @@
%section.settings.no-animate.qa-incident-management-settings
.settings-header
%h4= _('Incidents')
%h3{ :class => "h4" }= _('Incidents')
%button.btn.js-settings-toggle{ type: 'button' }
= _('Expand')
%p
......
......@@ -15,62 +15,62 @@
= form.check_box :push_events, class: 'form-check-input'
= form.label :push_events, class: 'list-label form-check-label ml-1' do
%strong Push events
%p.light.ml-1
= form.text_field :push_events_branch_filter, class: 'form-control', placeholder: 'Branch name or wildcard pattern to trigger on (leave blank for all)'
%p.text-muted.ml-1
This URL will be triggered by a push to the repository
= form.text_field :push_events_branch_filter, class: 'form-control', placeholder: 'Branch name or wildcard pattern to trigger on (leave blank for all)'
%li
= form.check_box :tag_push_events, class: 'form-check-input'
= form.label :tag_push_events, class: 'list-label form-check-label ml-1' do
%strong Tag push events
%p.light.ml-1
%p.text-muted.ml-1
This URL will be triggered when a new tag is pushed to the repository
%li
= form.check_box :note_events, class: 'form-check-input'
= form.label :note_events, class: 'list-label form-check-label ml-1' do
%strong Comments
%p.light.ml-1
%p.text-muted.ml-1
This URL will be triggered when someone adds a comment
%li
= form.check_box :confidential_note_events, class: 'form-check-input'
= form.label :confidential_note_events, class: 'list-label form-check-label ml-1' do
%strong Confidential Comments
%p.light.ml-1
%p.text-muted.ml-1
This URL will be triggered when someone adds a comment on a confidential issue
%li
= form.check_box :issues_events, class: 'form-check-input'
= form.label :issues_events, class: 'list-label form-check-label ml-1' do
%strong Issues events
%p.light.ml-1
%p.text-muted.ml-1
This URL will be triggered when an issue is created/updated/merged
%li
= form.check_box :confidential_issues_events, class: 'form-check-input'
= form.label :confidential_issues_events, class: 'list-label form-check-label ml-1' do
%strong Confidential Issues events
%p.light.ml-1
%p.text-muted.ml-1
This URL will be triggered when a confidential issue is created/updated/merged
%li
= form.check_box :merge_requests_events, class: 'form-check-input'
= form.label :merge_requests_events, class: 'list-label form-check-label ml-1' do
%strong Merge request events
%p.light.ml-1
%p.text-muted.ml-1
This URL will be triggered when a merge request is created/updated/merged
%li
= form.check_box :job_events, class: 'form-check-input'
= form.label :job_events, class: 'list-label form-check-label ml-1' do
%strong Job events
%p.light.ml-1
%p.text-muted.ml-1
This URL will be triggered when the job status changes
%li
= form.check_box :pipeline_events, class: 'form-check-input'
= form.label :pipeline_events, class: 'list-label form-check-label ml-1' do
%strong Pipeline events
%p.light.ml-1
%p.text-muted.ml-1
This URL will be triggered when the pipeline status changes
%li
= form.check_box :wiki_page_events, class: 'form-check-input'
= form.label :wiki_page_events, class: 'list-label form-check-label ml-1' do
%strong Wiki Page events
%p.light.ml-1
%p.text-muted.ml-1
This URL will be triggered when a wiki page is created/updated
.form-group
= form.label :enable_ssl_verification, 'SSL verification', class: 'label-bold checkbox'
......
---
title: Decouple Webhooks from Integrations within Project > Settings
merge_request: 23136
author:
type: changed
---
title: Allow group/project board to be queried by ID via GraphQL
merge_request: 24825
author:
type: added
---
title: Fix pipeline details page initialisation on invalid pipeline
merge_request: 25302
author: Fabio Huser
type: fixed
......@@ -2770,6 +2770,16 @@ type Group {
"""
avatarUrl: String
"""
A single board of the group
"""
board(
"""
Find a board by its ID
"""
id: ID
): Board
"""
Boards of the group
"""
......@@ -2789,6 +2799,11 @@ type Group {
"""
first: Int
"""
Find a board by its ID
"""
id: ID
"""
Returns the last _n_ elements from the list.
"""
......@@ -5254,6 +5269,16 @@ type Project {
"""
avatarUrl: String
"""
A single board of the project
"""
board(
"""
Find a board by its ID
"""
id: ID
): Board
"""
Boards of the project
"""
......@@ -5273,6 +5298,11 @@ type Project {
"""
first: Int
"""
Find a board by its ID
"""
id: ID
"""
Returns the last _n_ elements from the list.
"""
......
......@@ -368,10 +368,43 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "board",
"description": "A single board of the project",
"args": [
{
"name": "id",
"description": "Find a board by its ID",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "Board",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "boards",
"description": "Boards of the project",
"args": [
{
"name": "id",
"description": "Find a board by its ID",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
......@@ -3175,10 +3208,43 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "board",
"description": "A single board of the group",
"args": [
{
"name": "id",
"description": "Find a board by its ID",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "Board",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "boards",
"description": "Boards of the group",
"args": [
{
"name": "id",
"description": "Find a board by its ID",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
......
......@@ -426,6 +426,7 @@ Autogenerated return type of EpicTreeReorder
| --- | ---- | ---------- |
| `autoDevopsEnabled` | Boolean | Indicates whether Auto DevOps is enabled for all projects within this group |
| `avatarUrl` | String | Avatar URL of the group |
| `board` | Board | A single board of the group |
| `description` | String | Description of the namespace |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `emailsDisabled` | Boolean | Indicates if a group has email notifications disabled |
......@@ -801,6 +802,7 @@ Information about pagination in a connection.
| `archived` | Boolean | Indicates the archived status of the project |
| `autocloseReferencedIssues` | Boolean | Indicates if issues referenced by merge requests and commits within the default branch are closed automatically |
| `avatarUrl` | String | URL to avatar image file of the project |
| `board` | Board | A single board of the project |
| `containerRegistryEnabled` | Boolean | Indicates if the project stores Docker container images in a container registry |
| `createdAt` | Time | Timestamp of the project creation |
| `description` | String | Short description of the project |
......
......@@ -2,7 +2,7 @@
## Issue tracker guidelines
**[Search the issue tracker](https://gitlab.com/gitlab-org/gitlab-foss/issues)** for similar entries before
**[Search the issue tracker](https://gitlab.com/gitlab-org/gitlab/issues)** for similar entries before
submitting your own, there's a good chance somebody else had the same issue or
feature proposal. Show your support with an award emoji and/or join the
discussion.
......@@ -35,7 +35,7 @@ project.
## Labels
To allow for asynchronous issue handling, we use [milestones](https://gitlab.com/groups/gitlab-org/-/milestones)
and [labels](https://gitlab.com/gitlab-org/gitlab-foss/-/labels). Leads and product managers handle most of the
and [labels](https://gitlab.com/gitlab-org/gitlab/-/labels). Leads and product managers handle most of the
scheduling into milestones. Labelling is a task for everyone.
Most issues will have labels for at least one of the following:
......@@ -53,7 +53,7 @@ Most issues will have labels for at least one of the following:
- Severity: ~`S1`, `~S2`, `~S3`, `~S4`
All labels, their meaning and priority are defined on the
[labels page](https://gitlab.com/gitlab-org/gitlab-foss/-/labels).
[labels page](https://gitlab.com/gitlab-org/gitlab/-/labels).
If you come across an issue that has none of these, and you're allowed to set
labels, you can _always_ add the team and type, and often also the subject.
......@@ -372,14 +372,11 @@ A recent example of this was the issue for
## Feature proposals
To create a feature proposal for CE, open an issue on the
[issue tracker of CE](https://gitlab.com/gitlab-org/gitlab-foss/issues).
For feature proposals for EE, open an issue on the
[issue tracker of EE](https://gitlab.com/gitlab-org/gitlab/issues).
To create a feature proposal, open an issue on the
[issue tracker](https://gitlab.com/gitlab-org/gitlab/issues).
In order to help track the feature proposals, we have created a
[`feature`](https://gitlab.com/gitlab-org/gitlab-foss/issues?label_name=feature) label. For the time being, users that are not members
[`feature`](https://gitlab.com/gitlab-org/gitlab/issues?label_name=feature) label. For the time being, users that are not members
of the project cannot add labels. You can instead ask one of the [core team](https://about.gitlab.com/community/core-team/)
members to add the label ~feature to the issue or add the following
code snippet right after your description in a new line: `~feature`.
......@@ -441,7 +438,7 @@ addressed.
## Technical and UX debt
In order to track things that can be improved in GitLab's codebase,
we use the ~"technical debt" label in [GitLab's issue tracker](https://gitlab.com/gitlab-org/gitlab-foss/issues).
we use the ~"technical debt" label in [GitLab's issue tracker](https://gitlab.com/gitlab-org/gitlab/issues).
For missed user experience requirements, we use the ~"UX debt" label.
These labels should be added to issues that describe things that can be improved,
......
......@@ -472,7 +472,7 @@ end
```
If a computed update is needed, the value can be wrapped in `Arel.sql`, so Arel
treats it as an SQL literal. It's also a required deprecation for [Rails 6](https://gitlab.com/gitlab-org/gitlab-foss/issues/61451).
treats it as an SQL literal. It's also a required deprecation for [Rails 6](https://gitlab.com/gitlab-org/gitlab/issues/28497).
The below example is the same as the one above, but
the value is set to the product of the `bar` and `baz` columns:
......
......@@ -30,11 +30,11 @@ People are saying multiple inheritance is bad. Mixing multiple modules with
multiple instance variables scattering everywhere suffer from the same issue.
The same applies to `ActiveSupport::Concern`. See:
[Consider replacing concerns with dedicated classes & composition](
https://gitlab.com/gitlab-org/gitlab-foss/issues/23786)
https://gitlab.com/gitlab-org/gitlab/issues/16270)
There's also a similar idea:
[Use decorators and interface segregation to solve overgrowing models problem](
https://gitlab.com/gitlab-org/gitlab-foss/issues/13484)
https://gitlab.com/gitlab-org/gitlab/issues/14235)
Note that `included` doesn't solve the whole issue. They define the
dependencies, but they still allow each modules to talk implicitly via the
......
......@@ -25,7 +25,7 @@ by [`Namespaces#with_statistics`](https://gitlab.com/gitlab-org/gitlab/blob/4ab5
Additionally, the pattern that is currently used to update the project statistics
(the callback) doesn't scale adequately. It is currently one of the largest
[database queries transactions on production](https://gitlab.com/gitlab-org/gitlab-foss/issues/62488)
[database queries transactions on production](https://gitlab.com/gitlab-org/gitlab/issues/29070)
that takes the most time overall. We can't add one more query to it as
it will increase the transaction's length.
......@@ -142,7 +142,7 @@ but we refresh them through Sidekiq jobs and in different transactions:
1. Create a second table (`namespace_aggregation_schedules`) with two columns `id` and `namespace_id`.
1. Whenever the statistics of a project changes, insert a row into `namespace_aggregation_schedules`
- We don't insert a new row if there's already one related to the root namespace.
- Keeping in mind the length of the transaction that involves updating `project_statistics`(<https://gitlab.com/gitlab-org/gitlab-foss/issues/62488>), the insertion should be done in a different transaction and through a Sidekiq Job.
- Keeping in mind the length of the transaction that involves updating `project_statistics`(<https://gitlab.com/gitlab-org/gitlab/issues/29070>), the insertion should be done in a different transaction and through a Sidekiq Job.
1. After inserting the row, we schedule another worker to be executed async at two different moments:
- One enqueued for immediate execution and another one scheduled in `1.5h` hours.
- We only schedule the jobs, if we can obtain a `1.5h` lease on Redis on a key based on the root namespace ID.
......@@ -162,7 +162,7 @@ This implementation has the following benefits:
The only downside of this approach is that namespaces' statistics are updated up to `1.5` hours after the change is done,
which means there's a time window in which the statistics are inaccurate. Because we're still not
[enforcing storage limits](https://gitlab.com/gitlab-org/gitlab-foss/issues/30421), this is not a major problem.
[enforcing storage limits](https://gitlab.com/gitlab-org/gitlab/issues/17664), this is not a major problem.
## Conclusion
......
......@@ -45,7 +45,7 @@ They are available **per project** for GitLab Community Edition,
and **per project and per group** for **GitLab Enterprise Edition**.
Navigate to the webhooks page by going to your project's
**Settings ➔ Integrations**.
**Settings ➔ Webhooks**.
## Maximum number of webhooks (per tier)
......
......@@ -6,8 +6,8 @@ type: reference
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/15643) in GitLab 11.7.
GitLab supports using [Git push options](https://git-scm.com/docs/git-push#Documentation/git-push.txt--oltoptiongt)
to perform various actions at the same time as pushing changes.
GitLab supports using client-side [Git push options](https://git-scm.com/docs/git-push#Documentation/git-push.txt--oltoptiongt)
to perform various actions at the same time as pushing changes. Additionally, [Push Rules](https://docs.gitlab.com/ee/push_rules/push_rules.html) offer server-side control and enforcement options.
Currently, there are push options available for:
......
......@@ -6997,9 +6997,6 @@ msgstr ""
msgid "Edit Pipeline Schedule %{id}"
msgstr ""
msgid "Edit Project Hook"
msgstr ""
msgid "Edit Release"
msgstr ""
......@@ -9451,6 +9448,9 @@ msgstr ""
msgid "Go to %{link_to_google_takeout}."
msgstr ""
msgid "Go to Webhooks"
msgstr ""
msgid "Go to commits"
msgstr ""
......@@ -10607,10 +10607,13 @@ msgstr ""
msgid "Instance license"
msgstr ""
msgid "Integration Settings"
msgstr ""
msgid "Integrations"
msgstr ""
msgid "Integrations Settings"
msgid "Integrations allow you to integrate GitLab with other applications"
msgstr ""
msgid "Interested parties can even contribute by pushing commits if they want to."
......@@ -14780,9 +14783,6 @@ msgstr ""
msgid "Project Files"
msgstr ""
msgid "Project Hooks"
msgstr ""
msgid "Project ID"
msgstr ""
......@@ -14945,30 +14945,18 @@ msgstr ""
msgid "ProjectService|Comment will be posted on each event"
msgstr ""
msgid "ProjectService|Integrations"
msgstr ""
msgid "ProjectService|Last edit"
msgstr ""
msgid "ProjectService|Perform common operations on GitLab project: %{project_name}"
msgstr ""
msgid "ProjectService|Project services"
msgstr ""
msgid "ProjectService|Project services allow you to integrate GitLab with other applications"
msgstr ""
msgid "ProjectService|Service"
msgstr ""
msgid "ProjectService|Services"
msgstr ""
msgid "ProjectService|Settings"
msgstr ""
msgid "ProjectService|To set up this service:"
msgstr ""
......@@ -21811,6 +21799,15 @@ msgstr ""
msgid "WebIDE|Merge request"
msgstr ""
msgid "Webhook"
msgstr ""
msgid "Webhook Logs"
msgstr ""
msgid "Webhook Settings"
msgstr ""
msgid "Webhooks"
msgstr ""
......@@ -21820,6 +21817,9 @@ msgstr ""
msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
msgstr ""
msgid "Webhooks have moved. They can now be found under the Settings menu."
msgstr ""
msgid "Wednesday"
msgstr ""
......
......@@ -12,12 +12,11 @@ describe Projects::HooksController do
end
describe '#index' do
it 'redirects to settings/integrations page' do
get(:index, params: { namespace_id: project.namespace, project_id: project })
it 'renders index with 200 status code' do
get :index, params: { namespace_id: project.namespace, project_id: project }
expect(response).to redirect_to(
project_settings_integrations_path(project)
)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
end
......
......@@ -22,6 +22,7 @@ FactoryBot.define do
factory :ci_pipeline do
trait :invalid do
status { :failed }
yaml_errors { 'invalid YAML' }
failure_reason { :config_error }
end
......
......@@ -88,6 +88,7 @@ describe 'Project navbar' do
_('General'),
_('Members'),
_('Integrations'),
_('Webhooks'),
_('Repository'),
_('CI / CD'),
_('Operations'),
......
......@@ -1077,8 +1077,6 @@ describe 'Pipeline', :js do
end
context 'when pipeline has configuration errors' do
include_context 'pipeline builds'
let(:pipeline) do
create(:ci_pipeline,
:invalid,
......@@ -1119,6 +1117,10 @@ describe 'Pipeline', :js do
%Q{span[title="#{pipeline.present.failure_reason}"]})
end
end
it 'contains a pipeline header with title' do
expect(page).to have_content "Pipeline ##{pipeline.id}"
end
end
context 'when pipeline is stuck' do
......
......@@ -14,7 +14,7 @@ describe 'User views services' do
end
it 'shows the list of available services' do
expect(page).to have_content('Project services')
expect(page).to have_content('Integrations')
expect(page).to have_content('Campfire')
expect(page).to have_content('HipChat')
expect(page).to have_content('Assembla')
......
......@@ -35,7 +35,7 @@ describe 'Projects > Settings > For a forked project', :js do
end
it 'renders form for incident management' do
expect(page).to have_selector('h4', text: 'Incidents')
expect(page).to have_selector('h3', text: 'Incidents')
end
it 'sets correct default values' do
......
......@@ -2,11 +2,10 @@
require 'spec_helper'
describe 'Projects > Settings > Integration settings' do
describe 'Projects > Settings > Webhook Settings' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:role) { :developer }
let(:integrations_path) { project_settings_integrations_path(project) }
let(:webhooks_path) { project_hooks_path(project) }
before do
sign_in(user)
......@@ -17,7 +16,7 @@ describe 'Projects > Settings > Integration settings' do
let(:role) { :developer }
it 'to be disallowed to view' do
visit integrations_path
visit webhooks_path
expect(page.status_code).to eq(404)
end
......@@ -33,7 +32,7 @@ describe 'Projects > Settings > Integration settings' do
it 'show list of webhooks' do
hook
visit integrations_path
visit webhooks_path
expect(page.status_code).to eq(200)
expect(page).to have_content(hook.url)
......@@ -49,7 +48,7 @@ describe 'Projects > Settings > Integration settings' do
end
it 'create webhook' do
visit integrations_path
visit webhooks_path
fill_in 'hook_url', with: url
check 'Tag push events'
......@@ -68,7 +67,7 @@ describe 'Projects > Settings > Integration settings' do
it 'edit existing webhook' do
hook
visit integrations_path
visit webhooks_path
click_link 'Edit'
fill_in 'hook_url', with: url
......@@ -81,25 +80,25 @@ describe 'Projects > Settings > Integration settings' do
it 'test existing webhook', :js do
WebMock.stub_request(:post, hook.url)
visit integrations_path
visit webhooks_path
find('.hook-test-button.dropdown').click
click_link 'Push events'
expect(current_path).to eq(integrations_path)
expect(current_path).to eq(webhooks_path)
end
context 'delete existing webhook' do
it 'from webhooks list page' do
hook
visit integrations_path
visit webhooks_path
expect { click_link 'Delete' }.to change(ProjectHook, :count).by(-1)
end
it 'from webhook edit page' do
hook
visit integrations_path
visit webhooks_path
click_link 'Edit'
expect { click_link 'Delete' }.to change(ProjectHook, :count).by(-1)
......
......@@ -8,13 +8,13 @@ exports[`grafana integration component default state to match the default snapsh
<div
class="settings-header"
>
<h4
class="js-section-header"
<h3
class="js-section-header h4"
>
Grafana Authentication
</h4>
</h3>
<gl-button-stub
class="js-settings-toggle"
......
......@@ -22,7 +22,7 @@ import {
} from '../mock_data';
const localVue = createLocalVue();
const expectedPanelCount = 2;
const expectedPanelCount = 3;
describe('Dashboard', () => {
let store;
......
......@@ -513,6 +513,48 @@ export const metricsDashboardPayload = {
},
],
},
{
title: 'memories',
type: 'area-chart',
y_label: 'memories',
metrics: [
{
id: 'metric_of_ages_1000',
label: 'memory_1000',
unit: 'count',
prometheus_endpoint_path: '/root',
metric_id: 20,
},
{
id: 'metric_of_ages_1001',
label: 'memory_1000',
unit: 'count',
prometheus_endpoint_path: '/root',
metric_id: 21,
},
{
id: 'metric_of_ages_1002',
label: 'memory_1000',
unit: 'count',
prometheus_endpoint_path: '/root',
metric_id: 22,
},
{
id: 'metric_of_ages_1003',
label: 'memory_1000',
unit: 'count',
prometheus_endpoint_path: '/root',
metric_id: 23,
},
{
id: 'metric_of_ages_1004',
label: 'memory_1004',
unit: 'count',
prometheus_endpoint_path: '/root',
metric_id: 24,
},
],
},
],
},
],
......
......@@ -50,9 +50,10 @@ describe('Monitoring mutations', () => {
expect(groups[0].panels).toHaveLength(1);
expect(groups[0].panels[0].metrics).toHaveLength(1);
expect(groups[1].panels).toHaveLength(2);
expect(groups[1].panels).toHaveLength(3);
expect(groups[1].panels[0].metrics).toHaveLength(1);
expect(groups[1].panels[1].metrics).toHaveLength(1);
expect(groups[1].panels[2].metrics).toHaveLength(5);
});
it('assigns metrics a metric id', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload);
......
......@@ -13,7 +13,7 @@ describe('Release edit component', () => {
beforeEach(() => {
gon.api_version = 'v4';
releaseClone = JSON.parse(JSON.stringify(convertObjectPropsToCamelCase(release)));
releaseClone = convertObjectPropsToCamelCase(release, { deep: true });
state = {
release: releaseClone,
......
......@@ -2,12 +2,14 @@ import { mount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility';
import Icon from '~/vue_shared/components/icon.vue';
import { release } from '../mock_data';
import { release as originalRelease } from '../mock_data';
import EvidenceBlock from '~/releases/components/evidence_block.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
describe('Evidence Block', () => {
let wrapper;
let release;
const factory = (options = {}) => {
wrapper = mount(EvidenceBlock, {
......@@ -16,6 +18,8 @@ describe('Evidence Block', () => {
};
beforeEach(() => {
release = convertObjectPropsToCamelCase(originalRelease, { deep: true });
factory({
propsData: {
release,
......@@ -32,7 +36,7 @@ describe('Evidence Block', () => {
});
it('renders the title for the dowload link', () => {
expect(wrapper.find(GlLink).text()).toBe(`${release.tag_name}-evidence.json`);
expect(wrapper.find(GlLink).text()).toBe(`${release.tagName}-evidence.json`);
});
it('renders the correct hover text for the download', () => {
......@@ -40,19 +44,19 @@ describe('Evidence Block', () => {
});
it('renders the correct file link for download', () => {
expect(wrapper.find(GlLink).attributes().download).toBe(`${release.tag_name}-evidence.json`);
expect(wrapper.find(GlLink).attributes().download).toBe(`${release.tagName}-evidence.json`);
});
describe('sha text', () => {
it('renders the short sha initially', () => {
expect(wrapper.find('.js-short').text()).toBe(truncateSha(release.evidence_sha));
expect(wrapper.find('.js-short').text()).toBe(truncateSha(release.evidenceSha));
});
it('renders the long sha after expansion', () => {
wrapper.find('.js-text-expander-prepend').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find('.js-expanded').text()).toBe(release.evidence_sha);
expect(wrapper.find('.js-expanded').text()).toBe(release.evidenceSha);
});
});
});
......@@ -68,7 +72,7 @@ describe('Evidence Block', () => {
it('copies the sha', () => {
expect(wrapper.find(ClipboardButton).attributes('data-clipboard-text')).toBe(
release.evidence_sha,
release.evidenceSha,
);
});
});
......
......@@ -24,7 +24,7 @@ describe('Release block footer', () => {
const factory = (props = {}) => {
wrapper = mount(ReleaseBlockFooter, {
propsData: {
...convertObjectPropsToCamelCase(releaseClone),
...convertObjectPropsToCamelCase(releaseClone, { deep: true }),
...props,
},
});
......
import { shallowMount } from '@vue/test-utils';
import { cloneDeep, merge } from 'lodash';
import { merge } from 'lodash';
import { GlLink } from '@gitlab/ui';
import ReleaseBlockHeader from '~/releases/components/release_block_header.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
......@@ -18,9 +18,7 @@ describe('Release block header', () => {
};
beforeEach(() => {
release = convertObjectPropsToCamelCase(cloneDeep(originalRelease), {
ignoreKeyNames: ['_links'],
});
release = convertObjectPropsToCamelCase(originalRelease, { deep: true });
});
afterEach(() => {
......@@ -39,13 +37,13 @@ describe('Release block header', () => {
const link = findHeaderLink();
expect(link.text()).toBe(release.name);
expect(link.attributes('href')).toBe(release._links.self);
expect(link.attributes('href')).toBe(release.Links.self);
});
});
describe('when _links.self is missing', () => {
beforeEach(() => {
factory({ _links: { self: null } });
factory({ Links: { self: null } });
});
it('renders the title as text', () => {
......
......@@ -2,12 +2,13 @@ import { mount } from '@vue/test-utils';
import { GlProgressBar, GlLink, GlBadge, GlButton } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import ReleaseBlockMilestoneInfo from '~/releases/components/release_block_milestone_info.vue';
import { milestones } from '../mock_data';
import { milestones as originalMilestones } from '../mock_data';
import { MAX_MILESTONES_TO_DISPLAY } from '~/releases/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
describe('Release block milestone info', () => {
let wrapper;
let milestonesClone;
let milestones;
const factory = milestonesProp => {
wrapper = mount(ReleaseBlockMilestoneInfo, {
......@@ -20,7 +21,7 @@ describe('Release block milestone info', () => {
};
beforeEach(() => {
milestonesClone = JSON.parse(JSON.stringify(milestones));
milestones = convertObjectPropsToCamelCase(originalMilestones, { deep: true });
});
afterEach(() => {
......@@ -32,7 +33,7 @@ describe('Release block milestone info', () => {
const issuesContainer = () => wrapper.find('.js-issues-container');
describe('with default props', () => {
beforeEach(() => factory(milestonesClone));
beforeEach(() => factory(milestones));
it('renders the correct percentage', () => {
expect(milestoneProgressBarContainer().text()).toContain('41% complete');
......@@ -53,13 +54,13 @@ describe('Release block milestone info', () => {
it('renders a list of links to all associated milestones', () => {
expect(trimText(milestoneListContainer().text())).toContain('Milestones 13.6 • 13.5');
milestonesClone.forEach((m, i) => {
milestones.forEach((m, i) => {
const milestoneLink = milestoneListContainer()
.findAll(GlLink)
.at(i);
expect(milestoneLink.text()).toBe(m.title);
expect(milestoneLink.attributes('href')).toBe(m.web_url);
expect(milestoneLink.attributes('href')).toBe(m.webUrl);
expect(milestoneLink.attributes('title')).toBe(m.description);
});
});
......@@ -84,7 +85,7 @@ describe('Release block milestone info', () => {
beforeEach(() => {
lotsOfMilestones = [];
const template = milestonesClone[0];
const template = milestones[0];
for (let i = 0; i < MAX_MILESTONES_TO_DISPLAY + 10; i += 1) {
lotsOfMilestones.push({
......@@ -148,16 +149,16 @@ describe('Release block milestone info', () => {
/** Ensures we don't have any issues with dividing by zero when computing percentages */
describe('when all issue counts are zero', () => {
beforeEach(() => {
milestonesClone = milestonesClone.map(m => ({
milestones = milestones.map(m => ({
...m,
issue_stats: {
...m.issue_stats,
issueStats: {
...m.issueStats,
opened: 0,
closed: 0,
},
}));
return factory(milestonesClone);
return factory(milestones);
});
expectAllZeros();
......@@ -165,12 +166,12 @@ describe('Release block milestone info', () => {
describe('if the API response is missing the "issue_stats" property', () => {
beforeEach(() => {
milestonesClone = milestonesClone.map(m => ({
milestones = milestones.map(m => ({
...m,
issue_stats: undefined,
issueStats: undefined,
}));
return factory(milestonesClone);
return factory(milestones);
});
expectAllZeros();
......
......@@ -5,10 +5,12 @@ import EvidenceBlock from '~/releases/components/evidence_block.vue';
import ReleaseBlock from '~/releases/components/release_block.vue';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { release } from '../mock_data';
import { release as originalRelease } from '../mock_data';
import Icon from '~/vue_shared/components/icon.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
const { convertObjectPropsToCamelCase } = jest.requireActual('~/lib/utils/common_utils');
let mockLocationHash;
jest.mock('~/lib/utils/url_utility', () => ({
__esModule: true,
......@@ -22,7 +24,7 @@ jest.mock('~/lib/utils/common_utils', () => ({
describe('Release block', () => {
let wrapper;
let releaseClone;
let release;
const factory = (releaseProp, featureFlags = {}) => {
wrapper = mount(ReleaseBlock, {
......@@ -45,7 +47,7 @@ describe('Release block', () => {
beforeEach(() => {
jest.spyOn($.fn, 'renderGFM');
releaseClone = JSON.parse(JSON.stringify(release));
release = convertObjectPropsToCamelCase(originalRelease, { deep: true });
});
afterEach(() => {
......@@ -61,7 +63,7 @@ describe('Release block', () => {
it('renders an edit button that links to the "Edit release" page', () => {
expect(editButton().exists()).toBe(true);
expect(editButton().attributes('href')).toBe(release._links.edit_url);
expect(editButton().attributes('href')).toBe(release.Links.editUrl);
});
it('renders release name', () => {
......@@ -74,7 +76,7 @@ describe('Release block', () => {
});
it('renders release date', () => {
expect(wrapper.text()).toContain(timeagoMixin.methods.timeFormatted(release.released_at));
expect(wrapper.text()).toContain(timeagoMixin.methods.timeFormatted(release.releasedAt));
});
it('renders number of assets provided', () => {
......@@ -129,72 +131,72 @@ describe('Release block', () => {
});
it('renders commit sha', () => {
releaseClone.commit_path = '/commit/example';
release.commitPath = '/commit/example';
return factory(releaseClone).then(() => {
expect(wrapper.text()).toContain(release.commit.short_id);
return factory(release).then(() => {
expect(wrapper.text()).toContain(release.commit.shortId);
expect(wrapper.find('a[href="/commit/example"]').exists()).toBe(true);
});
});
it('renders tag name', () => {
releaseClone.tag_path = '/tag/example';
release.tagPath = '/tag/example';
return factory(releaseClone).then(() => {
expect(wrapper.text()).toContain(release.tag_name);
return factory(release).then(() => {
expect(wrapper.text()).toContain(release.tagName);
expect(wrapper.find('a[href="/tag/example"]').exists()).toBe(true);
});
});
it("does not render an edit button if release._links.edit_url isn't a string", () => {
delete releaseClone._links;
it("does not render an edit button if release.Links.editUrl isn't a string", () => {
delete release.Links;
return factory(releaseClone).then(() => {
return factory(release).then(() => {
expect(editButton().exists()).toBe(false);
});
});
it('does not render the milestone list if no milestones are associated to the release', () => {
delete releaseClone.milestones;
delete release.milestones;
return factory(releaseClone).then(() => {
return factory(release).then(() => {
expect(milestoneListLabel().exists()).toBe(false);
});
});
it('renders upcoming release badge', () => {
releaseClone.upcoming_release = true;
release.upcomingRelease = true;
return factory(releaseClone).then(() => {
return factory(release).then(() => {
expect(wrapper.text()).toContain('Upcoming Release');
});
});
it('slugifies the tag_name before setting it as the elements ID', () => {
releaseClone.tag_name = 'a dangerous tag name <script>alert("hello")</script>';
it('slugifies the tagName before setting it as the elements ID', () => {
release.tagName = 'a dangerous tag name <script>alert("hello")</script>';
return factory(releaseClone).then(() => {
return factory(release).then(() => {
expect(wrapper.attributes().id).toBe('a-dangerous-tag-name-script-alert-hello-script');
});
});
describe('evidence block', () => {
it('renders the evidence block when the evidence is available and the feature flag is true', () =>
factory(releaseClone, { releaseEvidenceCollection: true }).then(() =>
factory(release, { releaseEvidenceCollection: true }).then(() =>
expect(wrapper.find(EvidenceBlock).exists()).toBe(true),
));
it('does not render the evidence block when the evidence is available but the feature flag is false', () =>
factory(releaseClone, { releaseEvidenceCollection: true }).then(() =>
factory(release, { releaseEvidenceCollection: true }).then(() =>
expect(wrapper.find(EvidenceBlock).exists()).toBe(true),
));
it('does not render the evidence block when there is no evidence', () => {
releaseClone.evidence_sha = null;
release.evidenceSha = null;
return factory(releaseClone).then(() => {
return factory(release).then(() => {
expect(wrapper.find(EvidenceBlock).exists()).toBe(false);
});
});
......@@ -222,7 +224,7 @@ describe('Release block', () => {
});
it("attempts to scroll itself into view if the anchor tag matches the release's tag name", () => {
mockLocationHash = release.tag_name;
mockLocationHash = release.tagName;
return factory(release).then(() => {
expect(scrollToElement).toHaveBeenCalledTimes(1);
......@@ -231,7 +233,7 @@ describe('Release block', () => {
});
it('renders with a light blue background if it is the target of the anchor', () => {
mockLocationHash = release.tag_name;
mockLocationHash = release.tagName;
return factory(release).then(() => {
expect(hasTargetBlueBackground()).toBe(true);
......@@ -275,16 +277,16 @@ describe('Release block', () => {
expect(milestoneLink.text()).toBe(milestone.title);
expect(milestoneLink.attributes('href')).toBe(milestone.web_url);
expect(milestoneLink.attributes('href')).toBe(milestone.webUrl);
expect(milestoneLink.attributes('title')).toBe(milestone.description);
});
});
it('renders the label as "Milestone" if only a single milestone is passed in', () => {
releaseClone.milestones = releaseClone.milestones.slice(0, 1);
release.milestones = release.milestones.slice(0, 1);
return factory(releaseClone, { releaseIssueSummary: false }).then(() => {
return factory(release, { releaseIssueSummary: false }).then(() => {
expect(
milestoneListLabel()
.find('.js-label-text')
......
......@@ -45,6 +45,21 @@ describe Resolvers::BoardsResolver do
expect(resolve_boards).to eq [board1]
end
end
context 'when querying for a single board' do
let(:board1) { create(:board, name: 'One', resource_parent: board_parent) }
it 'returns specified board' do
expect(resolve_boards(args: { id: global_id_of(board1) })).to eq [board1]
end
it 'returns nil if board not found' do
outside_parent = create(board_parent.class.underscore.to_sym)
outside_board = create(:board, name: 'outside board', resource_parent: outside_parent)
expect(resolve_boards(args: { id: global_id_of(outside_board) })).to eq Board.none
end
end
end
describe '#resolve' do
......
import _ from 'underscore';
import { escape as esc } from 'lodash';
import VisualTokenValue from '~/filtered_search/visual_token_value';
import AjaxCache from '~/lib/utils/ajax_cache';
import UsersCache from '~/lib/utils/users_cache';
......@@ -121,7 +121,7 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
tokenValueElement.querySelector('.avatar').remove();
expect(tokenValueElement.innerHTML.trim()).toBe(_.escape(dummyUser.name));
expect(tokenValueElement.innerHTML.trim()).toBe(esc(dummyUser.name));
})
.then(done)
.catch(done.fail);
......
......@@ -12,6 +12,7 @@ import {
release,
releases,
} from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
describe('Releases App ', () => {
const Component = Vue.extend(app);
......@@ -27,7 +28,10 @@ describe('Releases App ', () => {
beforeEach(() => {
store = createStore({ list: listModule });
releasesPagination = _.range(21).map(index => ({ ...release, tag_name: `${index}.00` }));
releasesPagination = _.range(21).map(index => ({
...convertObjectPropsToCamelCase(release, { deep: true }),
tagName: `${index}.00`,
}));
});
afterEach(() => {
......
......@@ -8,16 +8,18 @@ import {
import state from '~/releases/stores/modules/list/state';
import * as types from '~/releases/stores/modules/list/mutation_types';
import api from '~/api';
import { parseIntPagination } from '~/lib/utils/common_utils';
import { pageInfoHeadersWithoutPagination, releases } from '../../../mock_data';
import { parseIntPagination, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { pageInfoHeadersWithoutPagination, releases as originalReleases } from '../../../mock_data';
describe('Releases State actions', () => {
let mockedState;
let pageInfo;
let releases;
beforeEach(() => {
mockedState = state();
pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination);
releases = convertObjectPropsToCamelCase(originalReleases, { deep: true });
});
describe('requestReleases', () => {
......
......@@ -9,14 +9,12 @@ describe 'get list of boards' do
describe 'for a project' do
let(:board_parent) { create(:project, :repository, :private) }
let(:boards_data) { graphql_data['project']['boards']['edges'] }
it_behaves_like 'group and project boards query'
end
describe 'for a group' do
let(:board_parent) { create(:group, :private) }
let(:boards_data) { graphql_data['group']['boards']['edges'] }
before do
allow(board_parent).to receive(:multiple_issue_boards_available?).and_return(false)
......
......@@ -5,6 +5,8 @@ RSpec.shared_context 'group and project boards query context' do
let(:current_user) { user }
let(:params) { '' }
let(:board_parent_type) { board_parent.class.to_s.downcase }
let(:boards_data) { graphql_data[board_parent_type]['boards']['edges'] }
let(:board_data) { graphql_data[board_parent_type]['board'] }
let(:start_cursor) { graphql_data[board_parent_type]['boards']['pageInfo']['startCursor'] }
let(:end_cursor) { graphql_data[board_parent_type]['boards']['pageInfo']['endCursor'] }
......@@ -28,6 +30,18 @@ RSpec.shared_context 'group and project boards query context' do
)
end
def query_single_board(board_params = params)
graphql_query_for(
board_parent_type,
{ 'fullPath' => board_parent.full_path },
<<~BOARD
board(#{board_params}) {
#{all_graphql_fields_for('board'.classify)}
}
BOARD
)
end
def grab_names(data = boards_data)
data.map do |board|
board.dig('node', 'name')
......
......@@ -89,4 +89,24 @@ RSpec.shared_examples 'group and project boards query' do
end
end
end
context 'when querying for a single board' do
before do
board_parent.add_reporter(current_user)
end
it_behaves_like 'a working graphql query' do
before do
post_graphql(query_single_board, current_user: current_user)
end
end
it 'finds the correct board' do
board = create(:board, resource_parent: board_parent, name: 'A')
post_graphql(query_single_board("id: \"#{global_id_of(board)}\""), current_user: current_user)
expect(board_data['name']).to eq board.name
end
end
end
......@@ -56,7 +56,7 @@ describe 'profiles/preferences/show' do
expect(rendered).not_to have_sourcegraph_field
end
it 'does not display integrations settings' do
it 'does not display Integration Settings' do
expect(rendered).not_to have_integrations_section
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