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:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-12-19 14:01:45 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-12-19 14:01:45 +0300
commit9297025d0b7ddf095eb618dfaaab2ff8f2018d8b (patch)
tree865198c01d1824a9b098127baa3ab980c9cd2c06 /app/assets/javascripts/ci
parent6372471f43ee03c05a7c1f8b0c6ac6b8a7431dbe (diff)
Add latest changes from gitlab-org/gitlab@16-7-stable-eev16.7.0-rc42
Diffstat (limited to 'app/assets/javascripts/ci')
-rw-r--r--app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue7
-rw-r--r--app/assets/javascripts/ci/artifacts/constants.js1
-rw-r--r--app/assets/javascripts/ci/artifacts/index.js3
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue2
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue11
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue6
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue19
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue4
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_header.vue13
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue14
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_search.vue81
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue84
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/empty_state.vue64
-rw-r--r--app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue13
-rw-r--r--app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue91
-rw-r--r--app/assets/javascripts/ci/catalog/constants.js40
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql7
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/mutations/client/update_current_page.mutation.graphql7
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/client/get_current_page.query.graphql5
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql23
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql5
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql5
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql4
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql18
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/settings.js44
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/typedefs.graphql11
-rw-r--r--app/assets/javascripts/ci/catalog/index.js3
-rw-r--r--app/assets/javascripts/ci/catalog/router/routes.js2
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue125
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/constants.js4
-rw-r--r--app/assets/javascripts/ci/common/private/job_name_component.vue10
-rw-r--r--app/assets/javascripts/ci/job_details/components/environments_block.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/components/job_header.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/components/job_log_controllers.vue71
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/collapsible_section.vue71
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line_header.vue11
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line_number.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/log.vue86
-rw-r--r--app/assets/javascripts/ci/job_details/components/manual_variables_form.vue5
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue38
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/index.js25
-rw-r--r--app/assets/javascripts/ci/job_details/job_app.vue19
-rw-r--r--app/assets/javascripts/ci/job_details/store/actions.js168
-rw-r--r--app/assets/javascripts/ci/job_details/store/getters.js3
-rw-r--r--app/assets/javascripts/ci/job_details/store/mutation_types.js10
-rw-r--r--app/assets/javascripts/ci/job_details/store/mutations.js54
-rw-r--r--app/assets/javascripts/ci/job_details/store/state.js13
-rw-r--r--app/assets/javascripts/ci/job_details/store/utils.js258
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue5
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue2
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql1
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/constants.js9
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/graphql/fragments/pipeline_header.fragment.graphql4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql18
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue109
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js44
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipelines_index.js2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js23
-rw-r--r--app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue15
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue42
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue5
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue7
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue62
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/constants.js11
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/index.js2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/options.js4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue16
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue3
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue97
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue8
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/actions.js30
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/getters.js63
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/index.js18
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/mutation_types.js5
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/mutations.js27
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/state.js16
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/utils/codequality_parser.js (renamed from app/assets/javascripts/ci/reports/codequality_report/store/utils/codequality_parser.js)0
-rw-r--r--app/assets/javascripts/ci/reports/components/report_section.vue1
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue9
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue4
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_job_count.vue36
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_jobs_table.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_list.vue5
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_list_header.vue2
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql1
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/runner_job_count.query.graphql6
-rw-r--r--app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue2
103 files changed, 1335 insertions, 985 deletions
diff --git a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
index de37aa431e6..3a0fd376d3c 100644
--- a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
+++ b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
@@ -10,7 +10,7 @@ import {
GlFormCheckbox,
GlTooltipDirective,
} from '@gitlab/ui';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import { createAlert } from '~/alert';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -40,7 +40,6 @@ import {
I18N_BULK_DELETE_ERROR,
I18N_BULK_DELETE_PARTIAL_ERROR,
I18N_BULK_DELETE_CONFIRMATION_TOAST,
- SELECTED_ARTIFACTS_MAX_COUNT,
I18N_BULK_DELETE_MAX_SELECTED,
} from '../constants';
import JobCheckbox from './job_checkbox.vue';
@@ -77,7 +76,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: ['projectId', 'projectPath', 'canDestroyArtifacts'],
+ inject: ['projectId', 'projectPath', 'canDestroyArtifacts', 'jobArtifactsCountLimit'],
apollo: {
jobArtifacts: {
query: getJobArtifactsQuery,
@@ -151,7 +150,7 @@ export default {
return Boolean(this.selectedArtifacts.length);
},
isSelectedArtifactsLimitReached() {
- return this.selectedArtifacts.length >= SELECTED_ARTIFACTS_MAX_COUNT;
+ return this.selectedArtifacts.length >= this.jobArtifactsCountLimit;
},
canBulkDestroyArtifacts() {
return this.canDestroyArtifacts;
diff --git a/app/assets/javascripts/ci/artifacts/constants.js b/app/assets/javascripts/ci/artifacts/constants.js
index 28c371cda1e..166946035d1 100644
--- a/app/assets/javascripts/ci/artifacts/constants.js
+++ b/app/assets/javascripts/ci/artifacts/constants.js
@@ -47,7 +47,6 @@ export const I18N_MODAL_BODY = s__(
export const I18N_MODAL_PRIMARY = s__('Artifacts|Delete artifact');
export const I18N_MODAL_CANCEL = __('Cancel');
-export const SELECTED_ARTIFACTS_MAX_COUNT = 50;
export const I18N_BULK_DELETE_MAX_SELECTED = s__(
'Artifacts|Maximum selected artifacts limit reached',
);
diff --git a/app/assets/javascripts/ci/artifacts/index.js b/app/assets/javascripts/ci/artifacts/index.js
index c6021eb056f..0a84b94f5fa 100644
--- a/app/assets/javascripts/ci/artifacts/index.js
+++ b/app/assets/javascripts/ci/artifacts/index.js
@@ -19,7 +19,7 @@ export const initArtifactsTable = () => {
return false;
}
- const { projectPath, projectId, canDestroyArtifacts } = el.dataset;
+ const { projectPath, projectId, canDestroyArtifacts, jobArtifactsCountLimit } = el.dataset;
return new Vue({
el,
@@ -28,6 +28,7 @@ export const initArtifactsTable = () => {
projectPath,
projectId,
canDestroyArtifacts: parseBoolean(canDestroyArtifacts),
+ jobArtifactsCountLimit: parseInt(jobArtifactsCountLimit, 10),
},
render: (createElement) => createElement(App),
});
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue
index 572a8183730..349ce761d25 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue
@@ -101,7 +101,7 @@ export default {
<span
v-for="item in projectInfoItems"
:key="`${item.icon}`"
- class="gl-display-flex gl-align-items-center gl-xs-mb-3"
+ class="gl-display-flex gl-align-items-center gl-mb-3 gl-sm-mb-0"
>
<gl-icon class="gl-text-primary gl-mr-2" :name="item.icon" />
<div
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
index fbc7ddf5c91..6d062d8b7f1 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
@@ -12,7 +12,7 @@ export default {
GlTableLite,
},
props: {
- resourceId: {
+ resourcePath: {
type: String,
required: true,
},
@@ -27,11 +27,11 @@ export default {
query: getCiCatalogResourceComponents,
variables() {
return {
- id: this.resourceId,
+ fullPath: this.resourcePath,
};
},
update(data) {
- return data?.ciCatalogResource?.components?.nodes || [];
+ return data?.ciCatalogResource?.latestVersion?.components?.nodes || [];
},
error() {
createAlert({ message: this.$options.i18n.fetchError });
@@ -64,7 +64,7 @@ export default {
thClass: 'gl-w-40p',
},
{
- key: 'defaultValue',
+ key: 'default',
label: s__('CiCatalogComponent|Default Value'),
thClass: 'gl-w-40p',
},
@@ -103,7 +103,6 @@ export default {
data-testid="component-section"
>
<h3 class="gl-font-size-h2" data-testid="component-name">{{ component.name }}</h3>
- <p class="gl-mt-5">{{ component.description }}</p>
<div class="gl-display-flex">
<pre
class="gl-w-85p gl-py-4 gl-display-flex gl-justify-content-space-between gl-m-0 gl-border-r-none"
@@ -124,7 +123,7 @@ export default {
</div>
<div class="gl-mt-5">
<b class="gl-display-block gl-mb-4"> {{ $options.i18n.inputTitle }}</b>
- <gl-table-lite :items="component.inputs.nodes" :fields="$options.fields">
+ <gl-table-lite :items="component.inputs" :fields="$options.fields">
<template #cell(required)="{ item }">
{{ humanizeBoolean(item.required) }}
</template>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
index 026a30988fd..b1170b13ef6 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
@@ -14,7 +14,7 @@ export default {
},
mixins: [glFeatureFlagsMixin()],
props: {
- resourceId: {
+ resourcePath: {
type: String,
required: true,
},
@@ -31,10 +31,10 @@ export default {
<template>
<gl-tabs>
<gl-tab :title="$options.i18n.tabs.readme" lazy>
- <ci-resource-readme :resource-id="resourceId" />
+ <ci-resource-readme :resource-path="resourcePath" />
</gl-tab>
<gl-tab v-if="glFeatures.ciCatalogComponentsTab" :title="$options.i18n.tabs.components" lazy>
- <ci-resource-components :resource-id="resourceId"
+ <ci-resource-components :resource-path="resourcePath"
/></gl-tab>
</gl-tabs>
</template>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
index 29009c14e1b..b9d6173a777 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
@@ -1,8 +1,8 @@
<script>
import { GlAvatar, GlAvatarLink, GlBadge } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { isNumeric } from '~/lib/utils/number_utils';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import CiResourceAbout from './ci_resource_about.vue';
import CiResourceHeaderSkeletonLoader from './ci_resource_header_skeleton_loader.vue';
@@ -48,9 +48,6 @@ export default {
entityId() {
return getIdFromGraphQLId(this.resource.id);
},
- fullPath() {
- return `${this.rootNamespace.fullPath}/${this.rootNamespace.name}`;
- },
hasLatestVersion() {
return this.latestVersion?.tagName;
},
@@ -60,13 +57,11 @@ export default {
latestVersion() {
return this.resource.latestVersion;
},
- rootNamespace() {
- return this.resource.rootNamespace;
- },
versionBadgeText() {
- return isNumeric(this.latestVersion.tagName)
- ? `v${this.latestVersion.tagName}`
- : this.latestVersion.tagName;
+ return this.latestVersion.tagName;
+ },
+ webPath() {
+ return cleanLeadingSeparator(this.resource?.webPath);
},
},
};
@@ -89,7 +84,7 @@ export default {
class="gl-display-flex gl-flex-direction-column gl-align-items-flex-start gl-justify-content-center"
>
<div class="gl-font-sm gl-text-secondary">
- {{ fullPath }}
+ {{ webPath }}
</div>
<span class="gl-display-flex">
<div class="gl-font-lg gl-font-weight-bold">{{ resource.name }}</div>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue
index d473833869d..343b555c4d8 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue
@@ -11,7 +11,7 @@ export default {
},
directives: { SafeHtml },
props: {
- resourceId: {
+ resourcePath: {
type: String,
required: true,
},
@@ -26,7 +26,7 @@ export default {
query: getCiCatalogResourceReadme,
variables() {
return {
- id: this.resourceId,
+ fullPath: this.resourcePath,
};
},
update(data) {
diff --git a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
index db84eaa82c2..3a9ec341789 100644
--- a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
@@ -2,15 +2,17 @@
import { GlBanner, GlLink } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
+import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue';
import { CATALOG_FEEDBACK_DISMISSED_KEY } from '../../constants';
const defaultTitle = __('CI/CD Catalog');
const defaultDescription = s__(
- 'CiCatalog|Discover CI configuration resources for a seamless CI/CD experience.',
+ 'CiCatalog|Discover CI/CD components that can improve your pipeline with additional functionality.',
);
export default {
components: {
+ BetaBadge,
GlBanner,
GlLink,
},
@@ -45,7 +47,7 @@ export default {
};
</script>
<template>
- <div class="gl-border-b-1 gl-border-gray-100 gl-border-b-solid">
+ <div class="page-title-holder">
<gl-banner
v-if="!isFeedbackBannerDismissed"
class="gl-mt-5"
@@ -58,9 +60,12 @@ export default {
{{ $options.i18n.banner.description }}
</p>
</gl-banner>
- <h1 class="gl-font-size-h-display">{{ pageTitle }}</h1>
+ <div class="gl-my-4 gl-display-flex gl-align-items-center">
+ <h1 class="gl-m-0 gl-font-size-h-display">{{ pageTitle }}</h1>
+ <beta-badge class="gl-ml-3" />
+ </div>
<p>
- <span data-testid="description">{{ pageDescription }}</span>
+ <span data-testid="page-description">{{ pageDescription }}</span>
<gl-link :href="$options.learnMorePath" target="_blank">{{
$options.i18n.learnMore
}}</gl-link>
diff --git a/app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue
index 3722b8e6c59..5de71fa1fc5 100644
--- a/app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue
@@ -37,21 +37,19 @@ export default {
>
<!-- Catalog project avatar -->
<rect x="0" y="0" width="48" height="48" rx="4" ry="4" />
- <!-- namespace path -->
- <rect x="60" y="4" width="400" height="16" rx="2" ry="2" />
+ <!-- resource path -->
+ <rect x="60" y="0" width="200" height="10" rx="2" ry="2" />
+ <!-- resource name -->
+ <rect x="60" y="14" width="400" height="16" rx="2" ry="2" />
<!-- Project description -->
- <rect x="60" y="30" width="500" height="12" rx="2" ry="2" />
+ <rect x="60" y="34" width="500" height="12" rx="2" ry="2" />
<!-- Release date line -->
<rect :x="coordinates.releaseDateX" y="30" width="200" height="12" rx="2" ry="2" />
<!-- Favorites -->
- <rect :x="coordinates.statsX" y="4" width="16" height="16" rx="2" ry="2" />
- <rect :x="coordinates.statsX + 18" y="7" width="18" height="10" rx="2" ry="2" />
-
- <!-- Forks -->
<rect :x="coordinates.statsX + 50" y="4" width="16" height="16" rx="2" ry="2" />
- <rect :x="coordinates.statsX + 68" y="7" width="18" height="10" rx="2" ry="2" />
+ <rect :x="coordinates.statsX + 70" y="7" width="18" height="10" rx="2" ry="2" />
</gl-skeleton-loader>
</div>
</template>
diff --git a/app/assets/javascripts/ci/catalog/components/list/catalog_search.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_search.vue
new file mode 100644
index 00000000000..e074cfda6f7
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_search.vue
@@ -0,0 +1,81 @@
+<script>
+import { GlSearchBoxByClick, GlSorting, GlSortingItem } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { SORT_ASC, SORT_DESC, SORT_OPTION_CREATED } from '../../constants';
+
+export default {
+ components: {
+ GlSearchBoxByClick,
+ GlSorting,
+ GlSortingItem,
+ },
+ data() {
+ return {
+ currentSortOption: SORT_OPTION_CREATED,
+ isAscending: false,
+ searchTerm: '',
+ };
+ },
+ computed: {
+ currentSortDirection() {
+ return this.isAscending ? SORT_ASC : SORT_DESC;
+ },
+ currentSorting() {
+ return `${this.currentSortOption}_${this.currentSortDirection}`;
+ },
+ currentSortText() {
+ const currentSort = this.$options.sortOptions.find(
+ (sort) => sort.key === this.currentSortOption,
+ );
+ return currentSort.text;
+ },
+ },
+ watch: {
+ currentSorting(newSorting) {
+ this.$emit('update-sorting', newSorting);
+ },
+ },
+ methods: {
+ isActiveSort(sortItem) {
+ return sortItem === this.currentSortOption;
+ },
+ onClear() {
+ this.$emit('update-search-term', '');
+ },
+ onSortDirectionChange() {
+ this.isAscending = !this.isAscending;
+ },
+ onSubmitSearch() {
+ this.$emit('update-search-term', this.searchTerm);
+ },
+ setSelectedSortOption(sortingItem) {
+ this.currentSortOption = sortingItem.key;
+ },
+ },
+ sortOptions: [{ key: SORT_OPTION_CREATED, text: __('Created at') }],
+};
+</script>
+<template>
+ <div class="gl-display-flex gl-gap-3">
+ <gl-search-box-by-click
+ v-model="searchTerm"
+ data-testid="catalog-search-bar"
+ @submit="onSubmitSearch"
+ @clear="onClear"
+ />
+ <gl-sorting
+ :is-ascending="isAscending"
+ :text="currentSortText"
+ @sortDirectionChange="onSortDirectionChange"
+ >
+ <gl-sorting-item
+ v-for="sortingItem in $options.sortOptions"
+ :key="sortingItem.key"
+ :active="isActiveSort(sortingItem.key)"
+ @click="setSelectedSortOption(sortingItem)"
+ >
+ {{ sortingItem.text }}
+ </gl-sorting-item>
+ </gl-sorting>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
index 080955b4322..57d19af614f 100644
--- a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
@@ -1,16 +1,9 @@
<script>
-import {
- GlAvatar,
- GlBadge,
- GlButton,
- GlIcon,
- GlLink,
- GlSprintf,
- GlTooltipDirective,
-} from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { GlAvatar, GlBadge, GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+import { s__, n__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
+import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import { CI_RESOURCE_DETAILS_PAGE_NAME } from '../../router/constants';
export default {
@@ -21,7 +14,6 @@ export default {
components: {
GlAvatar,
GlBadge,
- GlButton,
GlIcon,
GlLink,
GlSprintf,
@@ -42,12 +34,27 @@ export default {
authorProfileUrl() {
return this.latestVersion.author.webUrl;
},
+ resourceId() {
+ return cleanLeadingSeparator(this.resource.webPath);
+ },
+ detailsPageResolved() {
+ return this.$router.resolve({
+ name: CI_RESOURCE_DETAILS_PAGE_NAME,
+ params: { id: this.resourceId },
+ });
+ },
+ detailsPageHref() {
+ return decodeURIComponent(this.detailsPageResolved.href);
+ },
entityId() {
return getIdFromGraphQLId(this.resource.id);
},
starCount() {
return this.resource?.starCount || 0;
},
+ starCountText() {
+ return n__('Star', 'Stars', this.starCount);
+ },
hasReleasedVersion() {
return Boolean(this.latestVersion?.releasedAt);
},
@@ -60,26 +67,33 @@ export default {
releasedAt() {
return getTimeago().format(this.latestVersion?.releasedAt);
},
- resourcePath() {
- return `${this.resource.rootNamespace?.name} / ${this.resource.rootNamespace?.fullPath} / `;
- },
tagName() {
return this.latestVersion?.tagName || this.$options.i18n.unreleased;
},
+ webPath() {
+ return cleanLeadingSeparator(this.resource?.webPath);
+ },
},
methods: {
- navigateToDetailsPage() {
- this.$router.push({
- name: CI_RESOURCE_DETAILS_PAGE_NAME,
- params: { id: this.entityId },
- });
+ navigateToDetailsPage(e) {
+ // Open link in a new tab if any of these modifier key is held down.
+ if (e?.ctrlKey || e?.metaKey) {
+ return;
+ }
+
+ // Override the <a> tag if no modifier key is held down to use Vue router and not
+ // open a new tab.
+ e.preventDefault();
+
+ // Push to the decoded URL to avoid all the / being encoded
+ this.$router.push({ path: decodeURIComponent(this.resourceId) });
},
},
};
</script>
<template>
<li
- class="gl-display-flex gl-display-flex-wrap gl-border-b-1 gl-border-gray-100 gl-border-b-solid gl-text-gray-500 gl-py-3"
+ class="gl-display-flex gl-display-flex-wrap gl-align-items-center gl-border-b-1 gl-border-gray-100 gl-border-b-solid gl-text-gray-500 gl-py-3"
data-testid="catalog-resource-item"
>
<gl-avatar
@@ -92,36 +106,40 @@ export default {
@click="navigateToDetailsPage"
/>
<div class="gl-display-flex gl-flex-direction-column gl-flex-grow-1">
- <div class="gl-display-flex gl-flex-wrap gl-gap-2 gl-mb-2">
- <gl-button
- variant="link"
+ <span class="gl-font-sm gl-mb-1">{{ webPath }}</span>
+ <div class="gl-display-flex gl-flex-wrap gl-gap-2 gl-mb-1">
+ <gl-link
class="gl-text-gray-900! gl-mr-1"
+ :href="detailsPageHref"
data-testid="ci-resource-link"
@click="navigateToDetailsPage"
>
- {{ resourcePath }} <b> {{ resource.name }}</b>
- </gl-button>
+ <b> {{ resource.name }}</b>
+ </gl-link>
<div class="gl-display-flex gl-flex-grow-1 gl-md-justify-content-space-between">
- <gl-badge size="sm">{{ tagName }}</gl-badge>
+ <gl-badge size="sm" class="gl-h-5 gl-align-self-center">{{ tagName }}</gl-badge>
<span class="gl-display-flex gl-align-items-center gl-ml-5">
- <span class="gl--flex-center" data-testid="stats-favorites">
- <gl-icon name="star" :size="14" class="gl-mr-1" />
+ <span
+ v-gl-tooltip.top
+ :title="starCountText"
+ class="gl--flex-center"
+ data-testid="stats-favorites"
+ >
+ <gl-icon name="star-o" :size="14" class="gl-mr-2" />
<span class="gl-mr-3">{{ starCount }}</span>
</span>
</span>
</div>
</div>
<div
- class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-justify-content-space-between"
+ class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-justify-content-space-between gl-font-sm"
>
- <span class="gl-display-flex gl-flex-basis-two-thirds gl-font-sm">{{
- resource.description
- }}</span>
+ <span class="gl-display-flex gl-flex-basis-two-thirds">{{ resource.description }}</span>
<div class="gl-display-flex gl-justify-content-end">
<span v-if="hasReleasedVersion">
<gl-sprintf :message="$options.i18n.releasedMessage">
<template #timeAgo>
- <span v-gl-tooltip.bottom :title="formattedDate">
+ <span v-gl-tooltip.top :title="formattedDate">
{{ releasedAt }}
</span>
</template>
diff --git a/app/assets/javascripts/ci/catalog/components/list/empty_state.vue b/app/assets/javascripts/ci/catalog/components/list/empty_state.vue
index a53ddefaa50..e53a10d8935 100644
--- a/app/assets/javascripts/ci/catalog/components/list/empty_state.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/empty_state.vue
@@ -1,22 +1,70 @@
<script>
-import { GlEmptyState } from '@gitlab/ui';
+import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
+import { COMPONENTS_DOCS_URL } from '~/ci/catalog/constants';
export default {
- i18n: {
- title: s__('CiCatalog|Get started with the CI/CD Catalog'),
- description: s__(
- 'CiCatalog|Create a pipeline component repository and make reusing pipeline configurations faster and easier.',
- ),
- },
name: 'CiCatalogEmptyState',
+ COMPONENTS_DOCS_URL,
components: {
GlEmptyState,
+ GlLink,
+ GlSprintf,
+ },
+ props: {
+ searchTerm: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ searchTitle() {
+ return this.isQueryTooSmall
+ ? this.$options.i18n.searchTooSmall.title
+ : this.$options.i18n.search.title;
+ },
+ isSearching() {
+ return this.searchTerm?.length > 0;
+ },
+ isQueryTooSmall() {
+ return this.isSearching && this.searchTerm?.length < 3;
+ },
+ },
+ i18n: {
+ default: {
+ title: s__('CiCatalog|Get started with the CI/CD Catalog'),
+ description: s__(
+ 'CiCatalog|Create a pipeline component repository and make reusing pipeline configurations faster and easier.',
+ ),
+ },
+ search: {
+ title: s__('CiCatalog|No result found'),
+ description: s__(
+ 'CiCatalog|Edit your search and try again. Or %{linkStart}learn to create a component repository%{linkEnd}.',
+ ),
+ },
+ searchTooSmall: {
+ title: s__('CiCatalog|Search must be at least 3 characters'),
+ },
},
};
</script>
<template>
<div>
- <gl-empty-state :title="$options.i18n.title" :description="$options.i18n.description" />
+ <gl-empty-state v-if="isSearching" :title="searchTitle">
+ <template #description>
+ <gl-sprintf :message="$options.i18n.search.description">
+ <template #link="{ content }">
+ <gl-link :href="$options.COMPONENTS_DOCS_URL" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-empty-state>
+ <gl-empty-state
+ v-else
+ :title="$options.i18n.default.title"
+ :description="$options.i18n.default.description"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue b/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue
index da2c73be900..b7e117f9c26 100644
--- a/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue
+++ b/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue
@@ -2,8 +2,7 @@
import { GlEmptyState } from '@gitlab/ui';
import { s__ } from '~/locale';
import { createAlert } from '~/alert';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { CI_CATALOG_RESOURCE_TYPE } from '../../graphql/settings';
+import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import getCatalogCiResourceDetails from '../../graphql/queries/get_ci_catalog_resource_details.query.graphql';
import getCatalogCiResourceSharedData from '../../graphql/queries/get_ci_catalog_resource_shared_data.query.graphql';
import CiResourceDetails from '../details/ci_resource_details.vue';
@@ -28,7 +27,7 @@ export default {
query: getCatalogCiResourceSharedData,
variables() {
return {
- id: this.graphQLId,
+ fullPath: this.cleanFullPath,
};
},
update(data) {
@@ -43,7 +42,7 @@ export default {
query: getCatalogCiResourceDetails,
variables() {
return {
- id: this.graphQLId,
+ fullPath: this.cleanFullPath,
};
},
update(data) {
@@ -56,8 +55,8 @@ export default {
},
},
computed: {
- graphQLId() {
- return convertToGraphQLId(CI_CATALOG_RESOURCE_TYPE, this.$route.params.id);
+ cleanFullPath() {
+ return cleanLeadingSeparator(this.$route.params.id);
},
isLoadingDetails() {
return this.$apollo.queries.resourceAdditionalDetails.loading;
@@ -103,7 +102,7 @@ export default {
:pipeline-status="pipelineStatus"
:resource="resourceSharedData"
/>
- <ci-resource-details :resource-id="graphQLId" />
+ <ci-resource-details :resource-path="cleanFullPath" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue b/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue
index 5e8727a3ed0..e1c86f38d7e 100644
--- a/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue
+++ b/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue
@@ -1,17 +1,21 @@
<script>
import { createAlert } from '~/alert';
import { s__ } from '~/locale';
-import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue';
-import CatalogListSkeletonLoader from '~/ci/catalog/components/list/catalog_list_skeleton_loader.vue';
-import CiResourcesList from '~/ci/catalog/components/list/ci_resources_list.vue';
-import EmptyState from '~/ci/catalog/components/list/empty_state.vue';
import { ciCatalogResourcesItemsCount } from '~/ci/catalog/graphql/settings';
+import CatalogSearch from '../list/catalog_search.vue';
+import CiResourcesList from '../list/ci_resources_list.vue';
+import CatalogListSkeletonLoader from '../list/catalog_list_skeleton_loader.vue';
+import CatalogHeader from '../list/catalog_header.vue';
+import EmptyState from '../list/empty_state.vue';
import getCatalogResources from '../../graphql/queries/get_ci_catalog_resources.query.graphql';
+import getCurrentPage from '../../graphql/queries/client/get_current_page.query.graphql';
+import updateCurrentPageMutation from '../../graphql/mutations/client/update_current_page.mutation.graphql';
export default {
components: {
CatalogHeader,
CatalogListSkeletonLoader,
+ CatalogSearch,
CiResourcesList,
EmptyState,
},
@@ -19,8 +23,9 @@ export default {
return {
catalogResources: [],
currentPage: 1,
- totalCount: 0,
pageInfo: {},
+ searchTerm: '',
+ totalCount: 0,
};
},
apollo: {
@@ -43,6 +48,12 @@ export default {
createAlert({ message: e.message || this.$options.i18n.fetchError, variant: 'danger' });
},
},
+ currentPage: {
+ query: getCurrentPage,
+ update(data) {
+ return data?.page?.current || 1;
+ },
+ },
},
computed: {
hasResources() {
@@ -51,6 +62,12 @@ export default {
isLoading() {
return this.$apollo.queries.catalogResources.loading;
},
+ isSearching() {
+ return this.searchTerm?.length > 0;
+ },
+ showEmptyState() {
+ return !this.hasResources && !this.isSearching;
+ },
},
methods: {
async handlePrevPage() {
@@ -63,7 +80,7 @@ export default {
},
});
- this.currentPage -= 1;
+ this.decrementPage();
} catch (e) {
// Ensure that the current query is properly stoped if an error occurs.
this.$apollo.queries.catalogResources.stop();
@@ -78,7 +95,7 @@ export default {
},
});
- this.currentPage += 1;
+ this.incrementPage();
} catch (e) {
// Ensure that the current query is properly stoped if an error occurs.
this.$apollo.queries.catalogResources.stop();
@@ -86,6 +103,36 @@ export default {
createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' });
}
},
+ updatePageCount(pageNumber) {
+ this.$apollo.mutate({
+ mutation: updateCurrentPageMutation,
+ variables: {
+ pageNumber,
+ },
+ });
+ },
+ decrementPage() {
+ this.updatePageCount(this.currentPage - 1);
+ },
+ incrementPage() {
+ this.updatePageCount(this.currentPage + 1);
+ },
+ onUpdateSearchTerm(searchTerm) {
+ this.searchTerm = !searchTerm.length ? null : searchTerm;
+ this.resetPageCount();
+ this.$apollo.queries.catalogResources.refetch({
+ searchTerm: this.searchTerm,
+ });
+ },
+ onUpdateSorting(sortValue) {
+ this.resetPageCount();
+ this.$apollo.queries.catalogResources.refetch({
+ sortValue,
+ });
+ },
+ resetPageCount() {
+ this.updatePageCount(1);
+ },
},
i18n: {
fetchError: s__('CiCatalog|There was an error fetching CI/CD Catalog resources.'),
@@ -95,18 +142,24 @@ export default {
<template>
<div>
<catalog-header />
- <catalog-list-skeleton-loader v-if="isLoading" class="gl-w-full gl-mt-3" />
- <empty-state v-else-if="!hasResources" />
- <ci-resources-list
- v-else
- :current-page="currentPage"
- :page-info="pageInfo"
- :prev-text="__('Prev')"
- :next-text="__('Next')"
- :resources="catalogResources"
- :total-count="totalCount"
- @onPrevPage="handlePrevPage"
- @onNextPage="handleNextPage"
+ <catalog-search
+ class="gl-py-4 gl-border-b-1 gl-border-gray-100 gl-border-b-solid gl-border-t-1 gl-border-t-solid"
+ @update-search-term="onUpdateSearchTerm"
+ @update-sorting="onUpdateSorting"
/>
+ <catalog-list-skeleton-loader v-if="isLoading" class="gl-w-full gl-mt-3" />
+ <empty-state v-else-if="!hasResources" :search-term="searchTerm" />
+ <template v-else>
+ <ci-resources-list
+ :current-page="currentPage"
+ :page-info="pageInfo"
+ :prev-text="__('Prev')"
+ :next-text="__('Next')"
+ :resources="catalogResources"
+ :total-count="totalCount"
+ @onPrevPage="handlePrevPage"
+ @onNextPage="handleNextPage"
+ />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/ci/catalog/constants.js b/app/assets/javascripts/ci/catalog/constants.js
index ab067f991cd..34c0ac797c1 100644
--- a/app/assets/javascripts/ci/catalog/constants.js
+++ b/app/assets/javascripts/ci/catalog/constants.js
@@ -1,35 +1,9 @@
-// We disable this for the entire file until the mock data is cleanup
-/* eslint-disable @gitlab/require-i18n-strings */
+import { helpPagePath } from '~/helpers/help_page_helper';
+
export const CATALOG_FEEDBACK_DISMISSED_KEY = 'catalog_feedback_dismissed';
-export const componentsMockData = {
- __typename: 'CiComponentConnection',
- nodes: [
- {
- id: 'gid://gitlab/Ci::Component/1',
- name: 'Ruby gal',
- description: 'This is a pretty amazing component that does EVERYTHING ruby.',
- path: 'gitlab.com/gitlab-org/ruby-gal@~latest',
- inputs: { nodes: [{ name: 'version', defaultValue: '1.0.0', required: true }] },
- },
- {
- id: 'gid://gitlab/Ci::Component/2',
- name: 'Javascript madness',
- description: 'Adds some spice to your life.',
- path: 'gitlab.com/gitlab-org/javascript-madness@~latest',
- inputs: {
- nodes: [
- { name: 'isFun', defaultValue: 'true', required: true },
- { name: 'RandomNumber', defaultValue: '10', required: false },
- ],
- },
- },
- {
- id: 'gid://gitlab/Ci::Component/3',
- name: 'Go go go',
- description: 'When you write Go, you gotta go go go.',
- path: 'gitlab.com/gitlab-org/go-go-go@~latest',
- inputs: { nodes: [{ name: 'version', defaultValue: '1.0.0', required: true }] },
- },
- ],
-};
+export const SORT_OPTION_CREATED = 'CREATED';
+export const SORT_ASC = 'ASC';
+export const SORT_DESC = 'DESC';
+
+export const COMPONENTS_DOCS_URL = helpPagePath('ci/components/index');
diff --git a/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql b/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
index a86db4c1b03..b3a750e9604 100644
--- a/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
@@ -1,5 +1,6 @@
fragment CatalogResourceFields on CiCatalogResource {
id
+ webPath
icon
name
description
@@ -15,10 +16,4 @@ fragment CatalogResourceFields on CiCatalogResource {
webUrl
}
}
- rootNamespace {
- id
- fullPath
- name
- }
- webPath
}
diff --git a/app/assets/javascripts/ci/catalog/graphql/mutations/client/update_current_page.mutation.graphql b/app/assets/javascripts/ci/catalog/graphql/mutations/client/update_current_page.mutation.graphql
new file mode 100644
index 00000000000..7ffd8f6ea61
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/mutations/client/update_current_page.mutation.graphql
@@ -0,0 +1,7 @@
+mutation updateCurrentPage($pageNumber: Int!) {
+ updateCurrentPage(pageNumber: $pageNumber) @client {
+ page {
+ current
+ }
+ }
+}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/client/get_current_page.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/client/get_current_page.query.graphql
new file mode 100644
index 00000000000..b49895a64aa
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/client/get_current_page.query.graphql
@@ -0,0 +1,5 @@
+query getCurrentPage {
+ page @client {
+ current
+ }
+}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql
index 6aef5dcc4e7..41ac72aa9de 100644
--- a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql
@@ -1,17 +1,18 @@
-query getCiCatalogResourceComponents($id: CiCatalogResourceID!) {
- ciCatalogResource(id: $id) {
+query getCiCatalogResourceComponents($fullPath: ID!) {
+ ciCatalogResource(fullPath: $fullPath) {
id
- components @client {
- nodes {
- id
- name
- description
- path
- inputs {
- nodes {
+ webPath
+ latestVersion {
+ id
+ components {
+ nodes {
+ id
+ name
+ path
+ inputs {
name
- defaultValue
required
+ default
}
}
}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql
index 382d3866795..a77e8f12d03 100644
--- a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql
@@ -1,6 +1,7 @@
-query getCiCatalogResourceDetails($id: CiCatalogResourceID!) {
- ciCatalogResource(id: $id) {
+query getCiCatalogResourceDetails($fullPath: ID!) {
+ ciCatalogResource(fullPath: $fullPath) {
id
+ webPath
openIssuesCount
openMergeRequestsCount
versions(first: 1) {
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql
index 6b3d0cdcfc7..c1fde8dcb43 100644
--- a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql
@@ -1,6 +1,7 @@
-query getCiCatalogResourceReadme($id: CiCatalogResourceID!) {
- ciCatalogResource(id: $id) {
+query getCiCatalogResourceReadme($fullPath: ID!) {
+ ciCatalogResource(fullPath: $fullPath) {
id
+ webPath
readmeHtml
}
}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql
index 4ac4cb0e394..3d5d139a334 100644
--- a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql
@@ -1,7 +1,7 @@
#import "../fragments/catalog_resource.fragment.graphql"
-query getCiCatalogResourceSharedData($id: CiCatalogResourceID!) {
- ciCatalogResource(id: $id) {
+query getCiCatalogResourceSharedData($fullPath: ID!) {
+ ciCatalogResource(fullPath: $fullPath) {
...CatalogResourceFields
}
}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql
index aae29edef5e..1cf213dec63 100644
--- a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql
@@ -1,7 +1,21 @@
#import "~/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql"
-query getCatalogResources($after: String, $before: String, $first: Int = 20, $last: Int) {
- ciCatalogResources(after: $after, before: $before, first: $first, last: $last) {
+query getCatalogResources(
+ $searchTerm: String
+ $sortValue: CiCatalogResourceSort
+ $after: String
+ $before: String
+ $first: Int = 20
+ $last: Int
+) {
+ ciCatalogResources(
+ search: $searchTerm
+ sort: $sortValue
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ ) {
pageInfo {
startCursor
endCursor
diff --git a/app/assets/javascripts/ci/catalog/graphql/settings.js b/app/assets/javascripts/ci/catalog/graphql/settings.js
index a87b26ca4fc..4038188a7ce 100644
--- a/app/assets/javascripts/ci/catalog/graphql/settings.js
+++ b/app/assets/javascripts/ci/catalog/graphql/settings.js
@@ -1,32 +1,42 @@
-import { componentsMockData } from '../constants';
+import getCurrentPage from './queries/client/get_current_page.query.graphql';
export const ciCatalogResourcesItemsCount = 20;
export const CI_CATALOG_RESOURCE_TYPE = 'Ci::Catalog::Resource';
export const cacheConfig = {
- cacheConfig: {
- typePolicies: {
- Query: {
- fields: {
- ciCatalogResource(_, { args, toReference }) {
- return toReference({
- __typename: 'CiCatalogResource',
- id: args.id,
- });
- },
- ciCatalogResources: {
- keyArgs: false,
- },
+ typePolicies: {
+ Query: {
+ fields: {
+ ciCatalogResource(_, { args, toReference }) {
+ return toReference({
+ __typename: 'CiCatalogResource',
+ // Webpath is the fullpath with a leading slash
+ webPath: `/${args.fullPath}`,
+ });
+ },
+ ciCatalogResources: {
+ keyArgs: false,
},
},
},
+ CiCatalogResource: {
+ keyFields: ['webPath'],
+ },
},
};
export const resolvers = {
- CiCatalogResource: {
- components() {
- return componentsMockData;
+ Mutation: {
+ updateCurrentPage: (_, { pageNumber }, { cache }) => {
+ cache.writeQuery({
+ query: getCurrentPage,
+ data: {
+ page: {
+ __typename: 'CatalogPage',
+ current: pageNumber,
+ },
+ },
+ });
},
},
};
diff --git a/app/assets/javascripts/ci/catalog/graphql/typedefs.graphql b/app/assets/javascripts/ci/catalog/graphql/typedefs.graphql
new file mode 100644
index 00000000000..8604fae0655
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/typedefs.graphql
@@ -0,0 +1,11 @@
+type CatalogPage {
+ current: Int
+}
+
+extend type Query {
+ page: CatalogPage
+}
+
+extend type Mutation {
+ updateCurrentPage(pageNumber: Int!): CatalogPage
+}
diff --git a/app/assets/javascripts/ci/catalog/index.js b/app/assets/javascripts/ci/catalog/index.js
index 5815245506c..34866bfb821 100644
--- a/app/assets/javascripts/ci/catalog/index.js
+++ b/app/assets/javascripts/ci/catalog/index.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { cacheConfig, resolvers } from '~/ci/catalog/graphql/settings';
+import typeDefs from '~/ci/catalog/graphql/typedefs.graphql';
import GlobalCatalog from './global_catalog.vue';
import CiResourcesPage from './components/pages/ci_resources_page.vue';
@@ -19,7 +20,7 @@ export const initCatalog = (selector = '#js-ci-cd-catalog') => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(resolvers, cacheConfig),
+ defaultClient: createDefaultClient(resolvers, { cacheConfig, typeDefs }),
});
return new Vue({
diff --git a/app/assets/javascripts/ci/catalog/router/routes.js b/app/assets/javascripts/ci/catalog/router/routes.js
index ccfb0673c83..ce859e266d7 100644
--- a/app/assets/javascripts/ci/catalog/router/routes.js
+++ b/app/assets/javascripts/ci/catalog/router/routes.js
@@ -4,6 +4,6 @@ import { CI_RESOURCES_PAGE_NAME, CI_RESOURCE_DETAILS_PAGE_NAME } from './constan
export const createRoutes = (listComponent) => {
return [
{ name: CI_RESOURCES_PAGE_NAME, path: '', component: listComponent },
- { name: CI_RESOURCE_DETAILS_PAGE_NAME, path: '/:id', component: CiResourceDetailsPage },
+ { name: CI_RESOURCE_DETAILS_PAGE_NAME, path: '/:id+', component: CiResourceDetailsPage },
];
};
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
index ccfe773b01f..2ad6c7c6578 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
@@ -29,13 +29,16 @@ import {
EVENT_ACTION,
EXPANDED_VARIABLES_NOTE,
FLAG_LINK_TITLE,
+ MASKED_VALUE_MIN_LENGTH,
VARIABLE_ACTIONS,
variableOptions,
+ WHITESPACE_REG_EX,
} from '../constants';
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
import { awsTokenList } from './ci_variable_autocomplete_tokens';
const trackingMixin = Tracking.mixin({ label: DRAWER_EVENT_LABEL });
+const KEY_REGEX = /^\w+$/;
export const i18n = {
addVariable: s__('CiVariables|Add variable'),
@@ -50,25 +53,38 @@ export const i18n = {
flags: __('Flags'),
flagsLinkTitle: FLAG_LINK_TITLE,
key: __('Key'),
+ keyFeedback: s__("CiVariables|A variable key can only contain letters, numbers, and '_'."),
+ keyHelpText: s__(
+ 'CiVariables|You can use CI/CD variables with the same name in different places, but the variables might overwrite each other. %{linkStart}What is the order of precedence for variables?%{linkEnd}',
+ ),
maskedField: s__('CiVariables|Mask variable'),
maskedDescription: s__(
'CiVariables|Variable will be masked in job logs. Requires values to meet regular expression requirements.',
),
+ maskedValueMinLengthValidationText: s__(
+ 'CiVariables|The value must have at least %{charsAmount} characters.',
+ ),
modalDeleteMessage: s__('CiVariables|Do you want to delete the variable %{key}?'),
protectedField: s__('CiVariables|Protect variable'),
protectedDescription: s__(
'CiVariables|Export variable to pipelines running on protected branches and tags only.',
),
+ unsupportedCharsValidationText: s__(
+ 'CiVariables|This value cannot be masked because it contains the following characters: %{unsupportedChars}.',
+ ),
+ unsupportedAndWhitespaceCharsValidationText: s__(
+ 'CiVariables|This value cannot be masked because it contains the following characters: %{unsupportedChars} and whitespace characters.',
+ ),
valueFeedback: {
rawHelpText: s__('CiVariables|Variable value will be evaluated as raw string.'),
- maskedReqsNotMet: s__(
- 'CiVariables|This variable value does not meet the masking requirements.',
- ),
},
variableReferenceTitle: s__('CiVariables|Value might contain a variable reference'),
variableReferenceDescription: s__(
'CiVariables|Unselect "Expand variable reference" if you want to use the variable value as a raw string.',
),
+ whitespaceCharsValidationText: s__(
+ 'CiVariables|This value cannot be masked because it contains the following characters: whitespace characters.',
+ ),
type: __('Type'),
value: __('Value'),
};
@@ -146,7 +162,7 @@ export default {
return regex.test(this.variable.value);
},
canSubmit() {
- return this.variable.key.length > 0 && this.isValueValid;
+ return this.variable.key.length > 0 && this.isKeyValid && this.isValueValid;
},
getDrawerHeaderHeight() {
return getContentWrapperHeight();
@@ -157,6 +173,9 @@ export default {
isExpanded() {
return !this.variable.raw;
},
+ isKeyValid() {
+ return KEY_REGEX.test(this.variable.key);
+ },
isMaskedReqsMet() {
return !this.variable.masked || this.isValueMasked;
},
@@ -169,11 +188,76 @@ export default {
isEditing() {
return this.mode === EDIT_VARIABLE_ACTION;
},
+ isMaskedValueContainsWhitespaceChars() {
+ return this.isValueMaskable && WHITESPACE_REG_EX.test(this.variable.value);
+ },
maskedRegexToUse() {
return this.variable.raw ? this.maskableRawRegex : this.maskableRegex;
},
- maskedReqsNotMetText() {
- return !this.isMaskedReqsMet ? this.$options.i18n.valueFeedback.maskedReqsNotMet : '';
+ maskedSupportedCharsRegEx() {
+ const supportedChars = this.maskedRegexToUse.replace('^', '').replace(/{(\d,)}\$/, '');
+ return new RegExp(supportedChars, 'g');
+ },
+ maskedValueMinLengthValidationText() {
+ return sprintf(this.$options.i18n.maskedValueMinLengthValidationText, {
+ charsAmount: MASKED_VALUE_MIN_LENGTH,
+ });
+ },
+ unsupportedCharsList() {
+ if (this.isMaskedReqsMet) {
+ return [];
+ }
+
+ return [
+ ...new Set(
+ this.variable.value
+ .replace(WHITESPACE_REG_EX, '')
+ .replace(this.maskedSupportedCharsRegEx, '')
+ .split(''),
+ ),
+ ];
+ },
+ unsupportedChars() {
+ return this.unsupportedCharsList.join(', ');
+ },
+ unsupportedCharsValidationText() {
+ return sprintf(
+ this.$options.i18n.unsupportedCharsValidationText,
+ {
+ unsupportedChars: this.unsupportedChars,
+ },
+ false,
+ );
+ },
+ unsupportedAndWhitespaceCharsValidationText() {
+ return sprintf(
+ this.$options.i18n.unsupportedAndWhitespaceCharsValidationText,
+ {
+ unsupportedChars: this.unsupportedChars,
+ },
+ false,
+ );
+ },
+ maskedValidationIssuesText() {
+ if (this.isMaskedReqsMet) {
+ return '';
+ }
+
+ let validationIssuesText = '';
+
+ if (this.unsupportedCharsList.length && !this.isMaskedValueContainsWhitespaceChars) {
+ validationIssuesText = this.unsupportedCharsValidationText;
+ } else if (this.unsupportedCharsList.length && this.isMaskedValueContainsWhitespaceChars) {
+ validationIssuesText = this.unsupportedAndWhitespaceCharsValidationText;
+ } else if (!this.unsupportedCharsList.length && this.isMaskedValueContainsWhitespaceChars) {
+ validationIssuesText = this.$options.i18n.whitespaceCharsValidationText;
+ }
+
+ if (this.variable.value.length < MASKED_VALUE_MIN_LENGTH) {
+ validationIssuesText += ` ${this.maskedValueMinLengthValidationText}`;
+ }
+
+ return validationIssuesText.trim();
},
modalActionText() {
return this.isEditing ? this.$options.i18n.editVariable : this.$options.i18n.addVariable;
@@ -218,9 +302,7 @@ export default {
let property;
if (this.isValueMaskable) {
- const supportedChars = this.maskedRegexToUse.replace('^', '').replace(/{(\d,)}\$/, '');
- const regex = new RegExp(supportedChars, 'g');
- property = this.variable.value.replace(regex, '');
+ property = this.variable.value.replace(this.maskedSupportedCharsRegEx, '');
} else if (this.hasVariableReference) {
property = '$';
}
@@ -246,6 +328,9 @@ export default {
flagLink: helpPagePath('ci/variables/index', {
anchor: 'define-a-cicd-variable-in-the-ui',
}),
+ variablesPrecedenceLink: helpPagePath('ci/variables/index', {
+ anchor: 'cicd-variable-precedence',
+ }),
i18n,
variableOptions,
deleteModal: {
@@ -339,6 +424,7 @@ export default {
class="gl-display-flex"
:title="$options.i18n.flagsLinkTitle"
:href="$options.flagLink"
+ data-testid="ci-variable-flags-docs-link"
target="_blank"
>
<gl-icon name="question-o" :size="14" />
@@ -377,22 +463,39 @@ export default {
class="gl-border-none gl-pb-0! gl-mb-n5"
data-testid="ci-variable-key"
/>
+ <p
+ v-if="variable.key.length > 0 && !isKeyValid"
+ class="gl-pt-3! gl-pb-0! gl-mb-0 gl-text-red-500 gl-border-none"
+ >
+ {{ $options.i18n.keyFeedback }}
+ </p>
+ <p class="gl-pt-3! gl-pb-0! gl-mb-0 gl-text-secondary gl-border-none">
+ <gl-sprintf :message="$options.i18n.keyHelpText">
+ <template #link="{ content }"
+ ><gl-link
+ :href="$options.variablesPrecedenceLink"
+ data-testid="ci-variable-precedence-docs-link"
+ >{{ content }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </p>
<gl-form-group
:label="$options.i18n.value"
label-for="ci-variable-value"
class="gl-border-none gl-mb-n2"
data-testid="ci-variable-value-label"
- :invalid-feedback="maskedReqsNotMetText"
+ :invalid-feedback="maskedValidationIssuesText"
:state="isValueValid"
>
<gl-form-textarea
id="ci-variable-value"
v-model="variable.value"
+ :spellcheck="false"
class="gl-border-none gl-font-monospace!"
rows="3"
max-rows="10"
data-testid="ci-variable-value"
- spellcheck="false"
/>
<p
v-if="variable.raw"
diff --git a/app/assets/javascripts/ci/ci_variable_list/constants.js b/app/assets/javascripts/ci/ci_variable_list/constants.js
index d85827b8220..4ec7333f465 100644
--- a/app/assets/javascripts/ci/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci/ci_variable_list/constants.js
@@ -2,6 +2,10 @@ import { __, s__, sprintf } from '~/locale';
export const ENVIRONMENT_QUERY_LIMIT = 30;
+export const MASKED_VALUE_MIN_LENGTH = 8;
+
+export const WHITESPACE_REG_EX = /\s/;
+
export const SORT_DIRECTIONS = {
ASC: 'KEY_ASC',
DESC: 'KEY_DESC',
diff --git a/app/assets/javascripts/ci/common/private/job_name_component.vue b/app/assets/javascripts/ci/common/private/job_name_component.vue
index b4e831d69d4..a0e611acc9d 100644
--- a/app/assets/javascripts/ci/common/private/job_name_component.vue
+++ b/app/assets/javascripts/ci/common/private/job_name_component.vue
@@ -1,5 +1,5 @@
<script>
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
/**
* Component that renders both the CI icon status and the job name.
@@ -30,7 +30,13 @@ export default {
</script>
<template>
<span class="mw-100 gl-display-flex gl-align-items-center gl-flex-grow-1">
- <ci-icon :size="iconSize" :status="status" :show-tooltip="false" class="gl-line-height-0" />
+ <ci-icon
+ :size="iconSize"
+ :status="status"
+ :show-tooltip="false"
+ :use-link="false"
+ class="gl-line-height-0"
+ />
<span class="gl-text-truncate mw-70p gl-pl-3 gl-display-inline-block">
{{ name }}
</span>
diff --git a/app/assets/javascripts/ci/job_details/components/environments_block.vue b/app/assets/javascripts/ci/job_details/components/environments_block.vue
index 4046e1ade82..16d553fd071 100644
--- a/app/assets/javascripts/ci/job_details/components/environments_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/environments_block.vue
@@ -1,7 +1,7 @@
<script>
import { GlSprintf, GlLink } from '@gitlab/ui';
import { isEmpty } from 'lodash';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import { __ } from '~/locale';
export default {
diff --git a/app/assets/javascripts/ci/job_details/components/job_header.vue b/app/assets/javascripts/ci/job_details/components/job_header.vue
index 1aa83a94bc5..031abc7a36c 100644
--- a/app/assets/javascripts/ci/job_details/components/job_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/job_header.vue
@@ -4,7 +4,7 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { glEmojiTag } from '~/emoji';
import { __, sprintf } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
diff --git a/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue b/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue
index 4a30878bec5..837efa154e2 100644
--- a/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue
+++ b/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue
@@ -20,6 +20,9 @@ export default {
'Job|Search for substrings in your job log output. Currently search is only supported for the visible job log output, not for any log output that is truncated due to size.',
),
logLineNumberNotFound: s__('Job|We could not find this element'),
+ enterFullscreen: s__('Job|Show full screen'),
+ exitFullScreen: s__('Job|Exit full screen'),
+ fullScreenNotAvailable: s__('Job|Full screen mode is not available'),
},
components: {
GlLink,
@@ -65,6 +68,16 @@ export default {
type: Array,
required: true,
},
+ fullScreenModeAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ fullScreenEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -80,34 +93,32 @@ export default {
size: numberToHumanSize(this.size),
});
},
- showJumpToFailures() {
- return this.glFeatures.jobLogJumpToFailures;
- },
hasFailures() {
return this.failureCount > 0;
},
shouldDisableJumpToFailures() {
return !this.hasFailures;
},
+ fullScreenTooltipContent() {
+ return this.fullScreenModeAvailable ? '' : this.$options.i18n.fullScreenNotAvailable;
+ },
},
mounted() {
this.checkFailureCount();
},
methods: {
checkFailureCount() {
- if (this.glFeatures.jobLogJumpToFailures) {
- backOff((next, stop) => {
- this.failureCount = document.querySelectorAll('.term-fg-l-red').length;
+ backOff((next, stop) => {
+ this.failureCount = document.querySelectorAll('.term-fg-l-red').length;
- if (this.hasFailures || (this.isComplete && !this.hasFailures)) {
- stop();
- } else {
- next();
- }
- }).catch(() => {
- this.failureCount = null;
- });
- }
+ if (this.hasFailures || (this.isComplete && !this.hasFailures)) {
+ stop();
+ } else {
+ next();
+ }
+ }).catch(() => {
+ this.failureCount = null;
+ });
},
handleScrollToNextFailure() {
const failures = document.querySelectorAll('.term-fg-l-red');
@@ -126,6 +137,12 @@ export default {
this.$emit('scrollJobLogBottom');
this.failureIndex = 0;
},
+ handleFullscreenMode() {
+ this.$emit('enterFullscreen');
+ },
+ handleExitFullscreenMode() {
+ this.$emit('exitFullscreen');
+ },
searchJobLog() {
this.searchResults = [];
@@ -221,7 +238,6 @@ export default {
<!-- scroll buttons -->
<gl-button
- v-if="showJumpToFailures"
v-gl-tooltip
:title="$options.i18n.scrollToNextFailureButtonLabel"
:aria-label="$options.i18n.scrollToNextFailureButtonLabel"
@@ -255,6 +271,29 @@ export default {
/>
</div>
<!-- eo scroll buttons -->
+
+ <div v-gl-tooltip="fullScreenTooltipContent">
+ <gl-button
+ v-if="!fullScreenEnabled"
+ :disabled="!fullScreenModeAvailable"
+ :title="$options.i18n.enterFullscreen"
+ :aria-label="$options.i18n.enterFullscreen"
+ class="btn-scroll gl-ml-3"
+ data-testid="job-controller-enter-fullscreen"
+ icon="maximize"
+ @click="handleFullscreenMode"
+ />
+ </div>
+
+ <gl-button
+ v-if="fullScreenEnabled"
+ :title="$options.i18n.exitFullScreen"
+ :aria-label="$options.i18n.exitFullScreen"
+ class="btn-scroll gl-ml-3"
+ data-testid="job-controller-exit-fullscreen"
+ icon="minimize"
+ @click="handleExitFullscreenMode"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ci/job_details/components/log/collapsible_section.vue b/app/assets/javascripts/ci/job_details/components/log/collapsible_section.vue
deleted file mode 100644
index 39c612bc600..00000000000
--- a/app/assets/javascripts/ci/job_details/components/log/collapsible_section.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-<script>
-import LogLine from './line.vue';
-import LogLineHeader from './line_header.vue';
-
-export default {
- name: 'CollapsibleLogSection',
- components: {
- LogLine,
- LogLineHeader,
- },
- props: {
- section: {
- type: Object,
- required: true,
- },
- jobLogEndpoint: {
- type: String,
- required: true,
- },
- searchResults: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
- computed: {
- badgeDuration() {
- return this.section.line && this.section.line.section_duration;
- },
- highlightedLines() {
- return this.searchResults.map((result) => result.lineNumber);
- },
- headerIsHighlighted() {
- const {
- line: { lineNumber },
- } = this.section;
-
- return this.highlightedLines.includes(lineNumber);
- },
- },
- methods: {
- handleOnClickCollapsibleLine(section) {
- this.$emit('onClickCollapsibleLine', section);
- },
- lineIsHighlighted({ lineNumber }) {
- return this.highlightedLines.includes(lineNumber);
- },
- },
-};
-</script>
-<template>
- <div>
- <log-line-header
- :line="section.line"
- :duration="badgeDuration"
- :path="jobLogEndpoint"
- :is-closed="section.isClosed"
- :is-highlighted="headerIsHighlighted"
- @toggleLine="handleOnClickCollapsibleLine(section)"
- />
- <template v-if="!section.isClosed">
- <log-line
- v-for="line in section.lines"
- :key="line.offset"
- :line="line"
- :path="jobLogEndpoint"
- :is-highlighted="lineIsHighlighted(line)"
- />
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/ci/job_details/components/log/line.vue b/app/assets/javascripts/ci/job_details/components/log/line.vue
index 416f75372f9..6ff2bb766c7 100644
--- a/app/assets/javascripts/ci/job_details/components/log/line.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line.vue
@@ -68,7 +68,7 @@ export default {
{
class: [
'js-log-line',
- 'log-line',
+ 'job-log-line',
{ 'gl-bg-gray-700': isHighlighted || applyHashHighlight },
],
},
diff --git a/app/assets/javascripts/ci/job_details/components/log/line_header.vue b/app/assets/javascripts/ci/job_details/components/log/line_header.vue
index d36701323da..4716f1e5162 100644
--- a/app/assets/javascripts/ci/job_details/components/log/line_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line_header.vue
@@ -24,6 +24,11 @@ export default {
type: String,
required: true,
},
+ hideDuration: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
duration: {
type: String,
required: false,
@@ -63,7 +68,7 @@ export default {
<template>
<div
- class="js-log-line log-line collapsible-line d-flex justify-content-between ws-normal gl-align-items-flex-start gl-relative"
+ class="js-log-line job-log-line-header job-log-line"
:class="{ 'gl-bg-gray-700': isHighlighted || applyHashHighlight }"
role="button"
@click="handleOnClick"
@@ -73,10 +78,10 @@ export default {
<span
v-for="(content, i) in line.content"
:key="i"
- class="line-text w-100 gl-white-space-pre-wrap"
+ class="gl-flex-grow-1 gl-white-space-pre-wrap"
:class="content.style"
>{{ content.text }}</span
>
- <duration-badge v-if="duration" :duration="duration" />
+ <duration-badge v-if="duration && !hideDuration" :duration="duration" />
</div>
</template>
diff --git a/app/assets/javascripts/ci/job_details/components/log/line_number.vue b/app/assets/javascripts/ci/job_details/components/log/line_number.vue
index 30b4c80f3fa..ea39c00c8a3 100644
--- a/app/assets/javascripts/ci/job_details/components/log/line_number.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line_number.vue
@@ -20,7 +20,7 @@ export default {
return h(
'a',
{
- class: 'gl-link d-inline-block text-right line-number flex-shrink-0',
+ class: 'job-log-line-number',
attrs: {
id: lineId,
href: lineHref,
diff --git a/app/assets/javascripts/ci/job_details/components/log/log.vue b/app/assets/javascripts/ci/job_details/components/log/log.vue
index fb6a6a58074..8ca9515996c 100644
--- a/app/assets/javascripts/ci/job_details/components/log/log.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/log.vue
@@ -4,14 +4,15 @@
import { mapState, mapActions } from 'vuex';
import { scrollToElement } from '~/lib/utils/common_utils';
import { getLocationHash } from '~/lib/utils/url_utility';
-import CollapsibleLogSection from './collapsible_section.vue';
import LogLine from './line.vue';
+import LogLineHeader from './line_header.vue';
export default {
components: {
- CollapsibleLogSection,
+ LogLineHeader,
LogLine,
},
+ inject: ['pagePath'],
props: {
searchResults: {
type: Array,
@@ -20,23 +21,11 @@ export default {
},
},
computed: {
- ...mapState([
- 'jobLogEndpoint',
- 'jobLog',
- 'isJobLogComplete',
- 'isScrolledToBottomBeforeReceivingJobLog',
- ]),
+ ...mapState(['jobLog', 'jobLogSections', 'isJobLogComplete']),
highlightedLines() {
return this.searchResults.map((result) => result.lineNumber);
},
},
- updated() {
- this.$nextTick(() => {
- if (!window.location.hash) {
- this.handleScrollDown();
- }
- });
- },
mounted() {
if (window.location.hash) {
const lineNumber = getLocationHash();
@@ -51,25 +40,27 @@ export default {
}
});
}
+
+ this.setupFullScreenListeners();
},
methods: {
- ...mapActions(['toggleCollapsibleLine', 'scrollBottom']),
+ ...mapActions(['toggleCollapsibleLine', 'scrollBottom', 'setupFullScreenListeners']),
handleOnClickCollapsibleLine(section) {
this.toggleCollapsibleLine(section);
},
- /**
- * The job log is sent in HTML, which means we need to use `v-html` to render it
- * Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
- * in this case because it runs before `v-html` has finished running, since there's no
- * Vue binding.
- * In order to scroll the page down after `v-html` has finished, we need to use setTimeout
- */
- handleScrollDown() {
- if (this.isScrolledToBottomBeforeReceivingJobLog) {
- setTimeout(() => {
- this.scrollBottom();
- }, 0);
+ isLineVisible(line) {
+ const { lineNumber, section } = line;
+
+ if (!section) {
+ // lines outside of sections can't be collapsed
+ return true;
}
+
+ return !Object.values(this.jobLogSections).find(
+ ({ isClosed, startLineNumber, endLineNumber }) => {
+ return isClosed && lineNumber > startLineNumber && lineNumber <= endLineNumber;
+ },
+ );
},
isHighlighted({ lineNumber }) {
return this.highlightedLines.includes(lineNumber);
@@ -78,23 +69,28 @@ export default {
};
</script>
<template>
- <code class="job-log d-block" data-testid="job-log-content">
- <template v-for="(section, index) in jobLog">
- <collapsible-log-section
- v-if="section.isHeader"
- :key="`collapsible-${index}`"
- :section="section"
- :job-log-endpoint="jobLogEndpoint"
- :search-results="searchResults"
- @onClickCollapsibleLine="handleOnClickCollapsibleLine"
- />
- <log-line
- v-else
- :key="section.offset"
- :line="section"
- :path="jobLogEndpoint"
- :is-highlighted="isHighlighted(section)"
- />
+ <code class="job-log gl-display-block" data-testid="job-log-content">
+ <template v-for="line in jobLog">
+ <template v-if="isLineVisible(line)">
+ <log-line-header
+ v-if="line.isHeader"
+ :key="line.offset"
+ :line="line"
+ :path="pagePath"
+ :is-closed="jobLogSections[line.section].isClosed"
+ :duration="jobLogSections[line.section].duration"
+ :hide-duration="jobLogSections[line.section].hideDuration"
+ :is-highlighted="isHighlighted(line)"
+ @toggleLine="handleOnClickCollapsibleLine(line.section)"
+ />
+ <log-line
+ v-else
+ :key="line.offset"
+ :line="line"
+ :path="pagePath"
+ :is-highlighted="isHighlighted(line)"
+ />
+ </template>
</template>
<div v-if="!isJobLogComplete" class="js-log-animation loader-animation pt-3 pl-3">
diff --git a/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue b/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue
index 7f419a249cf..836426f0bde 100644
--- a/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue
+++ b/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue
@@ -50,6 +50,11 @@ export default {
id: convertToGraphQLId(TYPENAME_COMMIT_STATUS, this.jobId),
};
},
+ skip() {
+ // variables list always contains one empty variable
+ // skip refetch if form already has non-empty variables
+ return this.variables.length > 1;
+ },
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
update(data) {
const jobVariables = cloneDeep(data?.project?.job?.manualVariables?.nodes);
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue b/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
index 4ec9044a21c..19027265a12 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
@@ -2,7 +2,7 @@
import { GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import delayedJobMixin from '~/ci/mixins/delayed_job_mixin';
import { sprintf } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
index 231f45d7ae6..08eaa7c8ecd 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
@@ -85,7 +85,7 @@ export default {
};
</script>
<template>
- <aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix">
+ <aside class="right-sidebar build-sidebar">
<div class="sidebar-container">
<div class="blocks-container gl-p-4 gl-pt-0">
<sidebar-header
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue
index f04987a87b5..a8b29e7c581 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue
@@ -4,6 +4,7 @@ import { mapState } from 'vuex';
import { GlBadge } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import DetailRow from './sidebar_detail_row.vue';
@@ -15,8 +16,9 @@ export default {
GlBadge,
},
mixins: [timeagoMixin],
+ inject: ['pipelineTestReportUrl'],
computed: {
- ...mapState(['job']),
+ ...mapState(['job', 'testSummary']),
coverage() {
return `${this.job.coverage}%`;
},
@@ -74,6 +76,32 @@ export default {
runnerAdminPath() {
return this.job?.runner?.admin_path || '';
},
+ hasTestSummaryDetails() {
+ return Object.keys(this.testSummary).length > 0;
+ },
+ testSummaryDescription() {
+ let message;
+
+ if (this.testSummary?.total?.failed > 0) {
+ message = sprintf(__('%{failures} of %{total} failed'), {
+ failures: this.testSummary?.total?.failed,
+ total: this.testSummary?.total.count,
+ });
+ } else {
+ message = sprintf(__('%{total}'), {
+ total: this.testSummary?.total.count,
+ });
+ }
+
+ return message;
+ },
+ testReportUrlWithJobName() {
+ const urlParams = {
+ job_name: this.job.name,
+ };
+
+ return mergeUrlParams(urlParams, this.pipelineTestReportUrl);
+ },
},
i18n: {
COVERAGE: __('Coverage'),
@@ -82,6 +110,7 @@ export default {
QUEUED: __('Queued'),
RUNNER: __('Runner'),
TAGS: __('Tags'),
+ TEST_SUMMARY: __('Test summary'),
TIMEOUT: __('Timeout'),
},
TIMEOUT_HELP_URL: helpPagePath('/ci/pipelines/settings.md', {
@@ -115,6 +144,13 @@ export default {
:path="runnerAdminPath"
/>
<detail-row v-if="job.coverage" :value="coverage" :title="$options.i18n.COVERAGE" />
+ <detail-row
+ v-if="hasTestSummaryDetails"
+ :value="testSummaryDescription"
+ :title="$options.i18n.TEST_SUMMARY"
+ :path="testReportUrlWithJobName"
+ data-testid="test-summary"
+ />
<p v-if="hasTags" class="build-detail-row" data-testid="job-tags">
<span class="font-weight-bold">{{ $options.i18n.TAGS }}:</span>
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue b/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
index e229abcbe12..413eba4fb52 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlDisclosureDropdown, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import { Mousetrap } from '~/lib/mousetrap';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
diff --git a/app/assets/javascripts/ci/job_details/index.js b/app/assets/javascripts/ci/job_details/index.js
index 20235015ce6..9aa01c4686e 100644
--- a/app/assets/javascripts/ci/job_details/index.js
+++ b/app/assets/javascripts/ci/job_details/index.js
@@ -20,30 +20,41 @@ export const initJobDetails = () => {
}
const {
+ jobEndpoint,
+ logEndpoint,
+ pagePath,
+ projectPath,
artifactHelpUrl,
deploymentHelpUrl,
runnerSettingsUrl,
subscriptionsMoreMinutesUrl,
- endpoint,
- pagePath,
- buildStatus,
- projectPath,
retryOutdatedJobDocsUrl,
aiRootCauseAnalysisAvailable,
+ testReportSummaryUrl,
+ pipelineTestReportUrl,
} = el.dataset;
+ const fullScreenAPIAvailable = document.fullscreenEnabled;
+
// init store to start fetching log
const store = createStore();
- store.dispatch('init', { endpoint, pagePath });
+ store.dispatch('init', {
+ jobEndpoint,
+ logEndpoint,
+ testReportSummaryUrl,
+ fullScreenAPIAvailable,
+ });
return new Vue({
el,
apolloProvider,
store,
provide: {
+ pagePath,
projectPath,
retryOutdatedJobDocsUrl,
aiRootCauseAnalysisAvailable: parseBoolean(aiRootCauseAnalysisAvailable),
+ pipelineTestReportUrl,
},
render(h) {
return h(JobApp, {
@@ -52,10 +63,6 @@ export const initJobDetails = () => {
deploymentHelpUrl,
runnerSettingsUrl,
subscriptionsMoreMinutesUrl,
- endpoint,
- pagePath,
- buildStatus,
- projectPath,
},
});
},
diff --git a/app/assets/javascripts/ci/job_details/job_app.vue b/app/assets/javascripts/ci/job_details/job_app.vue
index e0708289b43..c2394aa4fac 100644
--- a/app/assets/javascripts/ci/job_details/job_app.vue
+++ b/app/assets/javascripts/ci/job_details/job_app.vue
@@ -56,15 +56,6 @@ export default {
required: false,
default: null,
},
- terminalPath: {
- type: String,
- required: false,
- default: null,
- },
- projectPath: {
- type: String,
- required: true,
- },
subscriptionsMoreMinutesUrl: {
type: String,
required: false,
@@ -88,9 +79,9 @@ export default {
'isJobLogSizeVisible',
'isScrollBottomDisabled',
'isScrollTopDisabled',
- 'isScrolledToBottomBeforeReceivingJobLog',
'hasError',
'selectedStage',
+ 'fullScreenEnabled',
]),
...mapGetters([
'headerTime',
@@ -104,6 +95,7 @@ export default {
'isScrollingDown',
'emptyStateAction',
'hasOfflineRunnersForProject',
+ 'fullScreenAPIAndContainerAvailable',
]),
shouldRenderContent() {
@@ -182,6 +174,8 @@ export default {
'stopPolling',
'toggleScrollButtons',
'toggleScrollAnimation',
+ 'enterFullscreen',
+ 'exitFullscreen',
]),
onHideManualVariablesForm() {
this.showUpdateVariablesState = false;
@@ -262,7 +256,6 @@ export default {
v-if="shouldRenderSharedRunnerLimitWarning"
:quota-used="job.runners.quota.used"
:quota-limit="job.runners.quota.limit"
- :project-path="projectPath"
:subscriptions-more-minutes-url="subscriptionsMoreMinutesUrl"
/>
@@ -303,9 +296,13 @@ export default {
:is-scrolling-down="isScrollingDown"
:is-complete="isJobLogComplete"
:job-log="jobLog"
+ :full-screen-mode-available="fullScreenAPIAndContainerAvailable"
+ :full-screen-enabled="fullScreenEnabled"
@scrollJobLogTop="scrollTop"
@scrollJobLogBottom="scrollBottom"
@searchResults="setSearchResults"
+ @enterFullscreen="enterFullscreen"
+ @exitFullscreen="exitFullscreen"
/>
<log :search-results="searchResults" />
</div>
diff --git a/app/assets/javascripts/ci/job_details/store/actions.js b/app/assets/javascripts/ci/job_details/store/actions.js
index 6f538e3b3d4..e1225ecd2c9 100644
--- a/app/assets/javascripts/ci/job_details/store/actions.js
+++ b/app/assets/javascripts/ci/job_details/store/actions.js
@@ -15,20 +15,85 @@ import { __ } from '~/locale';
import { reportToSentry } from '~/ci/utils';
import * as types from './mutation_types';
-export const init = ({ dispatch }, { endpoint, pagePath }) => {
- dispatch('setJobLogOptions', {
- endpoint,
- pagePath,
+export const init = (
+ { commit, dispatch },
+ { jobEndpoint, logEndpoint, testReportSummaryUrl, fullScreenAPIAvailable = false },
+) => {
+ commit(types.SET_JOB_LOG_OPTIONS, {
+ jobEndpoint,
+ logEndpoint,
+ testReportSummaryUrl,
+ fullScreenAPIAvailable,
});
return dispatch('fetchJob');
};
-export const setJobLogOptions = ({ commit }, options) => commit(types.SET_JOB_LOG_OPTIONS, options);
-
export const hideSidebar = ({ commit }) => commit(types.HIDE_SIDEBAR);
export const showSidebar = ({ commit }) => commit(types.SHOW_SIDEBAR);
+export const enterFullscreen = ({ dispatch }) => {
+ const el = document.querySelector('.build-log-container');
+
+ if (!document.fullscreenElement && el) {
+ el.requestFullscreen()
+ .then(() => {
+ dispatch('enterFullscreenSuccess');
+ })
+ .catch((err) => {
+ reportToSentry('job_enter_fullscreen_mode', err);
+ });
+ }
+};
+
+export const enterFullscreenSuccess = ({ commit }) => {
+ commit(types.ENTER_FULLSCREEN_SUCCESS);
+};
+
+export const exitFullscreen = ({ dispatch }) => {
+ if (document.fullscreenElement) {
+ document
+ .exitFullscreen()
+ .then(() => {
+ dispatch('exitFullscreenSuccess');
+ })
+ .catch((err) => {
+ reportToSentry('job_exit_fullscreen_mode', err);
+ });
+ }
+};
+
+export const exitFullscreenSuccess = ({ commit }) => {
+ commit(types.EXIT_FULLSCREEN_SUCCESS);
+};
+
+export const fullScreenContainerSetUpResult = ({ commit }, value) => {
+ commit(types.FULL_SCREEN_CONTAINER_SET_UP, value);
+};
+
+export const fullScreenModeAvailableSuccess = ({ commit }) => {
+ commit(types.FULL_SCREEN_MODE_AVAILABLE_SUCCESS);
+};
+
+export const setupFullScreenListeners = ({ dispatch, state, getters }) => {
+ if (!state.fullScreenContainerSetUp && getters.hasJobLog) {
+ const el = document.querySelector('.build-log-container');
+
+ if (el) {
+ dispatch('fullScreenModeAvailableSuccess');
+
+ el.addEventListener('fullscreenchange', () => {
+ if (!document.fullscreenElement) {
+ // Leaving fullscreen mode
+ dispatch('exitFullscreenSuccess');
+ }
+ });
+
+ dispatch('fullScreenContainerSetUpResult', true);
+ }
+ }
+};
+
export const toggleSidebar = ({ dispatch, state }) => {
if (state.isSidebarOpen) {
dispatch('hideSidebar');
@@ -149,39 +214,46 @@ export const enableScrollTop = ({ commit }) => commit(types.ENABLE_SCROLL_TOP);
export const toggleScrollAnimation = ({ commit }, toggle) =>
commit(types.TOGGLE_SCROLL_ANIMATION, toggle);
-/**
- * Responsible to handle automatic scroll
- */
-export const toggleScrollisInBottom = ({ commit }, toggle) => {
- commit(types.TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_JOB_LOG, toggle);
-};
-
export const requestJobLog = ({ commit }) => commit(types.REQUEST_JOB_LOG);
-export const fetchJobLog = ({ dispatch, state }) =>
- // update trace endpoint once BE compeletes trace re-naming in #340626
- axios
- .get(`${state.jobLogEndpoint}/trace.json`, {
- params: { state: state.jobLogState },
- })
- .then(({ data }) => {
- dispatch('toggleScrollisInBottom', isScrolledToBottom());
- dispatch('receiveJobLogSuccess', data);
-
- if (data.complete) {
- dispatch('stopPollingJobLog');
- } else if (!state.jobLogTimeout) {
- dispatch('startPollingJobLog');
- }
- })
- .catch((e) => {
- if (e.response?.status === HTTP_STATUS_FORBIDDEN) {
- dispatch('receiveJobLogUnauthorizedError');
- } else {
- reportToSentry('job_actions', e);
- dispatch('receiveJobLogError');
- }
- });
+export const fetchJobLog = ({ commit, dispatch, state }) => {
+ let isScrolledToBottomBeforeReceivingJobLog;
+
+ return (
+ axios
+ .get(state.logEndpoint, {
+ params: { state: state.jobLogState },
+ })
+ .then(({ data }) => {
+ isScrolledToBottomBeforeReceivingJobLog = isScrolledToBottom();
+
+ commit(types.RECEIVE_JOB_LOG_SUCCESS, data);
+
+ if (data.complete) {
+ dispatch('stopPollingJobLog');
+ dispatch('requestTestSummary');
+ } else if (!state.jobLogTimeout) {
+ dispatch('startPollingJobLog');
+ }
+ })
+ // place `scrollBottom` in a separate `then()` block
+ // to wait on related components to update
+ // after the RECEIVE_JOB_LOG_SUCCESS commit
+ .then(() => {
+ if (isScrolledToBottomBeforeReceivingJobLog) {
+ dispatch('scrollBottom');
+ }
+ })
+ .catch((e) => {
+ if (e.response?.status === HTTP_STATUS_FORBIDDEN) {
+ dispatch('receiveJobLogUnauthorizedError');
+ } else {
+ reportToSentry('job_actions', e);
+ dispatch('receiveJobLogError');
+ }
+ })
+ );
+};
export const startPollingJobLog = ({ dispatch, commit }) => {
const jobLogTimeout = setTimeout(() => {
@@ -198,8 +270,6 @@ export const stopPollingJobLog = ({ state, commit }) => {
commit(types.STOP_POLLING_JOB_LOG);
};
-export const receiveJobLogSuccess = ({ commit }, log) => commit(types.RECEIVE_JOB_LOG_SUCCESS, log);
-
export const receiveJobLogError = ({ dispatch }) => {
dispatch('stopPollingJobLog');
createAlert({
@@ -273,3 +343,23 @@ export const triggerManualJob = ({ state }, variables) => {
}),
);
};
+
+export const requestTestSummary = ({ state, commit, dispatch }) => {
+ if (!state.testSummaryComplete && state.testReportSummaryUrl?.length) {
+ axios
+ .get(state.testReportSummaryUrl)
+ .then(({ data }) => {
+ dispatch('receiveTestSummarySuccess', data);
+ })
+ .catch((e) => {
+ reportToSentry('job_test_summary_report', e);
+ })
+ .finally(() => {
+ commit(types.RECEIVE_TEST_SUMMARY_COMPLETE);
+ });
+ }
+};
+
+export const receiveTestSummarySuccess = ({ commit }, data) => {
+ commit(types.RECEIVE_TEST_SUMMARY_SUCCESS, data);
+};
diff --git a/app/assets/javascripts/ci/job_details/store/getters.js b/app/assets/javascripts/ci/job_details/store/getters.js
index a0f9db7409d..db967da87fb 100644
--- a/app/assets/javascripts/ci/job_details/store/getters.js
+++ b/app/assets/javascripts/ci/job_details/store/getters.js
@@ -48,3 +48,6 @@ export const isScrollingDown = (state) => isScrolledToBottom() && !state.isJobLo
export const hasOfflineRunnersForProject = (state) =>
state?.job?.runners?.available && !state?.job?.runners?.online;
+
+export const fullScreenAPIAndContainerAvailable = (state) =>
+ state.fullScreenAPIAvailable && state.fullScreenModeAvailable;
diff --git a/app/assets/javascripts/ci/job_details/store/mutation_types.js b/app/assets/javascripts/ci/job_details/store/mutation_types.js
index e125538317d..382bee9059f 100644
--- a/app/assets/javascripts/ci/job_details/store/mutation_types.js
+++ b/app/assets/javascripts/ci/job_details/store/mutation_types.js
@@ -11,8 +11,6 @@ export const ENABLE_SCROLL_BOTTOM = 'ENABLE_SCROLL_BOTTOM';
export const ENABLE_SCROLL_TOP = 'ENABLE_SCROLL_TOP';
export const TOGGLE_SCROLL_ANIMATION = 'TOGGLE_SCROLL_ANIMATION';
-export const TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_JOB_LOG = 'TOGGLE_IS_SCROLL_IN_BOTTOM';
-
export const REQUEST_JOB = 'REQUEST_JOB';
export const RECEIVE_JOB_SUCCESS = 'RECEIVE_JOB_SUCCESS';
export const RECEIVE_JOB_ERROR = 'RECEIVE_JOB_ERROR';
@@ -28,3 +26,11 @@ export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE';
export const REQUEST_JOBS_FOR_STAGE = 'REQUEST_JOBS_FOR_STAGE';
export const RECEIVE_JOBS_FOR_STAGE_SUCCESS = 'RECEIVE_JOBS_FOR_STAGE_SUCCESS';
export const RECEIVE_JOBS_FOR_STAGE_ERROR = 'RECEIVE_JOBS_FOR_STAGE_ERROR';
+
+export const RECEIVE_TEST_SUMMARY_SUCCESS = 'RECEIVE_TEST_SUMMARY_SUCCESS';
+export const RECEIVE_TEST_SUMMARY_COMPLETE = 'RECEIVE_TEST_SUMMARY_COMPLETE';
+
+export const ENTER_FULLSCREEN_SUCCESS = 'ENTER_FULLSCREEN_SUCCESS';
+export const EXIT_FULLSCREEN_SUCCESS = 'EXIT_FULLSCREEN_SUCCESS';
+export const FULL_SCREEN_CONTAINER_SET_UP = 'FULL_SCREEN_CONTAINER_SET_UP';
+export const FULL_SCREEN_MODE_AVAILABLE_SUCCESS = 'FULL_SCREEN_MODE_AVAILABLE_SUCCESS';
diff --git a/app/assets/javascripts/ci/job_details/store/mutations.js b/app/assets/javascripts/ci/job_details/store/mutations.js
index fe6506bf8a5..866ce48ce9f 100644
--- a/app/assets/javascripts/ci/job_details/store/mutations.js
+++ b/app/assets/javascripts/ci/job_details/store/mutations.js
@@ -1,11 +1,12 @@
-import Vue from 'vue';
import * as types from './mutation_types';
-import { logLinesParser, updateIncrementalJobLog } from './utils';
+import { logLinesParser } from './utils';
export default {
[types.SET_JOB_LOG_OPTIONS](state, options = {}) {
- state.jobLogEndpoint = options.pagePath;
- state.jobEndpoint = options.endpoint;
+ state.jobEndpoint = options.jobEndpoint;
+ state.logEndpoint = options.logEndpoint;
+ state.testReportSummaryUrl = options.testReportSummaryUrl;
+ state.fullScreenAPIAvailable = options.fullScreenAPIAvailable;
},
[types.HIDE_SIDEBAR](state) {
@@ -21,15 +22,27 @@ export default {
}
if (log.append) {
- state.jobLog = log.lines ? updateIncrementalJobLog(log.lines, state.jobLog) : state.jobLog;
-
+ if (log.lines) {
+ const { sections, lines } = logLinesParser(log.lines, {
+ currentLines: state.jobLog,
+ currentSections: state.jobLogSections,
+ });
+
+ state.jobLog = lines;
+ state.jobLogSections = sections;
+ }
state.jobLogSize += log.size;
} else {
// When the job still does not have a log
// the job log response will not have a defined
// html or size. We keep the old value otherwise these
// will be set to `null`
- state.jobLog = log.lines ? logLinesParser(log.lines, [], window.location.hash) : state.jobLog;
+ if (log.lines) {
+ const { sections, lines } = logLinesParser(log.lines, {}, window.location.hash);
+
+ state.jobLog = lines;
+ state.jobLogSections = sections;
+ }
state.jobLogSize = log.size || state.jobLogSize;
}
@@ -63,7 +76,9 @@ export default {
* @param {Object} section
*/
[types.TOGGLE_COLLAPSIBLE_LINE](state, section) {
- Vue.set(section, 'isClosed', !section.isClosed);
+ if (state.jobLogSections[section]) {
+ state.jobLogSections[section].isClosed = !state.jobLogSections[section].isClosed;
+ }
},
[types.REQUEST_JOB](state) {
@@ -110,11 +125,6 @@ export default {
[types.TOGGLE_SCROLL_ANIMATION](state, toggle) {
state.isScrollingDown = toggle;
},
-
- [types.TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_JOB_LOG](state, toggle) {
- state.isScrolledToBottomBeforeReceivingJobLog = toggle;
- },
-
[types.REQUEST_JOBS_FOR_STAGE](state, stage = {}) {
state.isLoadingJobs = true;
state.selectedStage = stage.name;
@@ -127,4 +137,22 @@ export default {
state.isLoadingJobs = false;
state.jobs = [];
},
+ [types.RECEIVE_TEST_SUMMARY_SUCCESS](state, testSummary) {
+ state.testSummary = testSummary;
+ },
+ [types.RECEIVE_TEST_SUMMARY_COMPLETE](state) {
+ state.testSummaryComplete = true;
+ },
+ [types.ENTER_FULLSCREEN_SUCCESS](state) {
+ state.fullScreenEnabled = true;
+ },
+ [types.EXIT_FULLSCREEN_SUCCESS](state) {
+ state.fullScreenEnabled = false;
+ },
+ [types.FULL_SCREEN_CONTAINER_SET_UP](state, value) {
+ state.fullScreenContainerSetUp = value;
+ },
+ [types.FULL_SCREEN_MODE_AVAILABLE_SUCCESS](state) {
+ state.fullScreenModeAvailable = true;
+ },
};
diff --git a/app/assets/javascripts/ci/job_details/store/state.js b/app/assets/javascripts/ci/job_details/store/state.js
index dfff65c364d..a3c1e7692c3 100644
--- a/app/assets/javascripts/ci/job_details/store/state.js
+++ b/app/assets/javascripts/ci/job_details/store/state.js
@@ -1,9 +1,12 @@
export default () => ({
jobEndpoint: null,
- jobLogEndpoint: null,
+ logEndpoint: null,
+ testReportSummaryUrl: null,
// sidebar
isSidebarOpen: true,
+ testSummary: {},
+ testSummaryComplete: false,
isLoading: false,
hasError: false,
@@ -13,10 +16,14 @@ export default () => ({
isScrollBottomDisabled: true,
isScrollTopDisabled: true,
- // Used to check if we should keep the automatic scroll
- isScrolledToBottomBeforeReceivingJobLog: true,
+ // fullscreen mode
+ fullScreenAPIAvailable: false,
+ fullScreenModeAvailable: false,
+ fullScreenEnabled: false,
+ fullScreenContainerSetUp: false,
jobLog: [],
+ jobLogSections: {},
isJobLogComplete: false,
jobLogSize: 0,
isJobLogSizeVisible: false,
diff --git a/app/assets/javascripts/ci/job_details/store/utils.js b/app/assets/javascripts/ci/job_details/store/utils.js
index c8b33638821..1536c1140d0 100644
--- a/app/assets/javascripts/ci/job_details/store/utils.js
+++ b/app/assets/javascripts/ci/job_details/store/utils.js
@@ -1,193 +1,105 @@
import { parseBoolean } from '~/lib/utils/common_utils';
/**
- * Adds the line number property
- * @param Object line
- * @param Number lineNumber
- */
-export const parseLine = (line = {}, lineNumber) => ({
- ...line,
- lineNumber,
-});
-
-/**
- * When a line has `section_header` set to true, we create a new
- * structure to allow to nest the lines that belong to the
- * collapsible section
+ * Filters out lines that have an offset lower than the offset provided.
*
- * @param Object line
- * @param Number lineNumber
- */
-export const parseHeaderLine = (line = {}, lineNumber, hash) => {
- let isClosed = parseBoolean(line.section_options?.collapsed);
-
- // if a hash is present in the URL then we ensure
- // all sections are visible so we can scroll to the hash
- // in the DOM
- if (hash) {
- isClosed = false;
- }
-
- return {
- isClosed,
- isHeader: true,
- line: parseLine(line, lineNumber),
- lines: [],
- };
-};
-
-/**
- * Finds the matching header section
- * for the section_duration object and adds it to it
+ * If no offset is provided, all the lines are returned back.
*
- * {
- * isHeader: true,
- * line: {
- * content: [],
- * lineNumber: 0,
- * section_duration: "",
- * },
- * lines: []
- * }
- *
- * @param Array data
- * @param Object durationLine
+ * @param {Array} newLines
+ * @param {Number} offset
+ * @returns Lines to be added to the log that have not been added.
*/
-export function addDurationToHeader(data, durationLine) {
- data.forEach((el) => {
- if (el.line && el.line.section === durationLine.section) {
- el.line.section_duration = durationLine.section_duration;
- }
- });
-}
-
-/**
- * Check is the current section belongs to a collapsible section
- *
- * @param Array acc
- * @param Object last
- * @param Object section
- *
- * @returns Boolean
- */
-export const isCollapsibleSection = (acc = [], last = {}, section = {}) =>
- acc.length > 0 &&
- last.isHeader === true &&
- !section.section_duration &&
- section.section === last.line.section;
-
-/**
- * Returns the next line number in the parsed log
- *
- * @param Array acc
- * @returns Number
- */
-export const getNextLineNumber = (acc) => {
- if (!acc?.length) {
- return 1;
- }
-
- const lastElement = acc[acc.length - 1];
- const nestedLines = lastElement.lines;
-
- if (lastElement.isHeader && !nestedLines.length && lastElement.line) {
- return lastElement.line.lineNumber + 1;
+const linesAfterOffset = (newLines = [], offset = -1) => {
+ if (offset === -1) {
+ return newLines;
}
-
- if (lastElement.isHeader && nestedLines.length) {
- return nestedLines[nestedLines.length - 1].lineNumber + 1;
- }
-
- return lastElement.lineNumber + 1;
+ return newLines.filter((newLine) => newLine.offset > offset);
};
/**
- * Parses the job log content into a structure usable by the template
+ * Parses a series of trace lines from a job and returns lines and
+ * sections of the log. Each line is annotated with a lineNumber.
*
- * For collaspible lines (section_header = true):
- * - creates a new array to hold the lines that are collapsible,
- * - adds a isClosed property to handle toggle
- * - adds a isHeader property to handle template logic
- * - adds the section_duration
- * For each line:
- * - adds the index as lineNumber
+ * Sections have a range: starting line and ending line, plus a
+ * "duration" string.
*
- * @param Array lines
- * @param Array accumulator
- * @returns Array parsed log lines
+ * @param {Array} newLines - Lines to add to the log
+ * @param {Object} currentState - Current log: lines and sections
+ * @returns Consolidated lines and sections to be displayed
*/
-export const logLinesParser = (lines = [], prevLogLines = [], hash = '') =>
- lines.reduce(
- (acc, line) => {
- const lineNumber = getNextLineNumber(acc);
+export const logLinesParser = (
+ newLines = [],
+ { currentLines = [], currentSections = {} } = {},
+ hash = '',
+) => {
+ const lastCurrentLine = currentLines[currentLines.length - 1];
+ const newLinesToAppend = linesAfterOffset(newLines, lastCurrentLine?.offset);
+
+ if (!newLinesToAppend.length) {
+ return { lines: currentLines, sections: currentSections };
+ }
- const last = acc[acc.length - 1];
+ let lineNumber = lastCurrentLine?.lineNumber || 0;
+ const lines = [...currentLines];
+ const sections = { ...currentSections };
+
+ newLinesToAppend.forEach((line) => {
+ const {
+ offset,
+ content,
+ section,
+ section_header: isHeader,
+ section_footer: isFooter,
+ section_duration: duration,
+ section_options: options,
+ } = line;
+
+ if (content.length) {
+ lineNumber += 1;
+ lines.push({
+ offset,
+ lineNumber,
+ content,
+ ...(section ? { section } : {}),
+ ...(isHeader ? { isHeader: true } : {}),
+ });
+ }
- // If the object is an header, we parse it into another structure
- if (line.section_header) {
- acc.push(parseHeaderLine(line, lineNumber, hash));
- } else if (isCollapsibleSection(acc, last, line)) {
- // if the object belongs to a nested section, we append it to the new `lines` array of the
- // previously formatted header
- last.lines.push(parseLine(line, lineNumber));
- } else if (line.section_duration) {
- // if the line has section_duration, we look for the correct header to add it
- addDurationToHeader(acc, line);
- } else {
- // otherwise it's a regular line
- acc.push(parseLine(line, lineNumber));
+ // root level lines have no section, skip creating one
+ if (section) {
+ sections[section] = sections[section] || {
+ startLineNumber: 0,
+ endLineNumber: Infinity, // by default, sections are unbounded / have no end
+ duration: null,
+ isClosed: false,
+ };
+
+ if (isHeader) {
+ sections[section].startLineNumber = lineNumber;
}
-
- return acc;
- },
- [...prevLogLines],
- );
-
-/**
- * Finds the repeated offset, removes the old one
- *
- * Returns a new array with the updated log without
- * the repeated offset
- *
- * @param Array newLog
- * @param Array oldParsed
- * @returns Array
- *
- */
-export const findOffsetAndRemove = (newLog = [], oldParsed = []) => {
- const cloneOldLog = [...oldParsed];
- const lastIndex = cloneOldLog.length - 1;
- const last = cloneOldLog[lastIndex];
-
- const firstNew = newLog[0];
-
- if (last && firstNew) {
- if (last.offset === firstNew.offset || (last.line && last.line.offset === firstNew.offset)) {
- cloneOldLog.splice(lastIndex);
- } else if (last.lines && last.lines.length) {
- const lastNestedIndex = last.lines.length - 1;
- const lastNested = last.lines[lastNestedIndex];
- if (lastNested.offset === firstNew.offset) {
- last.lines.splice(lastNestedIndex);
+ if (options) {
+ let isClosed = parseBoolean(options?.collapsed);
+ // if a hash is present in the URL then we ensure
+ // all sections are visible so we can scroll to the hash
+ // in the DOM
+ if (hash) {
+ isClosed = false;
+ }
+ sections[section].isClosed = isClosed;
+
+ const hideDuration = parseBoolean(options?.hide_duration);
+ if (hideDuration) {
+ sections[section].hideDuration = hideDuration;
+ }
+ }
+ if (duration) {
+ sections[section].duration = duration;
+ }
+ if (isFooter) {
+ sections[section].endLineNumber = lineNumber;
}
}
- }
-
- return cloneOldLog;
-};
-
-/**
- * When the job log is not complete, backend may send the last received line
- * in the new response.
- *
- * We need to check if that is the case by looking for the offset property
- * before parsing the incremental part
- *
- * @param array oldLog
- * @param array newLog
- */
-export const updateIncrementalJobLog = (newLog = [], oldParsed = []) => {
- const parsedLog = findOffsetAndRemove(newLog, oldParsed);
+ });
- return logLinesParser(newLog, parsedLog);
+ return { lines, sections };
};
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue
index 3ad2582e36b..458281eb385 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue
@@ -84,6 +84,9 @@ export default {
artifactDownloadPath() {
return this.hasArtifacts.downloadPath;
},
+ canCancelJob() {
+ return this.job.userPermissions?.cancelBuild;
+ },
canReadJob() {
return this.job.userPermissions?.readBuild;
},
@@ -185,7 +188,7 @@ export default {
<gl-button-group>
<template v-if="canReadJob && canUpdateJob">
<gl-button
- v-if="isActive"
+ v-if="isActive && canCancelJob"
v-gl-tooltip
icon="cancel"
:title="$options.CANCEL"
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
index efa74d86bd6..0ff535add6b 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { formatTime } from '~/lib/utils/datetime_utility';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
diff --git a/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql b/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql
index 69719011079..b1ce3a8597a 100644
--- a/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql
@@ -71,6 +71,7 @@ query getJobs($fullPath: ID!, $after: String, $first: Int = 30, $statuses: [CiJo
readBuild
readJobArtifacts
updateBuild
+ cancelBuild
}
}
}
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
index c6340e6787a..afe66588fb9 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
@@ -5,7 +5,7 @@ import delayedJobMixin from '~/ci/mixins/delayed_job_mixin';
import { helpPagePath } from '~/helpers/help_page_helper';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import ActionComponent from '../../../common/private/job_action_component.vue';
import JobNameComponent from '../../../common/private/job_name_component.vue';
import { BRIDGE_KIND, RETRY_ACTION_TITLE, SINGLE_JOB, SKIP_RETRY_MODAL_KEY } from '../constants';
@@ -343,7 +343,7 @@ export default {
</div>
<gl-badge
v-if="isBridge"
- class="gl-mt-3"
+ class="gl-mt-3 gl-ml-7"
variant="info"
size="sm"
data-testid="job-bridge-badge"
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
index 26521f87426..76ff662cd3f 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
@@ -14,7 +14,7 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
import CancelPipelineMutation from '~/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql';
import RetryPipelineMutation from '~/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import { reportToSentry } from '~/ci/utils';
import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from '../constants';
@@ -287,7 +287,7 @@ export default {
/>
<div v-else :class="$options.styles.actionSizeClasses"></div>
</div>
- <div class="gl-pt-2">
+ <div class="gl-pt-2 gl-ml-7">
<gl-badge size="sm" variant="info" data-testid="downstream-pipeline-label">
{{ label }}
</gl-badge>
diff --git a/app/assets/javascripts/ci/pipeline_details/header/constants.js b/app/assets/javascripts/ci/pipeline_details/header/constants.js
new file mode 100644
index 00000000000..a4aed7b8f46
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_details/header/constants.js
@@ -0,0 +1,9 @@
+export const DELETE_MODAL_ID = 'pipeline-delete-modal';
+
+export const POLL_INTERVAL = 10000;
+
+export const SCHEDULE_SOURCE = 'schedule';
+export const AUTO_DEVOPS_SOURCE = 'AUTO_DEVOPS_SOURCE';
+export const DETACHED_EVENT_TYPE = 'DETACHED';
+export const MERGED_RESULT_EVENT_TYPE = 'MERGED_RESULT';
+export const MERGE_TRAIN_EVENT_TYPE = 'MERGE_TRAIN';
diff --git a/app/assets/javascripts/ci/pipeline_details/header/graphql/fragments/pipeline_header.fragment.graphql b/app/assets/javascripts/ci/pipeline_details/header/graphql/fragments/pipeline_header.fragment.graphql
new file mode 100644
index 00000000000..80fc8b92a47
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_details/header/graphql/fragments/pipeline_header.fragment.graphql
@@ -0,0 +1,4 @@
+fragment PipelineHeaderData on Pipeline {
+ id
+ iid
+}
diff --git a/app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql b/app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql
index eb5643126a2..4ef79aaa03c 100644
--- a/app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql
@@ -1,15 +1,16 @@
+#import "ee_else_ce/ci/pipeline_details/header/graphql/fragments/pipeline_header.fragment.graphql"
+
query getPipelineHeaderData($fullPath: ID!, $iid: ID!) {
project(fullPath: $fullPath) {
id
pipeline(iid: $iid) {
- id
- iid
status
retryable
cancelable
userPermissions {
destroyPipeline
updatePipeline
+ cancelPipeline
}
detailedStatus {
id
@@ -41,6 +42,19 @@ query getPipelineHeaderData($fullPath: ID!, $iid: ID!) {
finishedAt
queuedDuration
duration
+ name
+ totalJobs
+ refText
+ triggeredByPath
+ stuck
+ child
+ complete
+ latest
+ mergeRequestEventType
+ configSource
+ failureReason
+ source
+ ...PipelineHeaderData
}
}
}
diff --git a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
index 651662d6395..1ecc4b2e1c1 100644
--- a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
+++ b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
@@ -17,7 +17,7 @@ import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility'; // eslint-
import { __, s__, sprintf, formatNumber } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants';
@@ -26,9 +26,15 @@ import deletePipelineMutation from '../graphql/mutations/delete_pipeline.mutatio
import retryPipelineMutation from '../graphql/mutations/retry_pipeline.mutation.graphql';
import { getQueryHeaders } from '../graph/utils';
import getPipelineQuery from './graphql/queries/get_pipeline_header_data.query.graphql';
-
-const DELETE_MODAL_ID = 'pipeline-delete-modal';
-const POLL_INTERVAL = 10000;
+import {
+ DELETE_MODAL_ID,
+ POLL_INTERVAL,
+ DETACHED_EVENT_TYPE,
+ AUTO_DEVOPS_SOURCE,
+ SCHEDULE_SOURCE,
+ MERGE_TRAIN_EVENT_TYPE,
+ MERGED_RESULT_EVENT_TYPE,
+} from './constants';
export default {
name: 'PipelineDetailsHeader',
@@ -129,40 +135,14 @@ export default {
},
},
props: {
- name: {
- type: String,
- required: false,
- default: '',
- },
- totalJobs: {
- type: String,
- required: false,
- default: '',
- },
- computeMinutes: {
- type: String,
- required: false,
- default: '',
- },
yamlErrors: {
type: String,
required: false,
default: '',
},
- failureReason: {
- type: String,
- required: false,
- default: '',
- },
- refText: {
- type: String,
- required: false,
- default: '',
- },
- badges: {
- type: Object,
- required: false,
- default: () => {},
+ trigger: {
+ type: Boolean,
+ required: true,
},
},
apollo: {
@@ -270,7 +250,7 @@ export default {
},
totalJobsText() {
return sprintf(__('%{jobs} Jobs'), {
- jobs: this.totalJobs,
+ jobs: this.pipeline?.totalJobs || 0,
});
},
triggeredText() {
@@ -312,10 +292,61 @@ export default {
canCancelPipeline() {
const { cancelable, userPermissions } = this.pipeline;
- return cancelable && userPermissions.updatePipeline;
+ return cancelable && userPermissions.cancelPipeline;
+ },
+ computeMinutes() {
+ return this.pipeline?.computeMinutes;
},
showComputeMinutes() {
- return this.isFinished && this.computeMinutes !== '0.0';
+ return this.isFinished && this.computeMinutes;
+ },
+ pipelineName() {
+ return this.pipeline?.name;
+ },
+ refText() {
+ return this.pipeline?.refText;
+ },
+ triggeredByPath() {
+ return this.pipeline?.triggeredByPath;
+ },
+ mergeRequestEventType() {
+ return this.pipeline.mergeRequestEventType;
+ },
+ isMergeTrainPipeline() {
+ return this.mergeRequestEventType === MERGE_TRAIN_EVENT_TYPE;
+ },
+ isMergedResultsPipeline() {
+ return this.mergeRequestEventType === MERGED_RESULT_EVENT_TYPE;
+ },
+ isDetachedPipeline() {
+ return this.mergeRequestEventType === DETACHED_EVENT_TYPE;
+ },
+ isAutoDevopsPipeline() {
+ return this.pipeline.configSource === AUTO_DEVOPS_SOURCE;
+ },
+ isScheduledPipeline() {
+ return this.pipeline.source === SCHEDULE_SOURCE;
+ },
+ isInvalidPipeline() {
+ return Boolean(this.yamlErrors);
+ },
+ failureReason() {
+ return this.pipeline.failureReason;
+ },
+ badges() {
+ return {
+ schedule: this.isScheduledPipeline,
+ trigger: this.trigger,
+ invalid: this.isInvalidPipeline,
+ child: this.pipeline.child,
+ latest: this.pipeline.latest,
+ mergeTrainPipeline: this.isMergeTrainPipeline,
+ mergedResultsPipeline: this.isMergedResultsPipeline,
+ detached: this.isDetachedPipeline,
+ failed: Boolean(this.failureReason),
+ autoDevops: this.isAutoDevopsPipeline,
+ stuck: this.pipeline.stuck,
+ };
},
},
methods: {
@@ -406,7 +437,9 @@ export default {
<gl-loading-icon v-if="loading" class="gl-text-left" size="lg" />
<div v-else class="gl-display-flex gl-justify-content-space-between gl-flex-wrap">
<div>
- <h3 v-if="name" class="gl-mt-0 gl-mb-3" data-testid="pipeline-name">{{ name }}</h3>
+ <h3 v-if="pipelineName" class="gl-mt-0 gl-mb-3" data-testid="pipeline-name">
+ {{ pipelineName }}
+ </h3>
<h3 v-else class="gl-mt-0 gl-mb-3" data-testid="pipeline-commit-title">
{{ commitTitle }}
</h3>
@@ -483,7 +516,7 @@ export default {
>
<gl-sprintf :message="$options.i18n.childBadgeText">
<template #link="{ content }">
- <gl-link :href="paths.triggeredByPath" target="_blank">
+ <gl-link :href="triggeredByPath" target="_blank">
{{ content }}
</gl-link>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue b/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
index 287f6e045c6..1823908c231 100644
--- a/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
@@ -5,7 +5,7 @@ import { __, s__ } from '~/locale';
import { createAlert } from '~/alert';
import Tracking from '~/tracking';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import { TRACKING_CATEGORIES } from '~/ci/constants';
import RetryFailedJobMutation from '../graphql/mutations/retry_failed_job.mutation.graphql';
import { DEFAULT_FIELDS } from '../../constants';
diff --git a/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
index 4966b657887..0430bc83dd7 100644
--- a/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
@@ -12,29 +12,7 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph
return;
}
- const {
- fullPath,
- pipelineIid,
- pipelinesPath,
- name,
- totalJobs,
- computeMinutes,
- yamlErrors,
- failureReason,
- triggeredByPath,
- schedule,
- trigger,
- child,
- latest,
- mergeTrainPipeline,
- mergedResultsPipeline,
- invalid,
- failed,
- autoDevops,
- detached,
- stuck,
- refText,
- } = el.dataset;
+ const { fullPath, pipelineIid, pipelinesPath, yamlErrors, trigger } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
@@ -46,32 +24,14 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph
fullProject: fullPath,
graphqlResourceEtag,
pipelinesPath,
- triggeredByPath,
},
pipelineIid,
},
render(createElement) {
return createElement(PipelineDetailsHeader, {
props: {
- name,
- totalJobs,
- computeMinutes,
yamlErrors,
- failureReason,
- refText,
- badges: {
- schedule: parseBoolean(schedule),
- trigger: parseBoolean(trigger),
- child: parseBoolean(child),
- latest: parseBoolean(latest),
- mergeTrainPipeline: parseBoolean(mergeTrainPipeline),
- mergedResultsPipeline: parseBoolean(mergedResultsPipeline),
- invalid: parseBoolean(invalid),
- failed: parseBoolean(failed),
- autoDevops: parseBoolean(autoDevops),
- detached: parseBoolean(detached),
- stuck: parseBoolean(stuck),
- },
+ trigger: parseBoolean(trigger),
},
});
},
diff --git a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
index ea2875713a9..b4528ab895d 100644
--- a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
@@ -44,6 +44,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
params,
fullPath,
visibilityPipelineIdType,
+ showJenkinsCiPrompt,
} = el.dataset;
return new Vue({
@@ -57,6 +58,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
pipelineEditorPath,
pipelineSchedulesPath,
suggestedCiTemplates: JSON.parse(suggestedCiTemplates),
+ showJenkinsCiPrompt: parseBoolean(showJenkinsCiPrompt),
},
data() {
return {
diff --git a/app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js
index 6b616601bc5..e3984685094 100644
--- a/app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js
@@ -1,4 +1,5 @@
import { __, sprintf } from '~/locale';
+import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import { TestStatus } from '../../constants';
/**
@@ -25,15 +26,27 @@ export function iconForTestStatus(status) {
return 'status_notfound';
}
}
-
export const formattedTime = (seconds = 0) => {
if (seconds < 1) {
- const milliseconds = seconds * 1000;
- return sprintf(__('%{milliseconds}ms'), { milliseconds: milliseconds.toFixed(2) });
+ return sprintf(__('%{milliseconds}ms'), {
+ milliseconds: (seconds * 1000).toFixed(2),
+ });
+ }
+ if (seconds < 60) {
+ return sprintf(__('%{seconds}s'), {
+ seconds: (seconds % 60).toFixed(2),
+ });
}
- return sprintf(__('%{seconds}s'), { seconds: seconds.toFixed(2) });
-};
+ const hoursAndMinutes = stringifyTime(parseSeconds(seconds));
+ const remainingSeconds =
+ seconds % 60 >= 1
+ ? sprintf(__('%{seconds}s'), {
+ seconds: Math.floor(seconds % 60),
+ })
+ : '';
+ return `${hoursAndMinutes} ${remainingSeconds}`.trim();
+};
export const addIconStatus = (testCase) => ({
...testCase,
icon: iconForTestStatus(testCase.status),
diff --git a/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue b/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue
index a7737d33285..6e9a705c046 100644
--- a/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue
+++ b/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue
@@ -2,6 +2,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
+import { getParameterValues } from '~/lib/utils/url_utility';
import EmptyState from './empty_state.vue';
import TestSuiteTable from './test_suite_table.vue';
import TestSummary from './test_summary.vue';
@@ -19,7 +20,7 @@ export default {
inject: ['blobPath', 'summaryEndpoint', 'suiteEndpoint'],
computed: {
...mapState('testReports', ['isLoading', 'selectedSuiteIndex', 'testReports']),
- ...mapGetters('testReports', ['getSelectedSuite']),
+ ...mapGetters('testReports', ['getSelectedSuite', 'getTestSuites']),
showSuite() {
return this.selectedSuiteIndex !== null;
},
@@ -28,8 +29,16 @@ export default {
return testSuites.length > 0;
},
},
- created() {
- this.fetchSummary();
+ async created() {
+ await this.fetchSummary();
+ const jobName = getParameterValues('job_name')[0] || '';
+ if (jobName.length > 0) {
+ // get the index from the job name
+ const indexToSelect = this.getTestSuites.findIndex((test) => test.name === jobName);
+
+ this.setSelectedSuiteIndex(indexToSelect);
+ this.fetchTestSuite(indexToSelect);
+ }
},
methods: {
...mapActions('testReports', [
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
index 204eaf20664..956f02de09d 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
@@ -4,7 +4,6 @@ import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
- EDITOR_APP_DRAWER_AI_ASSISTANT,
EDITOR_APP_DRAWER_HELP,
EDITOR_APP_DRAWER_JOB_ASSISTANT,
EDITOR_APP_DRAWER_NONE,
@@ -14,17 +13,17 @@ import {
export default {
i18n: {
+ browseCatalog: __('Browse CI/CD Catalog'),
browseTemplates: __('Browse templates'),
help: __('Help'),
jobAssistant: s__('JobAssistant|Job assistant'),
- aiAssistant: s__('PipelinesAiAssistant|Ai assistant'),
},
TEMPLATE_REPOSITORY_URL,
components: {
GlButton,
},
mixins: [glFeatureFlagMixin(), Tracking.mixin()],
- inject: ['aiChatAvailable'],
+ inject: ['ciCatalogPath'],
props: {
showHelpDrawer: {
type: Boolean,
@@ -34,15 +33,6 @@ export default {
type: Boolean,
required: true,
},
- showAiAssistantDrawer: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- isAiConfigChatAvailable() {
- return this.glFeatures.aiCiConfigGenerator && this.aiChatAvailable;
- },
},
methods: {
toggleHelpDrawer() {
@@ -59,11 +49,10 @@ export default {
this.showJobAssistantDrawer ? EDITOR_APP_DRAWER_NONE : EDITOR_APP_DRAWER_JOB_ASSISTANT,
);
},
- toggleAiAssistantDrawer() {
- this.$emit(
- 'switch-drawer',
- this.showAiAssistantDrawer ? EDITOR_APP_DRAWER_NONE : EDITOR_APP_DRAWER_AI_ASSISTANT,
- );
+ trackCatalogBrowsing() {
+ const { label, actions } = pipelineEditorTrackingOptions;
+
+ this.track(actions.browseCatalog, { label });
},
trackHelpDrawerClick() {
const { label, actions } = pipelineEditorTrackingOptions;
@@ -84,6 +73,16 @@ export default {
>
<slot></slot>
<gl-button
+ :href="ciCatalogPath"
+ size="small"
+ icon="external-link"
+ target="_blank"
+ data-testid="catalog-repo-link"
+ @click="trackCatalogBrowsing"
+ >
+ {{ $options.i18n.browseCatalog }}
+ </gl-button>
+ <gl-button
:href="$options.TEMPLATE_REPOSITORY_URL"
size="small"
icon="external-link"
@@ -109,14 +108,5 @@ export default {
>
{{ $options.i18n.jobAssistant }}
</gl-button>
- <gl-button
- v-if="isAiConfigChatAvailable"
- icon="bulb"
- size="small"
- data-testid="ai-assistant-drawer-toggle"
- @click="toggleAiAssistantDrawer"
- >
- {{ $options.i18n.aiAssistant }}
- </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
index 21e21d54758..0064dc51d97 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
@@ -105,9 +105,6 @@ export default {
branchesData() {
return this.availableBranches.map((branch) => ({
text: branch,
- extraAttrs: {
- 'data-qa-selector': 'branch_menu_item_button',
- },
value: branch,
}));
},
@@ -211,7 +208,6 @@ export default {
<gl-collapsible-listbox
v-model="currentBranch"
v-gl-tooltip.hover
- data-qa-selector="branch_selector_button"
searchable
:items="branchesData"
:title="$options.i18n.dropdownHeader"
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
index f00098105d3..f76243e81b9 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
@@ -19,6 +19,11 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ linkedPipelines: null,
+ };
+ },
apollo: {
linkedPipelines: {
query: getLinkedPipelinesQuery,
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
index 7c4a07e3f83..9c1bbff1cc4 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
@@ -7,7 +7,7 @@ import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.quer
import getPipelineEtag from '~/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql';
import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import PipelineMiniGraph from '~/ci/pipeline_mini_graph/pipeline_mini_graph.vue';
import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
index c7c15cdd76e..cd6150031d4 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -1,6 +1,5 @@
<script>
import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
-import CiEditorHeader from 'ee_else_ce/ci/pipeline_editor/components/editor/ci_editor_header.vue';
import { s__, __ } from '~/locale';
import PipelineGraph from '~/ci/pipeline_editor/components/graph/pipeline_graph.vue';
import { getParameterValues, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
@@ -20,6 +19,7 @@ import {
} from '../constants';
import getAppStatus from '../graphql/queries/client/app_status.query.graphql';
import CiConfigMergedPreview from './editor/ci_config_merged_preview.vue';
+import CiEditorHeader from './editor/ci_editor_header.vue';
import CiValidate from './validate/ci_validate.vue';
import TextEditor from './editor/text_editor.vue';
import EditorTab from './ui/editor_tab.vue';
@@ -95,10 +95,6 @@ export default {
type: Boolean,
required: true,
},
- showAiAssistantDrawer: {
- type: Boolean,
- required: true,
- },
isNewCiConfigFile: {
type: Boolean,
required: true,
@@ -198,7 +194,6 @@ export default {
<ci-editor-header
:show-help-drawer="showHelpDrawer"
:show-job-assistant-drawer="showJobAssistantDrawer"
- :show-ai-assistant-drawer="showAiAssistantDrawer"
v-on="$listeners"
/>
<text-editor :commit-sha="commitSha" :value="ciFileContent" v-on="$listeners" />
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue b/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
index 617088f303b..1d152a63407 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
@@ -9,6 +9,7 @@ import {
GlTooltip,
GlTooltipDirective,
GlSprintf,
+ GlEmptyState,
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
@@ -67,6 +68,7 @@ export default {
GlLink,
GlSprintf,
GlTooltip,
+ GlEmptyState,
ValidatePipelinePopover,
},
directives: {
@@ -226,38 +228,44 @@ export default {
</gl-button>
</div>
</div>
- <div v-if="isInitState" :class="$options.BASE_CLASSES">
- <img :src="validateTabIllustrationPath" />
- <h1 class="gl-font-size-h1 gl-mb-6">{{ $options.i18n.title }}</h1>
- <ul>
- <li class="gl-mb-3">{{ $options.i18n.contentNote }}</li>
- <li class="gl-mb-3">
+ <gl-empty-state
+ v-if="isInitState"
+ :svg-path="validateTabIllustrationPath"
+ :title="$options.i18n.title"
+ :primary-button-link="validateYaml"
+ :primary-button-text="$options.i18n.cta"
+ >
+ <template #description>
+ <p>{{ $options.i18n.contentNote }}</p>
+ <p>
<gl-sprintf :message="$options.i18n.simulationNote">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
</gl-sprintf>
- </li>
- </ul>
- <div ref="simulatePipelineButton">
- <gl-button
- ref="simulatePipelineButton"
- variant="confirm"
- class="gl-mt-3"
- :disabled="isInitialCiContentLoading"
- data-testid="simulate-pipeline-button"
- @click="validateYaml"
- >
- {{ $options.i18n.cta }}
- </gl-button>
- </div>
- <gl-tooltip
- v-if="isInitialCiContentLoading"
- :target="() => $refs.simulatePipelineButton"
- :title="$options.i18n.ctaDisabledTooltip"
- data-testid="cta-tooltip"
- />
- </div>
+ </p>
+ </template>
+ <template #actions>
+ <div ref="simulatePipelineButton">
+ <gl-button
+ ref="simulatePipelineButton"
+ variant="confirm"
+ class="gl-mt-3"
+ :disabled="isInitialCiContentLoading"
+ data-testid="simulate-pipeline-button"
+ @click="validateYaml"
+ >
+ {{ $options.i18n.cta }}
+ </gl-button>
+ </div>
+ <gl-tooltip
+ v-if="isInitialCiContentLoading"
+ :target="() => $refs.simulatePipelineButton"
+ :title="$options.i18n.ctaDisabledTooltip"
+ data-testid="cta-tooltip"
+ />
+ </template>
+ </gl-empty-state>
<div v-else-if="isSimulationLoading" :class="$options.BASE_CLASSES">
<gl-loading-icon size="lg" class="gl-m-3" />
<h1 class="gl-font-size-h1 gl-mb-6">{{ $options.i18n.loading }}</h1>
diff --git a/app/assets/javascripts/ci/pipeline_editor/constants.js b/app/assets/javascripts/ci/pipeline_editor/constants.js
index e85138e361f..66725df15f0 100644
--- a/app/assets/javascripts/ci/pipeline_editor/constants.js
+++ b/app/assets/javascripts/ci/pipeline_editor/constants.js
@@ -1,4 +1,5 @@
import { s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
export const EDITOR_APP_DRAWER_HELP = 'HELP';
export const EDITOR_APP_DRAWER_JOB_ASSISTANT = 'JOB_ASSISTANT';
@@ -93,6 +94,9 @@ export const VALIDATE_TAB_FEEDBACK_URL = 'https://gitlab.com/gitlab-org/gitlab/-
export const COMMIT_SHA_POLL_INTERVAL = 1000;
+export const MIGRATION_PLAN_HELP_PATH = helpPagePath('ci/migration/plan_a_migration');
+export const MIGRATE_FROM_JENKINS_TRACKING_LABEL = 'migrate_from_jenkins_prompt';
+
export const I18N = {
title: s__('Pipelines|Get started with GitLab CI/CD'),
learnBasics: {
@@ -107,6 +111,13 @@ export const I18N = {
),
cta: s__('Pipelines|Try test template'),
},
+ migrateFromJenkins: {
+ title: s__('Pipelines|Migrate to GitLab CI/CD from Jenkins'),
+ description: s__(
+ 'Pipelines|Take advantage of simple, scalable pipelines and CI/CD-enabled features. You can view integration results, security scans, tests, code coverage and more directly in merge requests!',
+ ),
+ cta: s__('Pipelines|Start with a migration plan'),
+ },
},
templates: {
title: s__('Pipelines|Ready to set up CI/CD for your project?'),
diff --git a/app/assets/javascripts/ci/pipeline_editor/index.js b/app/assets/javascripts/ci/pipeline_editor/index.js
index bc20e478876..408e91a4d62 100644
--- a/app/assets/javascripts/ci/pipeline_editor/index.js
+++ b/app/assets/javascripts/ci/pipeline_editor/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import { createAppOptions } from 'ee_else_ce/ci/pipeline_editor/options';
+import { createAppOptions } from '~/ci/pipeline_editor/options';
export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
const el = document.querySelector(selector);
diff --git a/app/assets/javascripts/ci/pipeline_editor/options.js b/app/assets/javascripts/ci/pipeline_editor/options.js
index 340cb6ab979..9520295c94d 100644
--- a/app/assets/javascripts/ci/pipeline_editor/options.js
+++ b/app/assets/javascripts/ci/pipeline_editor/options.js
@@ -19,6 +19,7 @@ export const createAppOptions = (el) => {
initialBranchName,
pipelineEtag,
// Add to provide/inject API for static values
+ ciCatalogPath,
ciConfigPath,
ciExamplesHelpPagePath,
ciHelpPagePath,
@@ -40,7 +41,6 @@ export const createAppOptions = (el) => {
usesExternalConfig,
validateTabIllustrationPath,
ymlHelpPagePath,
- aiChatAvailable,
} = el.dataset;
const configurationPaths = Object.fromEntries(
@@ -109,7 +109,7 @@ export const createAppOptions = (el) => {
el,
apolloProvider,
provide: {
- aiChatAvailable: parseBoolean(aiChatAvailable),
+ ciCatalogPath,
ciConfigPath,
ciExamplesHelpPagePath,
ciHelpPagePath,
diff --git a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
index 09ba6292e13..ca2e1fbf37d 100644
--- a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
@@ -2,7 +2,6 @@
import { GlModal } from '@gitlab/ui';
import { __ } from '~/locale';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import JobAssistantDrawer from 'jh_else_ce/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue';
import CommitSection from './components/commit/commit_section.vue';
import PipelineEditorDrawer from './components/drawer/pipeline_editor_drawer.vue';
@@ -19,9 +18,6 @@ import {
EDITOR_APP_DRAWER_NONE,
} from './constants';
-const AiAssistantDrawer = () =>
- import('ee_component/ci/pipeline_editor/components/ai_assistant_drawer.vue');
-
export default {
EDITOR_APP_DRAWER_HELP,
EDITOR_APP_DRAWER_JOB_ASSISTANT,
@@ -45,13 +41,11 @@ export default {
GlModal,
PipelineEditorDrawer,
JobAssistantDrawer,
- AiAssistantDrawer,
PipelineEditorFileNav,
PipelineEditorFileTree,
PipelineEditorHeader,
PipelineEditorTabs,
},
- mixins: [glFeatureFlagMixin()],
props: {
ciConfigData: {
type: Object,
@@ -105,9 +99,6 @@ export default {
showJobAssistantDrawer() {
return this.currentDrawer === EDITOR_APP_DRAWER_JOB_ASSISTANT;
},
- showAiAssistantDrawer() {
- return this.currentDrawer === EDITOR_APP_DRAWER_AI_ASSISTANT;
- },
},
mounted() {
this.showFileTree = JSON.parse(localStorage.getItem(FILE_TREE_DISPLAY_KEY)) || false;
@@ -189,7 +180,6 @@ export default {
:is-new-ci-config-file="isNewCiConfigFile"
:show-help-drawer="showHelpDrawer"
:show-job-assistant-drawer="showJobAssistantDrawer"
- :show-ai-assistant-drawer="showAiAssistantDrawer"
v-on="$listeners"
@switch-drawer="switchDrawer"
@set-current-tab="setCurrentTab"
@@ -222,11 +212,5 @@ export default {
v-on="$listeners"
@switch-drawer="switchDrawer"
/>
- <ai-assistant-drawer
- v-if="glFeatures.aiCiConfigGenerator"
- :is-visible="showAiAssistantDrawer"
- :z-index="drawerIndex[$options.EDITOR_APP_DRAWER_AI_ASSISTANT]"
- @switch-drawer="switchDrawer"
- />
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
index 4fded3aec60..4238f0e3872 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
@@ -38,7 +38,7 @@ export default {
},
tooltipConfig: {
boundary: 'viewport',
- placement: 'bottom',
+ placement: 'top',
customClass: 'gl-pointer-events-none',
},
components: {
@@ -161,7 +161,6 @@ export default {
:tooltip-text="jobActionTooltipText"
:link="status.action.path"
:action-icon="status.action.icon"
- data-qa-selector="action_button"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
index ed78a335453..38a071a0319 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
@@ -13,7 +13,7 @@
*/
import { GlDropdown, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import { createAlert } from '~/alert';
import eventHub from '~/ci/event_hub';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue b/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
index f6a375ab94c..bbe17a3eb22 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
@@ -1,7 +1,7 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import { accessValue } from './accessors/linked_pipelines_accessors';
/**
* Renders the upstream/downstream portions of the pipeline mini graph.
@@ -92,7 +92,7 @@ export default {
'is-upstream': isUpstream,
'is-downstream': isDownstream,
}"
- class="linked-pipeline-mini-list gl-display-inline gl-vertical-align-middle"
+ class="linked-pipeline-mini-list gl-display-inline-flex gl-gap-2 gl-vertical-align-middle"
>
<ci-icon
v-for="pipeline in linkedPipelinesTrimmed"
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
index 5444e66cbdf..44a377144a5 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
@@ -391,7 +391,6 @@ export default {
:placeholder="s__('CiVariables|Input variable key')"
:class="$options.formElementClasses"
data-testid="pipeline-form-ci-variable-key"
- data-qa-selector="ci_variable_key_field"
@change="addEmptyVariable(variable)"
/>
@@ -411,7 +410,6 @@ export default {
class="gl-mb-3 gl-h-7!"
:no-resize="false"
data-testid="pipeline-form-ci-variable-value"
- data-qa-selector="ci_variable_value_field"
@change="resetVariable(index)"
/>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
index d979c0efaf2..245d4257bbb 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
@@ -1,5 +1,5 @@
<script>
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue
index a6297213402..c9631d8f36b 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue
@@ -1,7 +1,12 @@
<script>
import { GlButton, GlCard, GlSprintf } from '@gitlab/ui';
import { mergeUrlParams } from '~/lib/utils/url_utility';
-import { STARTER_TEMPLATE_NAME, I18N } from '~/ci/pipeline_editor/constants';
+import {
+ STARTER_TEMPLATE_NAME,
+ I18N,
+ MIGRATION_PLAN_HELP_PATH,
+ MIGRATE_FROM_JENKINS_TRACKING_LABEL,
+} from '~/ci/pipeline_editor/constants';
import Tracking from '~/tracking';
import CiTemplates from './ci_templates.vue';
@@ -15,7 +20,7 @@ export default {
mixins: [Tracking.mixin()],
STARTER_TEMPLATE_NAME,
I18N,
- inject: ['pipelineEditorPath'],
+ inject: ['pipelineEditorPath', 'showJenkinsCiPrompt'],
data() {
return {
gettingStartedTemplateUrl: mergeUrlParams(
@@ -23,17 +28,23 @@ export default {
this.pipelineEditorPath,
),
tracker: null,
+ migrationPlanUrl: MIGRATION_PLAN_HELP_PATH,
+ migrationPromptTrackingLabel: MIGRATE_FROM_JENKINS_TRACKING_LABEL,
};
},
+ mounted() {
+ if (this.showJenkinsCiPrompt) {
+ this.trackEvent('render', this.migrationPromptTrackingLabel);
+ }
+ },
methods: {
- trackEvent(template) {
- this.track('template_clicked', {
- label: template,
- });
+ trackEvent(action, label) {
+ this.track(action, { label });
},
},
};
</script>
+
<template>
<div>
<h2 class="gl-font-size-h2 gl-text-gray-900">{{ $options.I18N.title }}</h2>
@@ -47,28 +58,62 @@ export default {
</gl-sprintf>
</p>
- <div class="gl-lg-w-25p gl-lg-pr-5 gl-mb-8">
- <gl-card>
- <div class="gl-flex-direction-row">
- <div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="wave" /></div>
- <div class="gl-mb-3">
- <strong class="gl-text-gray-800 gl-mb-2">
- {{ $options.I18N.learnBasics.gettingStarted.title }}
- </strong>
+ <div class="gl-display-flex gl-flex-direction-row gl-flex-wrap">
+ <div
+ v-if="showJenkinsCiPrompt"
+ class="gl-lg-w-25p gl-md-w-half gl-w-full gl-md-pr-5 gl-pb-8"
+ data-testid="migrate-from-jenkins-prompt"
+ >
+ <gl-card class="gl-bg-blue-50">
+ <div class="gl-flex-direction-row">
+ <div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="rocket" /></div>
+ <div class="gl-mb-3">
+ <strong class="gl-text-gray-800 gl-mb-2">{{
+ $options.I18N.learnBasics.migrateFromJenkins.title
+ }}</strong>
+ </div>
+ <p class="gl-font-sm gl-h-13">
+ {{ $options.I18N.learnBasics.migrateFromJenkins.description }}
+ </p>
+ </div>
+
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :href="migrationPlanUrl"
+ target="_blank"
+ @click="trackEvent('template_clicked', migrationPromptTrackingLabel)"
+ >
+ {{ $options.I18N.learnBasics.migrateFromJenkins.cta }}
+ </gl-button>
+ </gl-card>
+ </div>
+
+ <div class="gl-lg-w-25p gl-md-w-half gl-w-full gl-pb-8">
+ <gl-card>
+ <div class="gl-flex-direction-row">
+ <div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="wave" /></div>
+ <div class="gl-mb-3">
+ <strong class="gl-text-gray-800 gl-mb-2">
+ {{ $options.I18N.learnBasics.gettingStarted.title }}
+ </strong>
+ </div>
+ <p class="gl-font-sm gl-h-13">
+ {{ $options.I18N.learnBasics.gettingStarted.description }}
+ </p>
</div>
- <p class="gl-font-sm">{{ $options.I18N.learnBasics.gettingStarted.description }}</p>
- </div>
- <gl-button
- category="primary"
- variant="confirm"
- :href="gettingStartedTemplateUrl"
- data-testid="test-template-link"
- @click="trackEvent($options.STARTER_TEMPLATE_NAME)"
- >
- {{ $options.I18N.learnBasics.gettingStarted.cta }}
- </gl-button>
- </gl-card>
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :href="gettingStartedTemplateUrl"
+ data-testid="test-template-link"
+ @click="trackEvent('template_clicked', $options.STARTER_TEMPLATE_NAME)"
+ >
+ {{ $options.I18N.learnBasics.gettingStarted.cta }}
+ </gl-button>
+ </gl-card>
+ </div>
</div>
<h2 class="gl-font-lg gl-text-gray-900">{{ $options.I18N.templates.title }}</h2>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue
index 82f1d57912a..7a49bf6a809 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue
@@ -3,7 +3,7 @@ import { GlButton, GlIcon, GlLink, GlTooltip } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { __, s__, sprintf } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { BRIDGE_KIND } from '~/ci/pipeline_details/graph/constants';
import RetryMrFailedJobMutation from '~/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql';
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
index 380f8ce172f..2d5fb8c9799 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
@@ -1,6 +1,6 @@
<script>
import { TRACKING_CATEGORIES } from '~/ci/constants';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import Tracking from '~/tracking';
import PipelinesTimeago from './time_ago.vue';
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
index d62a68f0dcc..9ccb7012897 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
@@ -2,7 +2,7 @@
import { GlLink, GlModal, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { __, s__, sprintf } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
/**
* Pipeline Stop Modal.
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue b/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue
index 3021b4a2ef8..a45387ca676 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue
@@ -70,3 +70,11 @@ export default {
:items="items"
/>
</template>
+
+<style scoped>
+/* TODO: Use max-height prop when gitlab-ui got updated.
+See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2374 */
+::v-deep .gl-new-dropdown-inner {
+ max-height: 310px !important;
+}
+</style>
diff --git a/app/assets/javascripts/ci/reports/codequality_report/store/actions.js b/app/assets/javascripts/ci/reports/codequality_report/store/actions.js
deleted file mode 100644
index 04aca11b945..00000000000
--- a/app/assets/javascripts/ci/reports/codequality_report/store/actions.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import pollUntilComplete from '~/lib/utils/poll_until_complete';
-import { STATUS_NOT_FOUND } from '../../constants';
-import * as types from './mutation_types';
-import { parseCodeclimateMetrics } from './utils/codequality_parser';
-
-export const setPaths = ({ commit }, paths) => commit(types.SET_PATHS, paths);
-
-export const fetchReports = ({ state, dispatch, commit }) => {
- commit(types.REQUEST_REPORTS);
-
- return pollUntilComplete(state.reportsPath)
- .then(({ data }) => {
- if (data.status === STATUS_NOT_FOUND) {
- return dispatch('receiveReportsError', data);
- }
- return dispatch('receiveReportsSuccess', {
- newIssues: parseCodeclimateMetrics(data.new_errors, state.headBlobPath),
- resolvedIssues: parseCodeclimateMetrics(data.resolved_errors, state.baseBlobPath),
- });
- })
- .catch((error) => dispatch('receiveReportsError', error));
-};
-
-export const receiveReportsSuccess = ({ commit }, data) => {
- commit(types.RECEIVE_REPORTS_SUCCESS, data);
-};
-
-export const receiveReportsError = ({ commit }, error) => {
- commit(types.RECEIVE_REPORTS_ERROR, error);
-};
diff --git a/app/assets/javascripts/ci/reports/codequality_report/store/getters.js b/app/assets/javascripts/ci/reports/codequality_report/store/getters.js
deleted file mode 100644
index 70d11e96a54..00000000000
--- a/app/assets/javascripts/ci/reports/codequality_report/store/getters.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { spriteIcon } from '~/lib/utils/common_utils';
-import { sprintf, s__, n__ } from '~/locale';
-import { LOADING, ERROR, SUCCESS, STATUS_NOT_FOUND } from '../../constants';
-
-export const hasCodequalityIssues = (state) =>
- Boolean(state.newIssues?.length || state.resolvedIssues?.length);
-
-export const codequalityStatus = (state) => {
- if (state.isLoading) {
- return LOADING;
- }
- if (state.hasError) {
- return ERROR;
- }
-
- return SUCCESS;
-};
-
-export const codequalityText = (state) => {
- const { newIssues, resolvedIssues } = state;
- let text;
- if (!newIssues.length && !resolvedIssues.length) {
- text = s__('ciReport|No changes to code quality');
- } else if (newIssues.length && resolvedIssues.length) {
- text = sprintf(
- s__(`ciReport|Code quality scanning detected %{issueCount} changes in merged results`),
- {
- issueCount: newIssues.length + resolvedIssues.length,
- },
- );
- } else if (resolvedIssues.length) {
- text = n__(
- `ciReport|Code quality improved due to 1 resolved issue`,
- `ciReport|Code quality improved due to %d resolved issues`,
- resolvedIssues.length,
- );
- } else if (newIssues.length) {
- text = n__(
- `ciReport|Code quality degraded due to 1 new issue`,
- `ciReport|Code quality degraded due to %d new issues`,
- newIssues.length,
- );
- }
-
- return text;
-};
-
-export const codequalityPopover = (state) => {
- if (state.status === STATUS_NOT_FOUND) {
- return {
- title: s__('ciReport|Base pipeline codequality artifact not found'),
- content: sprintf(
- s__('ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}'),
- {
- linkStartTag: `<a href="${state.helpPath}" target="_blank" rel="noopener noreferrer">`,
- linkEndTag: `${spriteIcon('external-link', 's16')}</a>`,
- },
- false,
- ),
- };
- }
- return {};
-};
diff --git a/app/assets/javascripts/ci/reports/codequality_report/store/index.js b/app/assets/javascripts/ci/reports/codequality_report/store/index.js
deleted file mode 100644
index c2f706e56e6..00000000000
--- a/app/assets/javascripts/ci/reports/codequality_report/store/index.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import Vue from 'vue';
-// eslint-disable-next-line no-restricted-imports
-import Vuex from 'vuex';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-import state from './state';
-
-Vue.use(Vuex);
-
-export const getStoreConfig = (initialState) => ({
- actions,
- getters,
- mutations,
- state: state(initialState),
-});
-
-export default (initialState) => new Vuex.Store(getStoreConfig(initialState));
diff --git a/app/assets/javascripts/ci/reports/codequality_report/store/mutation_types.js b/app/assets/javascripts/ci/reports/codequality_report/store/mutation_types.js
deleted file mode 100644
index c362c973ae1..00000000000
--- a/app/assets/javascripts/ci/reports/codequality_report/store/mutation_types.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export const SET_PATHS = 'SET_PATHS';
-
-export const REQUEST_REPORTS = 'REQUEST_REPORTS';
-export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS';
-export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR';
diff --git a/app/assets/javascripts/ci/reports/codequality_report/store/mutations.js b/app/assets/javascripts/ci/reports/codequality_report/store/mutations.js
deleted file mode 100644
index 249c2f35c0b..00000000000
--- a/app/assets/javascripts/ci/reports/codequality_report/store/mutations.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import * as types from './mutation_types';
-
-export default {
- [types.SET_PATHS](state, paths) {
- state.baseBlobPath = paths.baseBlobPath;
- state.headBlobPath = paths.headBlobPath;
- state.reportsPath = paths.reportsPath;
- state.helpPath = paths.helpPath;
- },
- [types.REQUEST_REPORTS](state) {
- state.isLoading = true;
- },
- [types.RECEIVE_REPORTS_SUCCESS](state, data) {
- state.hasError = false;
- state.status = '';
- state.statusReason = '';
- state.isLoading = false;
- state.newIssues = data.newIssues;
- state.resolvedIssues = data.resolvedIssues;
- },
- [types.RECEIVE_REPORTS_ERROR](state, error) {
- state.isLoading = false;
- state.hasError = true;
- state.status = error?.status || '';
- state.statusReason = error?.response?.data?.status_reason;
- },
-};
diff --git a/app/assets/javascripts/ci/reports/codequality_report/store/state.js b/app/assets/javascripts/ci/reports/codequality_report/store/state.js
deleted file mode 100644
index f68dbc2a5fa..00000000000
--- a/app/assets/javascripts/ci/reports/codequality_report/store/state.js
+++ /dev/null
@@ -1,16 +0,0 @@
-export default () => ({
- reportsPath: null,
-
- baseBlobPath: null,
- headBlobPath: null,
-
- isLoading: false,
- hasError: false,
- status: '',
- statusReason: '',
-
- newIssues: [],
- resolvedIssues: [],
-
- helpPath: null,
-});
diff --git a/app/assets/javascripts/ci/reports/codequality_report/store/utils/codequality_parser.js b/app/assets/javascripts/ci/reports/codequality_report/utils/codequality_parser.js
index 417297df43c..417297df43c 100644
--- a/app/assets/javascripts/ci/reports/codequality_report/store/utils/codequality_parser.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/utils/codequality_parser.js
diff --git a/app/assets/javascripts/ci/reports/components/report_section.vue b/app/assets/javascripts/ci/reports/components/report_section.vue
index fd6c6cca6b7..4a6a5e6e221 100644
--- a/app/assets/javascripts/ci/reports/components/report_section.vue
+++ b/app/assets/javascripts/ci/reports/components/report_section.vue
@@ -207,7 +207,6 @@ export default {
>
<gl-button
data-testid="report-section-expand-button"
- data-qa-selector="expand_report_button"
category="tertiary"
size="small"
:icon="isExpanded ? 'chevron-lg-up' : 'chevron-lg-down'"
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
index 8a920c85e06..a099d238c79 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
@@ -5,12 +5,11 @@ import { sprintf, __, formatNumber } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import RunnerCreatedAt from '../runner_created_at.vue';
+import RunnerJobCount from '../runner_job_count.vue';
import RunnerName from '../runner_name.vue';
import RunnerTags from '../runner_tags.vue';
import RunnerTypeBadge from '../runner_type_badge.vue';
import RunnerManagersBadge from '../runner_managers_badge.vue';
-
-import { formatJobCount } from '../../utils';
import {
I18N_LOCKED_RUNNER_DESCRIPTION,
I18N_VERSION_LABEL,
@@ -25,6 +24,7 @@ export default {
TimeAgo,
RunnerSummaryField,
RunnerCreatedAt,
+ RunnerJobCount,
RunnerName,
RunnerTags,
RunnerTypeBadge,
@@ -52,9 +52,6 @@ export default {
additionalIpAddressCount() {
return this.managersCount - 1;
},
- jobCount() {
- return formatJobCount(this.runner.jobCount);
- },
createdBy() {
return this.runner?.createdBy;
},
@@ -135,7 +132,7 @@ export default {
</runner-summary-field>
<runner-summary-field icon="pipeline" data-testid="job-count" :tooltip="__('Jobs')">
- {{ jobCount }}
+ <runner-job-count :runner="runner" />
</runner-summary-field>
<runner-summary-field icon="calendar">
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue b/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue
index b1b61e03eec..5ed987d28e7 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue
@@ -26,8 +26,8 @@ export default {
<template>
<div v-gl-tooltip="tooltip" class="gl-display-inline-block gl-text-secondary gl-mb-3 gl-mr-4">
<gl-icon v-if="icon" :name="icon" :size="12" />
- <!-- display tooltip as a label for screen readers -->
- <span class="gl-sr-only">{{ tooltip }}</span>
+ <!-- display tooltip as a label for screen readers and make it unavailable for copying -->
+ <span class="gl-sr-only gl-user-select-none">{{ tooltip }}</span>
<slot></slot>
</div>
</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_job_count.vue b/app/assets/javascripts/ci/runner/components/runner_job_count.vue
new file mode 100644
index 00000000000..596e027efef
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_job_count.vue
@@ -0,0 +1,36 @@
+<script>
+import runnerJobCountQuery from '../graphql/list/runner_job_count.query.graphql';
+import { formatJobCount } from '../utils';
+
+export default {
+ name: 'RunnerJobCount',
+ props: {
+ runner: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ jobCount: '-',
+ };
+ },
+ apollo: {
+ jobCount: {
+ query: runnerJobCountQuery,
+ variables() {
+ return { id: this.runner.id };
+ },
+ context: {
+ batchKey: 'RunnerJobCount',
+ },
+ update(data) {
+ return formatJobCount(data?.runner?.jobCount);
+ },
+ },
+ },
+};
+</script>
+<template>
+ <span>{{ jobCount }}</span>
+</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue b/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
index 653d9b05330..3cad0c52cd7 100644
--- a/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
@@ -3,7 +3,7 @@ import { GlTableLite } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { formatTime } from '~/lib/utils/datetime_utility';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import RunnerTags from '~/ci/runner/components/runner_tags.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { tableField } from '../utils';
diff --git a/app/assets/javascripts/ci/runner/components/runner_list.vue b/app/assets/javascripts/ci/runner/components/runner_list.vue
index ec04701db2c..0282ac10fba 100644
--- a/app/assets/javascripts/ci/runner/components/runner_list.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_list.vue
@@ -4,7 +4,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__ } from '~/locale';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql';
-import { formatJobCount, tableField } from '../utils';
+import { tableField } from '../utils';
import RunnerBulkDelete from './runner_bulk_delete.vue';
import RunnerBulkDeleteCheckbox from './runner_bulk_delete_checkbox.vue';
import RunnerSummaryCell from './cells/runner_summary_cell.vue';
@@ -95,9 +95,6 @@ export default {
onDeleted(event) {
this.$emit('deleted', event);
},
- formatJobCount(jobCount) {
- return formatJobCount(jobCount);
- },
runnerTrAttr(runner) {
if (runner) {
return {
diff --git a/app/assets/javascripts/ci/runner/components/runner_list_header.vue b/app/assets/javascripts/ci/runner/components/runner_list_header.vue
index e4367db035e..6ed271d15ab 100644
--- a/app/assets/javascripts/ci/runner/components/runner_list_header.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_list_header.vue
@@ -7,7 +7,7 @@ export default {
<header
class="gl-my-5 gl-display-flex gl-align-items-flex-start gl-flex-wrap gl-justify-content-space-between"
>
- <h1 v-if="$scopedSlots.title" class="gl-my-0 gl-font-size-h1 header-title">
+ <h1 v-if="$scopedSlots.title" class="gl-mt-0 gl-mb-3 gl-font-size-h1 header-title">
<slot name="title"></slot>
</h1>
<div v-if="$scopedSlots.actions" class="gl-display-flex gl-gap-3">
diff --git a/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql
index 7ad9605d0a4..f6c96802004 100644
--- a/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql
@@ -8,7 +8,6 @@ fragment ListItemShared on CiRunner {
version
paused
locked
- jobCount
tagList
createdAt
createdBy {
diff --git a/app/assets/javascripts/ci/runner/graphql/list/runner_job_count.query.graphql b/app/assets/javascripts/ci/runner/graphql/list/runner_job_count.query.graphql
new file mode 100644
index 00000000000..79ea19b048a
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/graphql/list/runner_job_count.query.graphql
@@ -0,0 +1,6 @@
+query runnerJobCount($id: CiRunnerID!) {
+ runner(id: $id) {
+ id
+ jobCount
+ }
+}
diff --git a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
index b5042936b1e..cafac061c12 100644
--- a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
@@ -159,7 +159,7 @@ export default {
search: {
deep: true,
handler() {
- // TODO Implement back button reponse using onpopstate
+ // TODO Implement back button response using onpopstate
// See https://gitlab.com/gitlab-org/gitlab/-/issues/333804
updateHistory({
url: fromSearchToUrl(this.search),