Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG-EE.md7
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--app/assets/javascripts/grafana_integration/components/grafana_integration.vue4
-rw-r--r--app/assets/javascripts/operation_settings/components/external_dashboard.vue4
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/init_pipelines.js28
-rw-r--r--app/assets/javascripts/pages/projects/settings/integrations/show/index.js6
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js8
-rw-r--r--app/assets/javascripts/releases/components/app_index.vue2
-rw-r--r--app/assets/javascripts/releases/components/evidence_block.vue6
-rw-r--r--app/assets/javascripts/releases/components/release_block.vue14
-rw-r--r--app/assets/javascripts/releases/components/release_block_author.vue4
-rw-r--r--app/assets/javascripts/releases/components/release_block_footer.vue8
-rw-r--r--app/assets/javascripts/releases/components/release_block_header.vue6
-rw-r--r--app/assets/javascripts/releases/components/release_block_metadata.vue16
-rw-r--r--app/assets/javascripts/releases/components/release_block_milestone_info.vue4
-rw-r--r--app/assets/javascripts/releases/components/release_block_milestones.vue2
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/actions.js3
-rw-r--r--app/assets/javascripts/releases/stores/modules/list/actions.js12
-rw-r--r--app/controllers/projects/hooks_controller.rb11
-rw-r--r--app/controllers/projects/settings/integrations_controller.rb4
-rw-r--r--app/graphql/resolvers/boards_resolver.rb18
-rw-r--r--app/graphql/types/group_type.rb6
-rw-r--r--app/graphql/types/project_type.rb6
-rw-r--r--app/helpers/projects_helper.rb3
-rw-r--r--app/helpers/user_callouts_helper.rb5
-rw-r--r--app/models/hooks/project_hook.rb2
-rw-r--r--app/models/user_callout_enums.rb3
-rw-r--r--app/services/resource_events/change_milestone_service.rb42
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml6
-rw-r--r--app/views/projects/hook_logs/show.html.haml4
-rw-r--r--app/views/projects/hooks/edit.html.haml5
-rw-r--r--app/views/projects/hooks/index.html.haml (renamed from app/views/projects/hooks/_index.html.haml)4
-rw-r--r--app/views/projects/services/_index.html.haml6
-rw-r--r--app/views/projects/services/edit.html.haml3
-rw-r--r--app/views/projects/settings/integrations/show.html.haml14
-rw-r--r--app/views/projects/settings/operations/_error_tracking.html.haml2
-rw-r--r--app/views/projects/settings/operations/_incidents.html.haml2
-rw-r--r--app/views/shared/web_hooks/_form.html.haml22
-rw-r--r--changelogs/unreleased/119429-decouple-webhooks-from-integrations-within-project-settings.yml5
-rw-r--r--changelogs/unreleased/bw-board-query-by-id.yml5
-rw-r--r--changelogs/unreleased/fix-pipeline-details-invalid-buttons.yml5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql30
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json66
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/development/contributing/issue_workflow.md17
-rw-r--r--doc/development/migration_style_guide.md2
-rw-r--r--doc/development/module_with_instance_variables.md4
-rw-r--r--doc/development/namespaces_storage_statistics.md6
-rw-r--r--doc/user/project/integrations/webhooks.md2
-rw-r--r--doc/user/project/push_options.md4
-rw-r--r--locale/gitlab.pot38
-rw-r--r--spec/controllers/projects/hooks_controller_spec.rb9
-rw-r--r--spec/factories/ci/pipelines.rb1
-rw-r--r--spec/features/projects/navbar_spec.rb1
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb6
-rw-r--r--spec/features/projects/services/user_views_services_spec.rb2
-rw-r--r--spec/features/projects/settings/operations_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/webhooks_settings_spec.rb (renamed from spec/features/projects/settings/integration_settings_spec.rb)21
-rw-r--r--spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap6
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js813
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js2
-rw-r--r--spec/frontend/monitoring/mock_data.js42
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js3
-rw-r--r--spec/frontend/releases/components/app_edit_spec.js2
-rw-r--r--spec/frontend/releases/components/evidence_block_spec.js16
-rw-r--r--spec/frontend/releases/components/release_block_footer_spec.js2
-rw-r--r--spec/frontend/releases/components/release_block_header_spec.js10
-rw-r--r--spec/frontend/releases/components/release_block_milestone_info_spec.js29
-rw-r--r--spec/frontend/releases/components/release_block_spec.js62
-rw-r--r--spec/graphql/resolvers/boards_resolver_spec.rb15
-rw-r--r--spec/javascripts/filtered_search/visual_token_value_spec.js4
-rw-r--r--spec/javascripts/releases/components/app_index_spec.js6
-rw-r--r--spec/javascripts/releases/stores/modules/list/actions_spec.js6
-rw-r--r--spec/requests/api/graphql/boards/boards_query_spec.rb2
-rw-r--r--spec/support/shared_contexts/requests/api/graphql/group_and_project_boards_query_shared_context.rb14
-rw-r--r--spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb20
-rw-r--r--spec/views/profiles/preferences/show.html.haml_spec.rb2
77 files changed, 963 insertions, 625 deletions
diff --git a/CHANGELOG-EE.md b/CHANGELOG-EE.md
index 94863cfeecb..fe723f5d1ef 100644
--- a/CHANGELOG-EE.md
+++ b/CHANGELOG-EE.md
@@ -1,5 +1,12 @@
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)
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 426cb9d6d8c..6fb2bffa71e 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-8.22.0
+8.23.0
diff --git a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
index 41d83e45c52..22cafd50bf3 100644
--- a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
+++ b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
@@ -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.') }}
diff --git a/app/assets/javascripts/operation_settings/components/external_dashboard.vue b/app/assets/javascripts/operation_settings/components/external_dashboard.vue
index e90e27a402a..8b6467bc0f6 100644
--- a/app/assets/javascripts/operation_settings/components/external_dashboard.vue
+++ b/app/assets/javascripts/operation_settings/components/external_dashboard.vue
@@ -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">
{{
diff --git a/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js b/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js
index ade6908c4a5..5fd3fce88aa 100644
--- a/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js
+++ b/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js
@@ -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',
+ },
+ });
+ }
};
diff --git a/app/assets/javascripts/pages/projects/settings/integrations/show/index.js b/app/assets/javascripts/pages/projects/settings/integrations/show/index.js
new file mode 100644
index 00000000000..f2cf2eb9b28
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/settings/integrations/show/index.js
@@ -0,0 +1,6 @@
+import PersistentUserCallout from '~/persistent_user_callout';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const callout = document.querySelector('.js-webhooks-moved-alert');
+ PersistentUserCallout.factory(callout);
+});
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index d9192d3d76b..ffcb0f24cc6 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -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
diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue
index f602c9fdda2..b9e80899e25 100644
--- a/app/assets/javascripts/releases/components/app_index.vue
+++ b/app/assets/javascripts/releases/components/app_index.vue
@@ -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 }"
/>
diff --git a/app/assets/javascripts/releases/components/evidence_block.vue b/app/assets/javascripts/releases/components/evidence_block.vue
index d9abd195fee..edbea33f1d1 100644
--- a/app/assets/javascripts/releases/components/evidence_block.vue
+++ b/app/assets/javascripts/releases/components/evidence_block.vue
@@ -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;
},
},
};
diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue
index bc3f2c3bf30..f2cc36e38bb 100644
--- a/app/assets/javascripts/releases/components/release_block.vue
+++ b/app/assets/javascripts/releases/components/release_block.vue
@@ -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>
diff --git a/app/assets/javascripts/releases/components/release_block_author.vue b/app/assets/javascripts/releases/components/release_block_author.vue
index e7075d4d67a..0432d45b2dc 100644
--- a/app/assets/javascripts/releases/components/release_block_author.vue
+++ b/app/assets/javascripts/releases/components/release_block_author.vue
@@ -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"
/>
diff --git a/app/assets/javascripts/releases/components/release_block_footer.vue b/app/assets/javascripts/releases/components/release_block_footer.vue
index 8533fc17ffd..a95fbc0b373 100644
--- a/app/assets/javascripts/releases/components/release_block_footer.vue
+++ b/app/assets/javascripts/releases/components/release_block_footer.vue
@@ -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"
diff --git a/app/assets/javascripts/releases/components/release_block_header.vue b/app/assets/javascripts/releases/components/release_block_header.vue
index b459418aef2..f0d3f3f8c1d 100644
--- a/app/assets/javascripts/releases/components/release_block_header.vue
+++ b/app/assets/javascripts/releases/components/release_block_header.vue
@@ -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>
diff --git a/app/assets/javascripts/releases/components/release_block_metadata.vue b/app/assets/javascripts/releases/components/release_block_metadata.vue
index f0aad594062..052e4088a5f 100644
--- a/app/assets/javascripts/releases/components/release_block_metadata.vue
+++ b/app/assets/javascripts/releases/components/release_block_metadata.vue
@@ -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>
diff --git a/app/assets/javascripts/releases/components/release_block_milestone_info.vue b/app/assets/javascripts/releases/components/release_block_milestone_info.vue
index d3e354d6157..50accf6b679 100644
--- a/app/assets/javascripts/releases/components/release_block_milestone_info.vue
+++ b/app/assets/javascripts/releases/components/release_block_milestone_info.vue
@@ -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 }}
diff --git a/app/assets/javascripts/releases/components/release_block_milestones.vue b/app/assets/javascripts/releases/components/release_block_milestones.vue
index a3dff75b828..9abd3345b22 100644
--- a/app/assets/javascripts/releases/components/release_block_milestones.vue
+++ b/app/assets/javascripts/releases/components/release_block_milestones.vue
@@ -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 }}
diff --git a/app/assets/javascripts/releases/stores/modules/detail/actions.js b/app/assets/javascripts/releases/stores/modules/detail/actions.js
index c9749582f5c..f730af1c7dc 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/actions.js
@@ -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);
diff --git a/app/assets/javascripts/releases/stores/modules/list/actions.js b/app/assets/javascripts/releases/stores/modules/list/actions.js
index b15fb69226f..06d13890a9d 100644
--- a/app/assets/javascripts/releases/stores/modules/list/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/list/actions.js
@@ -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 }) => {
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index 5fa0339f44d..097a357889f 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -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
diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb
index 0c5cf01d912..a4a53676ec7 100644
--- a/app/controllers/projects/settings/integrations_controller.rb
+++ b/app/controllers/projects/settings/integrations_controller.rb
@@ -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
diff --git a/app/graphql/resolvers/boards_resolver.rb b/app/graphql/resolvers/boards_resolver.rb
index 45c03bf0bef..eceb5b38031 100644
--- a/app/graphql/resolvers/boards_resolver.rb
+++ b/app/graphql/resolvers/boards_resolver.rb
@@ -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
diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb
index 9f3905000b2..699aa51e6c8 100644
--- a/app/graphql/types/group_type.rb
+++ b/app/graphql/types/group_type.rb
@@ -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
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index f89bd5575a3..1142459f6eb 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -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
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 5aff12621b5..cf9f3b9e924 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -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
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index ab691916706..841599abe81 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -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)
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index a5f68831f34..bc480b14e67 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -21,7 +21,7 @@ class ProjectHook < WebHook
validates :project, presence: true
def pluralized_name
- _('Project Hooks')
+ _('Webhooks')
end
end
diff --git a/app/models/user_callout_enums.rb b/app/models/user_callout_enums.rb
index ef0b2407e23..625cbb4fe5f 100644
--- a/app/models/user_callout_enums.rb
+++ b/app/models/user_callout_enums.rb
@@ -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
diff --git a/app/services/resource_events/change_milestone_service.rb b/app/services/resource_events/change_milestone_service.rb
index bad7d002c65..dd637bcc765 100644
--- a/app/services/resource_events/change_milestone_service.rb
+++ b/app/services/resource_events/change_milestone_service.rb
@@ -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
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index b9324f0596c..160e2a7c952 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -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
diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml
index a8796cd7b1c..873fb4d47b7 100644
--- a/app/views/projects/hook_logs/show.html.haml
+++ b/app/views/projects/hook_logs/show.html.haml
@@ -1,3 +1,7 @@
+- @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
diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml
index c1fdf619eb5..f7eae802dac 100644
--- a/app/views/projects/hooks/edit.html.haml
+++ b/app/views/projects/hooks/edit.html.haml
@@ -1,5 +1,6 @@
-- 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
diff --git a/app/views/projects/hooks/_index.html.haml b/app/views/projects/hooks/index.html.haml
index 70f2fa0e758..169a5cc9d6b 100644
--- a/app/views/projects/hooks/_index.html.haml
+++ b/app/views/projects/hooks/index.html.haml
@@ -1,3 +1,7 @@
+- @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
diff --git a/app/views/projects/services/_index.html.haml b/app/views/projects/services/_index.html.haml
index 3f33d72d3ec..a4041d09415 100644
--- a/app/views/projects/services/_index.html.haml
+++ b/app/views/projects/services/_index.html.haml
@@ -1,8 +1,8 @@
-.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
diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml
index e3e8a312431..ef799d2c046 100644
--- a/app/views/projects/services/edit.html.haml
+++ b/app/views/projects/services/edit.html.haml
@@ -1,7 +1,6 @@
- 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
diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml
index 76770290f36..f603f23a2c7 100644
--- a/app/views/projects/settings/integrations/show.html.haml
+++ b/app/views/projects/settings/integrations/show.html.haml
@@ -1,5 +1,15 @@
- @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'
diff --git a/app/views/projects/settings/operations/_error_tracking.html.haml b/app/views/projects/settings/operations/_error_tracking.html.haml
index 06b5243dfd9..393b1f9d21a 100644
--- a/app/views/projects/settings/operations/_error_tracking.html.haml
+++ b/app/views/projects/settings/operations/_error_tracking.html.haml
@@ -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')
diff --git a/app/views/projects/settings/operations/_incidents.html.haml b/app/views/projects/settings/operations/_incidents.html.haml
index 756d4042613..a96a41b78c2 100644
--- a/app/views/projects/settings/operations/_incidents.html.haml
+++ b/app/views/projects/settings/operations/_incidents.html.haml
@@ -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
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index 9c5b9593bba..ce85cbd7f07 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -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'
diff --git a/changelogs/unreleased/119429-decouple-webhooks-from-integrations-within-project-settings.yml b/changelogs/unreleased/119429-decouple-webhooks-from-integrations-within-project-settings.yml
new file mode 100644
index 00000000000..32a15defb58
--- /dev/null
+++ b/changelogs/unreleased/119429-decouple-webhooks-from-integrations-within-project-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Decouple Webhooks from Integrations within Project > Settings
+merge_request: 23136
+author:
+type: changed
diff --git a/changelogs/unreleased/bw-board-query-by-id.yml b/changelogs/unreleased/bw-board-query-by-id.yml
new file mode 100644
index 00000000000..51246a4519f
--- /dev/null
+++ b/changelogs/unreleased/bw-board-query-by-id.yml
@@ -0,0 +1,5 @@
+---
+title: Allow group/project board to be queried by ID via GraphQL
+merge_request: 24825
+author:
+type: added
diff --git a/changelogs/unreleased/fix-pipeline-details-invalid-buttons.yml b/changelogs/unreleased/fix-pipeline-details-invalid-buttons.yml
new file mode 100644
index 00000000000..afb162f7440
--- /dev/null
+++ b/changelogs/unreleased/fix-pipeline-details-invalid-buttons.yml
@@ -0,0 +1,5 @@
+---
+title: Fix pipeline details page initialisation on invalid pipeline
+merge_request: 25302
+author: Fabio Huser
+type: fixed
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 4c12465f5b0..816512c83bb 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -2771,6 +2771,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
"""
boards(
@@ -2790,6 +2800,11 @@ type Group {
first: Int
"""
+ Find a board by its ID
+ """
+ id: ID
+
+ """
Returns the last _n_ elements from the list.
"""
last: Int
@@ -5255,6 +5270,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
"""
boards(
@@ -5274,6 +5299,11 @@ type Project {
first: Int
"""
+ Find a board by its ID
+ """
+ id: ID
+
+ """
Returns the last _n_ elements from the list.
"""
last: Int
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 209b6da5ab2..2053bdb9404 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -369,10 +369,43 @@
"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.",
"type": {
@@ -3176,10 +3209,43 @@
"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.",
"type": {
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 2b17b86b1d0..978fbc35125 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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 |
diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md
index 7c2a7a6560e..ec2386a2bd4 100644
--- a/doc/development/contributing/issue_workflow.md
+++ b/doc/development/contributing/issue_workflow.md
@@ -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,
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index d31729d2b0f..7698492b29b 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -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:
diff --git a/doc/development/module_with_instance_variables.md b/doc/development/module_with_instance_variables.md
index 1687a9f5ed4..b0eab95190b 100644
--- a/doc/development/module_with_instance_variables.md
+++ b/doc/development/module_with_instance_variables.md
@@ -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
diff --git a/doc/development/namespaces_storage_statistics.md b/doc/development/namespaces_storage_statistics.md
index 71c9a0b96fb..f175739e55e 100644
--- a/doc/development/namespaces_storage_statistics.md
+++ b/doc/development/namespaces_storage_statistics.md
@@ -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
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index d93c4b3cdae..202490f2638 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -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)
diff --git a/doc/user/project/push_options.md b/doc/user/project/push_options.md
index c52320ef656..7af7960404d 100644
--- a/doc/user/project/push_options.md
+++ b/doc/user/project/push_options.md
@@ -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:
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 430c4b932b0..f1e788c0be1 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/controllers/projects/hooks_controller_spec.rb b/spec/controllers/projects/hooks_controller_spec.rb
index f50ef2d804c..e97f602d9ab 100644
--- a/spec/controllers/projects/hooks_controller_spec.rb
+++ b/spec/controllers/projects/hooks_controller_spec.rb
@@ -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
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index afc203562ba..40b2aa3042e 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -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
diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb
index 4ab6b0ce506..95a8f974261 100644
--- a/spec/features/projects/navbar_spec.rb
+++ b/spec/features/projects/navbar_spec.rb
@@ -88,6 +88,7 @@ describe 'Project navbar' do
_('General'),
_('Members'),
_('Integrations'),
+ _('Webhooks'),
_('Repository'),
_('CI / CD'),
_('Operations'),
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 63c0695fe95..1c72c54f0a1 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -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
diff --git a/spec/features/projects/services/user_views_services_spec.rb b/spec/features/projects/services/user_views_services_spec.rb
index d9358a40602..cf403a131b0 100644
--- a/spec/features/projects/services/user_views_services_spec.rb
+++ b/spec/features/projects/services/user_views_services_spec.rb
@@ -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')
diff --git a/spec/features/projects/settings/operations_settings_spec.rb b/spec/features/projects/settings/operations_settings_spec.rb
index d57401471ff..3c9102431e8 100644
--- a/spec/features/projects/settings/operations_settings_spec.rb
+++ b/spec/features/projects/settings/operations_settings_spec.rb
@@ -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
diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/webhooks_settings_spec.rb
index de987b879eb..7e22117c63c 100644
--- a/spec/features/projects/settings/integration_settings_spec.rb
+++ b/spec/features/projects/settings/webhooks_settings_spec.rb
@@ -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)
diff --git a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
index 3d56bef4b33..09977ecc7a3 100644
--- a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
+++ b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
@@ -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"
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index 49f2a70a8b2..4dd376faac0 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { setTestTimeout } from 'helpers/timeout';
import { GlLink } from '@gitlab/ui';
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
+import { cloneDeep } from 'lodash';
import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper';
import { chartColorValues } from '~/monitoring/constants';
import { createStore } from '~/monitoring/stores';
@@ -32,501 +33,563 @@ jest.mock('~/lib/utils/icon_utils', () => ({
describe('Time series component', () => {
let mockGraphData;
- let makeTimeSeriesChart;
let store;
- beforeEach(() => {
- setTestTimeout(1000);
-
- store = createStore();
-
- store.commit(
- `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
- metricsDashboardPayload,
- );
-
- store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
+ const makeTimeSeriesChart = (graphData, type) =>
+ shallowMount(TimeSeries, {
+ propsData: {
+ graphData: { ...graphData, type },
+ deploymentData: store.state.monitoringDashboard.deploymentData,
+ projectPath: `${mockHost}${mockProjectDir}`,
+ },
+ store,
+ });
- // Mock data contains 2 panel groups, with 1 and 2 panels respectively
- store.commit(
- `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`,
- mockedQueryResultPayload,
- );
+ describe('With a single time series', () => {
+ beforeEach(() => {
+ setTestTimeout(1000);
- // Pick the second panel group and the first panel in it
- [mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[1].panels;
+ store = createStore();
- makeTimeSeriesChart = (graphData, type) =>
- shallowMount(TimeSeries, {
- propsData: {
- graphData: { ...graphData, type },
- deploymentData: store.state.monitoringDashboard.deploymentData,
- projectPath: `${mockHost}${mockProjectDir}`,
- },
- store,
- });
- });
+ store.commit(
+ `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
+ metricsDashboardPayload,
+ );
- describe('general functions', () => {
- let timeSeriesChart;
+ store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
- const findChart = () => timeSeriesChart.find({ ref: 'chart' });
+ // Mock data contains 2 panel groups, with 1 and 2 panels respectively
+ store.commit(
+ `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`,
+ mockedQueryResultPayload,
+ );
- beforeEach(done => {
- timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart');
- timeSeriesChart.vm.$nextTick(done);
+ // Pick the second panel group and the first panel in it
+ [mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[1].panels;
});
- it('allows user to override max value label text using prop', () => {
- timeSeriesChart.setProps({ legendMaxText: 'legendMaxText' });
-
- return timeSeriesChart.vm.$nextTick().then(() => {
- expect(timeSeriesChart.props().legendMaxText).toBe('legendMaxText');
- });
- });
+ describe('general functions', () => {
+ let timeSeriesChart;
- it('allows user to override average value label text using prop', () => {
- timeSeriesChart.setProps({ legendAverageText: 'averageText' });
+ const findChart = () => timeSeriesChart.find({ ref: 'chart' });
- return timeSeriesChart.vm.$nextTick().then(() => {
- expect(timeSeriesChart.props().legendAverageText).toBe('averageText');
+ beforeEach(done => {
+ timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart');
+ timeSeriesChart.vm.$nextTick(done);
});
- });
- describe('events', () => {
- describe('datazoom', () => {
- let eChartMock;
- let startValue;
- let endValue;
-
- beforeEach(done => {
- eChartMock = {
- handlers: {},
- getOption: () => ({
- dataZoom: [
- {
- startValue,
- endValue,
- },
- ],
- }),
- off: jest.fn(eChartEvent => {
- delete eChartMock.handlers[eChartEvent];
- }),
- on: jest.fn((eChartEvent, fn) => {
- eChartMock.handlers[eChartEvent] = fn;
- }),
- };
-
- timeSeriesChart = makeTimeSeriesChart(mockGraphData);
- timeSeriesChart.vm.$nextTick(() => {
- findChart().vm.$emit('created', eChartMock);
- done();
- });
- });
+ it('allows user to override max value label text using prop', () => {
+ timeSeriesChart.setProps({ legendMaxText: 'legendMaxText' });
- it('handles datazoom event from chart', () => {
- startValue = 1577836800000; // 2020-01-01T00:00:00.000Z
- endValue = 1577840400000; // 2020-01-01T01:00:00.000Z
- eChartMock.handlers.datazoom();
-
- expect(timeSeriesChart.emitted('datazoom')).toHaveLength(1);
- expect(timeSeriesChart.emitted('datazoom')[0]).toEqual([
- {
- start: new Date(startValue).toISOString(),
- end: new Date(endValue).toISOString(),
- },
- ]);
+ return timeSeriesChart.vm.$nextTick().then(() => {
+ expect(timeSeriesChart.props().legendMaxText).toBe('legendMaxText');
});
});
- });
- describe('methods', () => {
- describe('formatTooltipText', () => {
- let mockDate;
- let mockCommitUrl;
- let generateSeriesData;
+ it('allows user to override average value label text using prop', () => {
+ timeSeriesChart.setProps({ legendAverageText: 'averageText' });
- beforeEach(() => {
- mockDate = deploymentData[0].created_at;
- mockCommitUrl = deploymentData[0].commitUrl;
- generateSeriesData = type => ({
- seriesData: [
- {
- seriesName: timeSeriesChart.vm.chartData[0].name,
- componentSubType: type,
- value: [mockDate, 5.55555],
- dataIndex: 0,
- },
- ],
- value: mockDate,
- });
+ return timeSeriesChart.vm.$nextTick().then(() => {
+ expect(timeSeriesChart.props().legendAverageText).toBe('averageText');
});
+ });
- it('does not throw error if data point is outside the zoom range', () => {
- const seriesDataWithoutValue = generateSeriesData('line');
- expect(
- timeSeriesChart.vm.formatTooltipText({
- ...seriesDataWithoutValue,
- seriesData: seriesDataWithoutValue.seriesData.map(data => ({
- ...data,
- value: undefined,
- })),
- }),
- ).toBeUndefined();
- });
+ describe('events', () => {
+ describe('datazoom', () => {
+ let eChartMock;
+ let startValue;
+ let endValue;
- describe('when series is of line type', () => {
beforeEach(done => {
- timeSeriesChart.vm.formatTooltipText(generateSeriesData('line'));
- timeSeriesChart.vm.$nextTick(done);
- });
+ eChartMock = {
+ handlers: {},
+ getOption: () => ({
+ dataZoom: [
+ {
+ startValue,
+ endValue,
+ },
+ ],
+ }),
+ off: jest.fn(eChartEvent => {
+ delete eChartMock.handlers[eChartEvent];
+ }),
+ on: jest.fn((eChartEvent, fn) => {
+ eChartMock.handlers[eChartEvent] = fn;
+ }),
+ };
- it('formats tooltip title', () => {
- expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM');
+ timeSeriesChart = makeTimeSeriesChart(mockGraphData);
+ timeSeriesChart.vm.$nextTick(() => {
+ findChart().vm.$emit('created', eChartMock);
+ done();
+ });
});
- it('formats tooltip content', () => {
- const name = 'Pod average';
- const value = '5.556';
- const dataIndex = 0;
- const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel);
+ it('handles datazoom event from chart', () => {
+ startValue = 1577836800000; // 2020-01-01T00:00:00.000Z
+ endValue = 1577840400000; // 2020-01-01T01:00:00.000Z
+ eChartMock.handlers.datazoom();
- expect(seriesLabel.vm.color).toBe('');
- expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true);
- expect(timeSeriesChart.vm.tooltip.content).toEqual([
- { name, value, dataIndex, color: undefined },
+ expect(timeSeriesChart.emitted('datazoom')).toHaveLength(1);
+ expect(timeSeriesChart.emitted('datazoom')[0]).toEqual([
+ {
+ start: new Date(startValue).toISOString(),
+ end: new Date(endValue).toISOString(),
+ },
]);
-
- expect(
- shallowWrapperContainsSlotText(
- timeSeriesChart.find(GlAreaChart),
- 'tooltipContent',
- value,
- ),
- ).toBe(true);
});
});
+ });
+
+ describe('methods', () => {
+ describe('formatTooltipText', () => {
+ let mockDate;
+ let mockCommitUrl;
+ let generateSeriesData;
- describe('when series is of scatter type, for deployments', () => {
beforeEach(() => {
- timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter'));
+ mockDate = deploymentData[0].created_at;
+ mockCommitUrl = deploymentData[0].commitUrl;
+ generateSeriesData = type => ({
+ seriesData: [
+ {
+ seriesName: timeSeriesChart.vm.chartData[0].name,
+ componentSubType: type,
+ value: [mockDate, 5.55555],
+ dataIndex: 0,
+ },
+ ],
+ value: mockDate,
+ });
});
- it('formats tooltip title', () => {
- expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM');
+ it('does not throw error if data point is outside the zoom range', () => {
+ const seriesDataWithoutValue = generateSeriesData('line');
+ expect(
+ timeSeriesChart.vm.formatTooltipText({
+ ...seriesDataWithoutValue,
+ seriesData: seriesDataWithoutValue.seriesData.map(data => ({
+ ...data,
+ value: undefined,
+ })),
+ }),
+ ).toBeUndefined();
});
- it('formats tooltip sha', () => {
- expect(timeSeriesChart.vm.tooltip.sha).toBe('f5bcd1d9');
+ describe('when series is of line type', () => {
+ beforeEach(done => {
+ timeSeriesChart.vm.formatTooltipText(generateSeriesData('line'));
+ timeSeriesChart.vm.$nextTick(done);
+ });
+
+ it('formats tooltip title', () => {
+ expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM');
+ });
+
+ it('formats tooltip content', () => {
+ const name = 'Pod average';
+ const value = '5.556';
+ const dataIndex = 0;
+ const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel);
+
+ expect(seriesLabel.vm.color).toBe('');
+ expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true);
+ expect(timeSeriesChart.vm.tooltip.content).toEqual([
+ { name, value, dataIndex, color: undefined },
+ ]);
+
+ expect(
+ shallowWrapperContainsSlotText(
+ timeSeriesChart.find(GlAreaChart),
+ 'tooltipContent',
+ value,
+ ),
+ ).toBe(true);
+ });
});
- it('formats tooltip commit url', () => {
- expect(timeSeriesChart.vm.tooltip.commitUrl).toBe(mockCommitUrl);
+ describe('when series is of scatter type, for deployments', () => {
+ beforeEach(() => {
+ timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter'));
+ });
+
+ it('formats tooltip title', () => {
+ expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM');
+ });
+
+ it('formats tooltip sha', () => {
+ expect(timeSeriesChart.vm.tooltip.sha).toBe('f5bcd1d9');
+ });
+
+ it('formats tooltip commit url', () => {
+ expect(timeSeriesChart.vm.tooltip.commitUrl).toBe(mockCommitUrl);
+ });
});
});
- });
- describe('setSvg', () => {
- const mockSvgName = 'mockSvgName';
+ describe('setSvg', () => {
+ const mockSvgName = 'mockSvgName';
- beforeEach(done => {
- timeSeriesChart.vm.setSvg(mockSvgName);
- timeSeriesChart.vm.$nextTick(done);
- });
+ beforeEach(done => {
+ timeSeriesChart.vm.setSvg(mockSvgName);
+ timeSeriesChart.vm.$nextTick(done);
+ });
- it('gets svg path content', () => {
- expect(iconUtils.getSvgIconPathContent).toHaveBeenCalledWith(mockSvgName);
- });
+ it('gets svg path content', () => {
+ expect(iconUtils.getSvgIconPathContent).toHaveBeenCalledWith(mockSvgName);
+ });
- it('sets svg path content', () => {
- timeSeriesChart.vm.$nextTick(() => {
- expect(timeSeriesChart.vm.svgs[mockSvgName]).toBe(`path://${mockSvgPathContent}`);
+ it('sets svg path content', () => {
+ timeSeriesChart.vm.$nextTick(() => {
+ expect(timeSeriesChart.vm.svgs[mockSvgName]).toBe(`path://${mockSvgPathContent}`);
+ });
});
- });
- it('contains an svg object within an array to properly render icon', () => {
- timeSeriesChart.vm.$nextTick(() => {
- expect(timeSeriesChart.vm.chartOptions.dataZoom).toEqual([
- {
- handleIcon: `path://${mockSvgPathContent}`,
- },
- ]);
+ it('contains an svg object within an array to properly render icon', () => {
+ timeSeriesChart.vm.$nextTick(() => {
+ expect(timeSeriesChart.vm.chartOptions.dataZoom).toEqual([
+ {
+ handleIcon: `path://${mockSvgPathContent}`,
+ },
+ ]);
+ });
});
});
- });
- describe('onResize', () => {
- const mockWidth = 233;
+ describe('onResize', () => {
+ const mockWidth = 233;
- beforeEach(() => {
- jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({
- width: mockWidth,
- }));
- timeSeriesChart.vm.onResize();
- });
+ beforeEach(() => {
+ jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({
+ width: mockWidth,
+ }));
+ timeSeriesChart.vm.onResize();
+ });
- it('sets area chart width', () => {
- expect(timeSeriesChart.vm.width).toBe(mockWidth);
+ it('sets area chart width', () => {
+ expect(timeSeriesChart.vm.width).toBe(mockWidth);
+ });
});
});
- });
- describe('computed', () => {
- const getChartOptions = () => findChart().props('option');
+ describe('computed', () => {
+ const getChartOptions = () => findChart().props('option');
- describe('chartData', () => {
- let chartData;
- const seriesData = () => chartData[0];
+ describe('chartData', () => {
+ let chartData;
+ const seriesData = () => chartData[0];
- beforeEach(() => {
- ({ chartData } = timeSeriesChart.vm);
- });
+ beforeEach(() => {
+ ({ chartData } = timeSeriesChart.vm);
+ });
- it('utilizes all data points', () => {
- const { values } = mockGraphData.metrics[0].result[0];
+ it('utilizes all data points', () => {
+ const { values } = mockGraphData.metrics[0].result[0];
- expect(chartData.length).toBe(1);
- expect(seriesData().data.length).toBe(values.length);
- });
+ expect(chartData.length).toBe(1);
+ expect(seriesData().data.length).toBe(values.length);
+ });
- it('creates valid data', () => {
- const { data } = seriesData();
+ it('creates valid data', () => {
+ const { data } = seriesData();
- expect(
- data.filter(
- ([time, value]) => new Date(time).getTime() > 0 && typeof value === 'number',
- ).length,
- ).toBe(data.length);
- });
+ expect(
+ data.filter(
+ ([time, value]) => new Date(time).getTime() > 0 && typeof value === 'number',
+ ).length,
+ ).toBe(data.length);
+ });
- it('formats line width correctly', () => {
- expect(chartData[0].lineStyle.width).toBe(2);
- });
+ it('formats line width correctly', () => {
+ expect(chartData[0].lineStyle.width).toBe(2);
+ });
- it('formats line color correctly', () => {
- expect(chartData[0].lineStyle.color).toBe(chartColorValues[0]);
+ it('formats line color correctly', () => {
+ expect(chartData[0].lineStyle.color).toBe(chartColorValues[0]);
+ });
});
- });
- describe('chartOptions', () => {
- describe('are extended by `option`', () => {
- const mockSeriesName = 'Extra series 1';
- const mockOption = {
- option1: 'option1',
- option2: 'option2',
- };
-
- it('arbitrary options', () => {
- timeSeriesChart.setProps({
- option: mockOption,
- });
+ describe('chartOptions', () => {
+ describe('are extended by `option`', () => {
+ const mockSeriesName = 'Extra series 1';
+ const mockOption = {
+ option1: 'option1',
+ option2: 'option2',
+ };
- return timeSeriesChart.vm.$nextTick().then(() => {
- expect(getChartOptions()).toEqual(expect.objectContaining(mockOption));
- });
- });
+ it('arbitrary options', () => {
+ timeSeriesChart.setProps({
+ option: mockOption,
+ });
- it('additional series', () => {
- timeSeriesChart.setProps({
- option: {
- series: [
- {
- name: mockSeriesName,
- },
- ],
- },
+ return timeSeriesChart.vm.$nextTick().then(() => {
+ expect(getChartOptions()).toEqual(expect.objectContaining(mockOption));
+ });
});
- return timeSeriesChart.vm.$nextTick().then(() => {
- const optionSeries = getChartOptions().series;
+ it('additional series', () => {
+ timeSeriesChart.setProps({
+ option: {
+ series: [
+ {
+ name: mockSeriesName,
+ },
+ ],
+ },
+ });
+
+ return timeSeriesChart.vm.$nextTick().then(() => {
+ const optionSeries = getChartOptions().series;
- expect(optionSeries.length).toEqual(2);
- expect(optionSeries[0].name).toEqual(mockSeriesName);
+ expect(optionSeries.length).toEqual(2);
+ expect(optionSeries[0].name).toEqual(mockSeriesName);
+ });
});
- });
- it('additional y axis data', () => {
- const mockCustomYAxisOption = {
- name: 'Custom y axis label',
- axisLabel: {
- formatter: jest.fn(),
- },
- };
+ it('additional y axis data', () => {
+ const mockCustomYAxisOption = {
+ name: 'Custom y axis label',
+ axisLabel: {
+ formatter: jest.fn(),
+ },
+ };
- timeSeriesChart.setProps({
- option: {
- yAxis: mockCustomYAxisOption,
- },
+ timeSeriesChart.setProps({
+ option: {
+ yAxis: mockCustomYAxisOption,
+ },
+ });
+
+ return timeSeriesChart.vm.$nextTick().then(() => {
+ const { yAxis } = getChartOptions();
+
+ expect(yAxis[0]).toMatchObject(mockCustomYAxisOption);
+ });
});
- return timeSeriesChart.vm.$nextTick().then(() => {
- const { yAxis } = getChartOptions();
+ it('additional x axis data', () => {
+ const mockCustomXAxisOption = {
+ name: 'Custom x axis label',
+ };
+
+ timeSeriesChart.setProps({
+ option: {
+ xAxis: mockCustomXAxisOption,
+ },
+ });
+
+ return timeSeriesChart.vm.$nextTick().then(() => {
+ const { xAxis } = getChartOptions();
- expect(yAxis[0]).toMatchObject(mockCustomYAxisOption);
+ expect(xAxis).toMatchObject(mockCustomXAxisOption);
+ });
});
});
- it('additional x axis data', () => {
- const mockCustomXAxisOption = {
- name: 'Custom x axis label',
- };
+ describe('yAxis formatter', () => {
+ let dataFormatter;
+ let deploymentFormatter;
- timeSeriesChart.setProps({
- option: {
- xAxis: mockCustomXAxisOption,
- },
+ beforeEach(() => {
+ dataFormatter = getChartOptions().yAxis[0].axisLabel.formatter;
+ deploymentFormatter = getChartOptions().yAxis[1].axisLabel.formatter;
});
- return timeSeriesChart.vm.$nextTick().then(() => {
- const { xAxis } = getChartOptions();
+ it('rounds to 3 decimal places', () => {
+ expect(dataFormatter(0.88888)).toBe('0.889');
+ });
- expect(xAxis).toMatchObject(mockCustomXAxisOption);
+ it('deployment formatter is set as is required to display a tooltip', () => {
+ expect(deploymentFormatter).toEqual(expect.any(Function));
});
});
});
- describe('yAxis formatter', () => {
- let dataFormatter;
- let deploymentFormatter;
+ describe('deploymentSeries', () => {
+ it('utilizes deployment data', () => {
+ expect(timeSeriesChart.vm.deploymentSeries.yAxisIndex).toBe(1); // same as deployment y axis
+ expect(timeSeriesChart.vm.deploymentSeries.data).toEqual([
+ ['2019-07-16T10:14:25.589Z', expect.any(Number)],
+ ['2019-07-16T11:14:25.589Z', expect.any(Number)],
+ ['2019-07-16T12:14:25.589Z', expect.any(Number)],
+ ]);
- beforeEach(() => {
- dataFormatter = getChartOptions().yAxis[0].axisLabel.formatter;
- deploymentFormatter = getChartOptions().yAxis[1].axisLabel.formatter;
+ expect(timeSeriesChart.vm.deploymentSeries.symbolSize).toBe(14);
});
+ });
- it('rounds to 3 decimal places', () => {
- expect(dataFormatter(0.88888)).toBe('0.889');
+ describe('yAxisLabel', () => {
+ it('y axis is configured correctly', () => {
+ const { yAxis } = getChartOptions();
+
+ expect(yAxis).toHaveLength(2);
+
+ const [dataAxis, deploymentAxis] = yAxis;
+
+ expect(dataAxis.boundaryGap).toHaveLength(2);
+ expect(dataAxis.scale).toBe(true);
+
+ expect(deploymentAxis.show).toBe(false);
+ expect(deploymentAxis.min).toEqual(expect.any(Number));
+ expect(deploymentAxis.max).toEqual(expect.any(Number));
+ expect(deploymentAxis.min).toBeLessThan(deploymentAxis.max);
});
- it('deployment formatter is set as is required to display a tooltip', () => {
- expect(deploymentFormatter).toEqual(expect.any(Function));
+ it('constructs a label for the chart y-axis', () => {
+ const { yAxis } = getChartOptions();
+
+ expect(yAxis[0].name).toBe('Memory Used per Pod');
});
});
});
- describe('deploymentSeries', () => {
- it('utilizes deployment data', () => {
- expect(timeSeriesChart.vm.deploymentSeries.yAxisIndex).toBe(1); // same as deployment y axis
- expect(timeSeriesChart.vm.deploymentSeries.data).toEqual([
- ['2019-07-16T10:14:25.589Z', expect.any(Number)],
- ['2019-07-16T11:14:25.589Z', expect.any(Number)],
- ['2019-07-16T12:14:25.589Z', expect.any(Number)],
- ]);
-
- expect(timeSeriesChart.vm.deploymentSeries.symbolSize).toBe(14);
- });
+ afterEach(() => {
+ timeSeriesChart.destroy();
});
+ });
- describe('yAxisLabel', () => {
- it('y axis is configured correctly', () => {
- const { yAxis } = getChartOptions();
+ describe('wrapped components', () => {
+ const glChartComponents = [
+ {
+ chartType: 'area-chart',
+ component: GlAreaChart,
+ },
+ {
+ chartType: 'line-chart',
+ component: GlLineChart,
+ },
+ ];
- expect(yAxis).toHaveLength(2);
+ glChartComponents.forEach(dynamicComponent => {
+ describe(`GitLab UI: ${dynamicComponent.chartType}`, () => {
+ let timeSeriesAreaChart;
+ const findChartComponent = () => timeSeriesAreaChart.find(dynamicComponent.component);
- const [dataAxis, deploymentAxis] = yAxis;
+ beforeEach(done => {
+ timeSeriesAreaChart = makeTimeSeriesChart(mockGraphData, dynamicComponent.chartType);
+ timeSeriesAreaChart.vm.$nextTick(done);
+ });
- expect(dataAxis.boundaryGap).toHaveLength(2);
- expect(dataAxis.scale).toBe(true);
+ afterEach(() => {
+ timeSeriesAreaChart.destroy();
+ });
- expect(deploymentAxis.show).toBe(false);
- expect(deploymentAxis.min).toEqual(expect.any(Number));
- expect(deploymentAxis.max).toEqual(expect.any(Number));
- expect(deploymentAxis.min).toBeLessThan(deploymentAxis.max);
- });
+ it('is a Vue instance', () => {
+ expect(findChartComponent().exists()).toBe(true);
+ expect(findChartComponent().isVueInstance()).toBe(true);
+ });
- it('constructs a label for the chart y-axis', () => {
- const { yAxis } = getChartOptions();
+ it('receives data properties needed for proper chart render', () => {
+ const props = findChartComponent().props();
- expect(yAxis[0].name).toBe('Memory Used per Pod');
- });
- });
- });
+ expect(props.data).toBe(timeSeriesAreaChart.vm.chartData);
+ expect(props.option).toBe(timeSeriesAreaChart.vm.chartOptions);
+ expect(props.formatTooltipText).toBe(timeSeriesAreaChart.vm.formatTooltipText);
+ expect(props.thresholds).toBe(timeSeriesAreaChart.vm.thresholds);
+ });
- afterEach(() => {
- timeSeriesChart.destroy();
- });
- });
+ it('recieves a tooltip title', done => {
+ const mockTitle = 'mockTitle';
+ timeSeriesAreaChart.vm.tooltip.title = mockTitle;
- describe('wrapped components', () => {
- const glChartComponents = [
- {
- chartType: 'area-chart',
- component: GlAreaChart,
- },
- {
- chartType: 'line-chart',
- component: GlLineChart,
- },
- ];
+ timeSeriesAreaChart.vm.$nextTick(() => {
+ expect(
+ shallowWrapperContainsSlotText(findChartComponent(), 'tooltipTitle', mockTitle),
+ ).toBe(true);
+ done();
+ });
+ });
- glChartComponents.forEach(dynamicComponent => {
- describe(`GitLab UI: ${dynamicComponent.chartType}`, () => {
- let timeSeriesAreaChart;
- const findChartComponent = () => timeSeriesAreaChart.find(dynamicComponent.component);
+ describe('when tooltip is showing deployment data', () => {
+ const mockSha = 'mockSha';
+ const commitUrl = `${mockProjectDir}/-/commit/${mockSha}`;
- beforeEach(done => {
- timeSeriesAreaChart = makeTimeSeriesChart(mockGraphData, dynamicComponent.chartType);
- timeSeriesAreaChart.vm.$nextTick(done);
- });
+ beforeEach(done => {
+ timeSeriesAreaChart.vm.tooltip.isDeployment = true;
+ timeSeriesAreaChart.vm.$nextTick(done);
+ });
- afterEach(() => {
- timeSeriesAreaChart.destroy();
- });
+ it('uses deployment title', () => {
+ expect(
+ shallowWrapperContainsSlotText(findChartComponent(), 'tooltipTitle', 'Deployed'),
+ ).toBe(true);
+ });
- it('is a Vue instance', () => {
- expect(findChartComponent().exists()).toBe(true);
- expect(findChartComponent().isVueInstance()).toBe(true);
- });
+ it('renders clickable commit sha in tooltip content', done => {
+ timeSeriesAreaChart.vm.tooltip.sha = mockSha;
+ timeSeriesAreaChart.vm.tooltip.commitUrl = commitUrl;
- it('receives data properties needed for proper chart render', () => {
- const props = findChartComponent().props();
+ timeSeriesAreaChart.vm.$nextTick(() => {
+ const commitLink = timeSeriesAreaChart.find(GlLink);
- expect(props.data).toBe(timeSeriesAreaChart.vm.chartData);
- expect(props.option).toBe(timeSeriesAreaChart.vm.chartOptions);
- expect(props.formatTooltipText).toBe(timeSeriesAreaChart.vm.formatTooltipText);
- expect(props.thresholds).toBe(timeSeriesAreaChart.vm.thresholds);
+ expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true);
+ expect(commitLink.attributes('href')).toEqual(commitUrl);
+ done();
+ });
+ });
+ });
});
+ });
+ });
+ });
- it('recieves a tooltip title', done => {
- const mockTitle = 'mockTitle';
- timeSeriesAreaChart.vm.tooltip.title = mockTitle;
+ describe('with multiple time series', () => {
+ const mockedResultMultipleSeries = [];
+ const [, , panelData] = metricsDashboardPayload.panel_groups[1].panels;
- timeSeriesAreaChart.vm.$nextTick(() => {
- expect(
- shallowWrapperContainsSlotText(findChartComponent(), 'tooltipTitle', mockTitle),
- ).toBe(true);
- done();
- });
- });
+ for (let i = 0; i < panelData.metrics.length; i += 1) {
+ mockedResultMultipleSeries.push(cloneDeep(mockedQueryResultPayload));
+ mockedResultMultipleSeries[
+ i
+ ].metricId = `${panelData.metrics[i].metric_id}_${panelData.metrics[i].id}`;
+ }
- describe('when tooltip is showing deployment data', () => {
- const mockSha = 'mockSha';
- const commitUrl = `${mockProjectDir}/-/commit/${mockSha}`;
+ beforeEach(() => {
+ setTestTimeout(1000);
- beforeEach(done => {
- timeSeriesAreaChart.vm.tooltip.isDeployment = true;
- timeSeriesAreaChart.vm.$nextTick(done);
- });
+ store = createStore();
- it('uses deployment title', () => {
- expect(
- shallowWrapperContainsSlotText(findChartComponent(), 'tooltipTitle', 'Deployed'),
- ).toBe(true);
- });
+ store.commit(
+ `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
+ metricsDashboardPayload,
+ );
- it('renders clickable commit sha in tooltip content', done => {
- timeSeriesAreaChart.vm.tooltip.sha = mockSha;
- timeSeriesAreaChart.vm.tooltip.commitUrl = commitUrl;
+ store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
- timeSeriesAreaChart.vm.$nextTick(() => {
- const commitLink = timeSeriesAreaChart.find(GlLink);
+ // Mock data contains the metric_id for a multiple time series panel
+ for (let i = 0; i < panelData.metrics.length; i += 1) {
+ store.commit(
+ `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`,
+ mockedResultMultipleSeries[i],
+ );
+ }
- expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true);
- expect(commitLink.attributes('href')).toEqual(commitUrl);
- done();
- });
- });
+ // Pick the second panel group and the second panel in it
+ [, , mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[1].panels;
+ });
+
+ describe('General functions', () => {
+ let timeSeriesChart;
+
+ beforeEach(done => {
+ timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart');
+ timeSeriesChart.vm.$nextTick(done);
+ });
+
+ describe('computed', () => {
+ let chartData;
+
+ beforeEach(() => {
+ ({ chartData } = timeSeriesChart.vm);
+ });
+
+ it('should contain different colors for each time series', () => {
+ expect(chartData[0].lineStyle.color).toBe('#1f78d1');
+ expect(chartData[1].lineStyle.color).toBe('#1aaa55');
+ expect(chartData[2].lineStyle.color).toBe('#fc9403');
+ expect(chartData[3].lineStyle.color).toBe('#6d49cb');
+ expect(chartData[4].lineStyle.color).toBe('#1f78d1');
});
});
});
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 15c82242262..fcf70a1af63 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -22,7 +22,7 @@ import {
} from '../mock_data';
const localVue = createLocalVue();
-const expectedPanelCount = 2;
+const expectedPanelCount = 3;
describe('Dashboard', () => {
let store;
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index 4d83933f2b8..bad3962dd8f 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -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,
+ },
+ ],
+ },
],
},
],
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index d9aebafb9ec..3fb7b84fae5 100644
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -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);
diff --git a/spec/frontend/releases/components/app_edit_spec.js b/spec/frontend/releases/components/app_edit_spec.js
index cb940facbd6..b2dbb8cc435 100644
--- a/spec/frontend/releases/components/app_edit_spec.js
+++ b/spec/frontend/releases/components/app_edit_spec.js
@@ -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,
diff --git a/spec/frontend/releases/components/evidence_block_spec.js b/spec/frontend/releases/components/evidence_block_spec.js
index 7b896575965..fb62f4a3bfe 100644
--- a/spec/frontend/releases/components/evidence_block_spec.js
+++ b/spec/frontend/releases/components/evidence_block_spec.js
@@ -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,
);
});
});
diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js
index 4125d5c7e74..c63637c4cae 100644
--- a/spec/frontend/releases/components/release_block_footer_spec.js
+++ b/spec/frontend/releases/components/release_block_footer_spec.js
@@ -24,7 +24,7 @@ describe('Release block footer', () => {
const factory = (props = {}) => {
wrapper = mount(ReleaseBlockFooter, {
propsData: {
- ...convertObjectPropsToCamelCase(releaseClone),
+ ...convertObjectPropsToCamelCase(releaseClone, { deep: true }),
...props,
},
});
diff --git a/spec/frontend/releases/components/release_block_header_spec.js b/spec/frontend/releases/components/release_block_header_spec.js
index 157df15ff3c..78adad13f69 100644
--- a/spec/frontend/releases/components/release_block_header_spec.js
+++ b/spec/frontend/releases/components/release_block_header_spec.js
@@ -1,5 +1,5 @@
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', () => {
diff --git a/spec/frontend/releases/components/release_block_milestone_info_spec.js b/spec/frontend/releases/components/release_block_milestone_info_spec.js
index 5a3204a4ce2..10f5db96b31 100644
--- a/spec/frontend/releases/components/release_block_milestone_info_spec.js
+++ b/spec/frontend/releases/components/release_block_milestone_info_spec.js
@@ -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();
diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js
index aba1b8aff41..5d365b77560 100644
--- a/spec/frontend/releases/components/release_block_spec.js
+++ b/spec/frontend/releases/components/release_block_spec.js
@@ -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')
diff --git a/spec/graphql/resolvers/boards_resolver_spec.rb b/spec/graphql/resolvers/boards_resolver_spec.rb
index ab77dfa8fc3..02d6f808118 100644
--- a/spec/graphql/resolvers/boards_resolver_spec.rb
+++ b/spec/graphql/resolvers/boards_resolver_spec.rb
@@ -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
diff --git a/spec/javascripts/filtered_search/visual_token_value_spec.js b/spec/javascripts/filtered_search/visual_token_value_spec.js
index a039e280028..4469ade1874 100644
--- a/spec/javascripts/filtered_search/visual_token_value_spec.js
+++ b/spec/javascripts/filtered_search/visual_token_value_spec.js
@@ -1,4 +1,4 @@
-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);
diff --git a/spec/javascripts/releases/components/app_index_spec.js b/spec/javascripts/releases/components/app_index_spec.js
index bcf062f357a..962fe9c448d 100644
--- a/spec/javascripts/releases/components/app_index_spec.js
+++ b/spec/javascripts/releases/components/app_index_spec.js
@@ -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(() => {
diff --git a/spec/javascripts/releases/stores/modules/list/actions_spec.js b/spec/javascripts/releases/stores/modules/list/actions_spec.js
index 037c9d8d54a..bf85e18997b 100644
--- a/spec/javascripts/releases/stores/modules/list/actions_spec.js
+++ b/spec/javascripts/releases/stores/modules/list/actions_spec.js
@@ -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', () => {
diff --git a/spec/requests/api/graphql/boards/boards_query_spec.rb b/spec/requests/api/graphql/boards/boards_query_spec.rb
index d0a2d0fffaf..a17554aba21 100644
--- a/spec/requests/api/graphql/boards/boards_query_spec.rb
+++ b/spec/requests/api/graphql/boards/boards_query_spec.rb
@@ -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)
diff --git a/spec/support/shared_contexts/requests/api/graphql/group_and_project_boards_query_shared_context.rb b/spec/support/shared_contexts/requests/api/graphql/group_and_project_boards_query_shared_context.rb
index e744c3d0abb..ca77c68c130 100644
--- a/spec/support/shared_contexts/requests/api/graphql/group_and_project_boards_query_shared_context.rb
+++ b/spec/support/shared_contexts/requests/api/graphql/group_and_project_boards_query_shared_context.rb
@@ -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')
diff --git a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
index 6044fefd2f7..90ac60a6fe7 100644
--- a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
@@ -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
diff --git a/spec/views/profiles/preferences/show.html.haml_spec.rb b/spec/views/profiles/preferences/show.html.haml_spec.rb
index e3eb822b045..16e4bd9c6d1 100644
--- a/spec/views/profiles/preferences/show.html.haml_spec.rb
+++ b/spec/views/profiles/preferences/show.html.haml_spec.rb
@@ -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