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:
Diffstat (limited to 'app/assets/javascripts/ci')
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue2
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue17
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/constants.js3
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql1
-rw-r--r--app/assets/javascripts/ci/artifacts/components/bulk_delete_modal.vue1
-rw-r--r--app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue67
-rw-r--r--app/assets/javascripts/ci/catalog/components/ci_catalog_home.vue8
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue120
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue103
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue41
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue130
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_header_skeleton_loader.vue13
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue55
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_header.vue59
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue57
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/ci_resources_list.vue74
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue144
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/empty_state.vue22
-rw-r--r--app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue109
-rw-r--r--app/assets/javascripts/ci/catalog/constants.js35
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql25
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql20
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql29
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql6
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql7
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/settings.js32
-rw-r--r--app/assets/javascripts/ci/catalog/router/constants.js2
-rw-r--r--app/assets/javascripts/ci/catalog/router/index.js13
-rw-r--r--app/assets/javascripts/ci/catalog/router/routes.js9
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue43
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue368
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue19
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue10
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue5
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql1
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/utils.js23
-rw-r--r--app/assets/javascripts/ci/common/pipelines_table.vue110
-rw-r--r--app/assets/javascripts/ci/common/private/job_action_component.vue2
-rw-r--r--app/assets/javascripts/ci/constants.js15
-rw-r--r--app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue4
-rw-r--r--app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql1
-rw-r--r--app/assets/javascripts/ci/job_details/components/job_header.vue43
-rw-r--r--app/assets/javascripts/ci/job_details/components/job_log_controllers.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line.vue8
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line_header.vue4
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line_number.vue5
-rw-r--r--app/assets/javascripts/ci/job_details/components/manual_variables_form.vue8
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue14
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue6
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue34
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue4
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue141
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue6
-rw-r--r--app/assets/javascripts/ci/job_details/components/stuck_block.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql1
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql1
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql2
-rw-r--r--app/assets/javascripts/ci/job_details/index.js32
-rw-r--r--app/assets/javascripts/ci/job_details/job_app.vue33
-rw-r--r--app/assets/javascripts/ci/job_details/store/actions.js6
-rw-r--r--app/assets/javascripts/ci/job_details/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/ci/job_details/store/mutations.js6
-rw-r--r--app/assets/javascripts/ci/job_details/store/utils.js83
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue6
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue87
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue29
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue (renamed from app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue)23
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/jobs_table.vue36
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue1
-rw-r--r--app/assets/javascripts/ci/jobs_page/constants.js15
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql2
-rw-r--r--app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/constants.js2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/dag/dag.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue17
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue22
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue14
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue248
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js11
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipelines_index.js30
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue11
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue17
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue24
-rw-r--r--app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue15
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue85
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue26
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/constants.js1
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql18
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue1
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue20
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue33
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue59
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue (renamed from app/assets/javascripts/ci/pipelines_page/components/pipelines_status_badge.vue)16
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue31
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue19
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue3
-rw-r--r--app/assets/javascripts/ci/pipelines_page/pipelines.vue81
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/utils.js45
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_details.vue15
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_form_fields.vue10
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_header.vue3
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_type_icon.vue62
-rw-r--r--app/assets/javascripts/ci/runner/constants.js50
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql4
-rw-r--r--app/assets/javascripts/ci/runner/sentry_utils.js17
-rw-r--r--app/assets/javascripts/ci/utils.js16
109 files changed, 2347 insertions, 1135 deletions
diff --git a/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue b/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue
index cbb80a5175f..9d516fc267d 100644
--- a/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue
@@ -23,6 +23,6 @@ export default {
</script>
<template>
<div class="gl-text-truncate">
- <gl-link :href="projectUrl"> {{ projectName }}</gl-link>
+ <gl-link :href="projectUrl" data-testid="job-project-link">{{ projectName }}</gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue b/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue
index a76829aa129..e44f756a5c5 100644
--- a/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue
@@ -1,5 +1,6 @@
<script>
-import { GlLink } from '@gitlab/ui';
+import { GlLink, GlTooltipDirective } from '@gitlab/ui';
+import RunnerTypeIcon from '~/ci/runner/components/runner_type_icon.vue';
import { RUNNER_EMPTY_TEXT, RUNNER_NO_DESCRIPTION } from '../../constants';
export default {
@@ -9,6 +10,10 @@ export default {
},
components: {
GlLink,
+ RunnerTypeIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
props: {
job: {
@@ -25,15 +30,19 @@ export default {
? this.job.runner.description
: this.$options.i18n.noRunnerDescription;
},
+ runnerType() {
+ return this.job.runner?.runnerType;
+ },
},
};
</script>
<template>
<div class="gl-text-truncate">
- <gl-link v-if="adminUrl" :href="adminUrl">
- {{ description }}
- </gl-link>
+ <span v-if="adminUrl">
+ <runner-type-icon :type="runnerType" class="gl-vertical-align-middle" />
+ <gl-link :href="adminUrl" data-testid="job-runner-link"> {{ description }} </gl-link>
+ </span>
<span v-else data-testid="empty-runner-text"> {{ $options.i18n.emptyRunnerText }}</span>
</div>
</template>
diff --git a/app/assets/javascripts/ci/admin/jobs_table/constants.js b/app/assets/javascripts/ci/admin/jobs_table/constants.js
index ff0efdb1f5b..86c9ab53e75 100644
--- a/app/assets/javascripts/ci/admin/jobs_table/constants.js
+++ b/app/assets/javascripts/ci/admin/jobs_table/constants.js
@@ -26,9 +26,6 @@ export const DEFAULT_FIELDS_ADMIN = [
{ key: 'project', label: __('Project'), columnClass: 'gl-w-20p' },
{ key: 'runner', label: __('Runner'), columnClass: 'gl-w-15p' },
{ key: 'pipeline', label: __('Pipeline'), columnClass: 'gl-w-10p' },
- { key: 'stage', label: __('Stage'), columnClass: 'gl-w-10p' },
- { key: 'name', label: __('Name'), columnClass: 'gl-w-15p' },
- { key: 'duration', label: __('Duration'), columnClass: 'gl-w-15p' },
{ key: 'actions', label: '', columnClass: 'gl-w-10p' },
];
diff --git a/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql
index 89fb1782e46..2e77f4db907 100644
--- a/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql
+++ b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql
@@ -16,6 +16,7 @@ query getAllJobs(
id
description
adminUrl
+ runnerType
}
artifacts {
nodes {
diff --git a/app/assets/javascripts/ci/artifacts/components/bulk_delete_modal.vue b/app/assets/javascripts/ci/artifacts/components/bulk_delete_modal.vue
index 00f5b2eab7d..c27ec0dd500 100644
--- a/app/assets/javascripts/ci/artifacts/components/bulk_delete_modal.vue
+++ b/app/assets/javascripts/ci/artifacts/components/bulk_delete_modal.vue
@@ -65,6 +65,7 @@ export default {
:title="$options.i18n.modalTitle(checkedCount)"
:action-primary="modalActionPrimary"
:action-cancel="modalActionCancel"
+ data-testid="artifacts-bulk-delete-modal"
v-bind="$attrs"
v-on="$listeners"
>
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 e08470c62be..d8f9eb65236 100644
--- a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
+++ b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
@@ -5,16 +5,15 @@ import {
GlLink,
GlButtonGroup,
GlButton,
- GlBadge,
GlIcon,
GlPagination,
GlFormCheckbox,
GlTooltipDirective,
} from '@gitlab/ui';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { createAlert } from '~/alert';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
import getJobArtifactsQuery from '../graphql/queries/get_job_artifacts.query.graphql';
import { totalArtifactsSizeForJob, mapArchivesToJobNodes, mapBooleansToJobNodes } from '../utils';
@@ -65,12 +64,11 @@ export default {
GlLink,
GlButtonGroup,
GlButton,
- GlBadge,
GlIcon,
GlPagination,
GlFormCheckbox,
- CiIcon,
TimeAgo,
+ CiBadgeLink,
JobCheckbox,
ArtifactsBulkDelete,
BulkDeleteModal,
@@ -328,7 +326,7 @@ export default {
{
key: 'artifacts',
label: I18N_ARTIFACTS,
- thClass: 'gl-w-quarter',
+ thClass: 'gl-w-eighth',
},
{
key: 'job',
@@ -350,7 +348,7 @@ export default {
{
key: 'actions',
label: '',
- thClass: 'gl-w-eighth',
+ thClass: 'gl-w-20p',
tdClass: 'gl-text-right',
},
],
@@ -403,6 +401,7 @@ export default {
:checked="isAnyVisibleArtifactSelected"
:indeterminate="isAnyVisibleArtifactSelected && !areAllVisibleArtifactsSelected"
:disabled="isSelectedArtifactsLimitReached && !isAnyVisibleArtifactSelected"
+ data-testid="select-all-artifacts-checkbox"
@change="handleSelectAllChecked"
/>
</template>
@@ -441,45 +440,37 @@ export default {
</span>
</template>
<template #cell(job)="{ item }">
- <span class="gl-display-inline-flex gl-align-items-center gl-w-full gl-mb-4">
+ <div class="gl-display-inline-flex gl-align-items-center gl-mb-3 gl-gap-3">
<span data-testid="job-artifacts-job-status">
- <ci-icon v-if="item.succeeded" :status="item.detailedStatus" class="gl-mr-3" />
- <gl-badge
- v-else
- :icon="item.detailedStatus.icon"
- :variant="$options.STATUS_BADGE_VARIANTS[item.detailedStatus.group]"
- class="gl-mr-3"
- >
- {{ item.detailedStatus.label }}
- </gl-badge>
+ <ci-badge-link :status="item.detailedStatus" size="sm" :show-text="false" />
</span>
- <gl-link :href="item.webPath" class="gl-font-weight-bold">
+ <gl-link :href="item.webPath">
{{ item.name }}
</gl-link>
- </span>
- <span class="gl-display-inline-flex">
+ </div>
+ <div class="gl-mb-1">
<gl-icon name="pipeline" class="gl-mr-2" />
- <gl-link
- :href="item.pipeline.path"
- class="gl-text-black-normal gl-text-decoration-underline gl-mr-4"
- >
+ <gl-link :href="item.pipeline.path" class="gl-mr-2">
{{ pipelineId(item) }}
</gl-link>
- <gl-icon name="branch" class="gl-mr-2" />
- <gl-link
- :href="item.refPath"
- class="gl-text-black-normal gl-text-decoration-underline gl-mr-4"
- >
- {{ item.refName }}
- </gl-link>
- <gl-icon name="commit" class="gl-mr-2" />
- <gl-link
- :href="item.commitPath"
- class="gl-text-black-normal gl-text-decoration-underline gl-mr-4"
- >
- {{ item.shortSha }}
- </gl-link>
- </span>
+ <span class="gl-display-inline-block gl-rounded-base gl-px-2 gl-bg-gray-50">
+ <gl-icon name="commit" :size="12" class="gl-mr-2" />
+ <gl-link
+ :href="item.commitPath"
+ class="gl-text-black-normal gl-font-sm gl-font-monospace"
+ >
+ {{ item.shortSha }}
+ </gl-link>
+ </span>
+ </div>
+ <div>
+ <span class="gl-display-inline-block gl-rounded-base gl-px-2 gl-bg-gray-50">
+ <gl-icon name="branch" :size="12" class="gl-mr-1" />
+ <gl-link :href="item.refPath" class="gl-text-black-normal gl-font-sm gl-font-monospace">
+ {{ item.refName }}
+ </gl-link>
+ </span>
+ </div>
</template>
<template #cell(size)="{ item }">
<span data-testid="job-artifacts-size">{{ artifactsSize(item) }}</span>
diff --git a/app/assets/javascripts/ci/catalog/components/ci_catalog_home.vue b/app/assets/javascripts/ci/catalog/components/ci_catalog_home.vue
new file mode 100644
index 00000000000..5fe7e8286ec
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/ci_catalog_home.vue
@@ -0,0 +1,8 @@
+<script>
+export default {};
+</script>
+<template>
+ <div>
+ <router-view />
+ </div>
+</template>
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
new file mode 100644
index 00000000000..572a8183730
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue
@@ -0,0 +1,120 @@
+<script>
+import { GlIcon, GlLink } from '@gitlab/ui';
+import { n__, s__, sprintf } from '~/locale';
+import { formatDate } from '~/lib/utils/datetime_utility';
+
+export default {
+ components: {
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ isLoadingDetails: {
+ type: Boolean,
+ required: true,
+ },
+ isLoadingSharedData: {
+ type: Boolean,
+ required: true,
+ },
+ openIssuesCount: {
+ required: false,
+ type: Number,
+ default: 0,
+ },
+ openMergeRequestsCount: {
+ required: false,
+ type: Number,
+ default: 0,
+ },
+ latestVersion: {
+ required: false,
+ type: Object,
+ default: () => ({}),
+ },
+ webPath: {
+ required: false,
+ type: String,
+ default: '',
+ },
+ },
+ computed: {
+ hasVersion() {
+ return this.latestVersion;
+ },
+ lastReleaseText() {
+ if (this.hasVersion) {
+ return sprintf(this.$options.i18n.lastRelease, {
+ date: this.releasedAt,
+ });
+ }
+
+ return this.$options.i18n.lastReleaseMissing;
+ },
+ openIssuesText() {
+ return n__('%d issue', '%d issues', this.openIssuesCount);
+ },
+ openMergeRequestText() {
+ return n__('%d merge request', '%d merge requests', this.openMergeRequestsCount);
+ },
+ releasedAt() {
+ return this.hasVersion && formatDate(this.latestVersion.releasedAt, 'yyyy-mm-dd');
+ },
+ projectInfoItems() {
+ return [
+ {
+ icon: 'project',
+ link: `${this.webPath}`,
+ text: this.$options.i18n.projectLink,
+ isLoading: this.isLoadingSharedData,
+ },
+ {
+ icon: 'issues',
+ link: `${this.webPath}/issues`,
+ text: this.openIssuesText,
+ isLoading: this.isLoadingDetails,
+ },
+ {
+ icon: 'merge-request',
+ link: `${this.webPath}/merge_requests`,
+ text: this.openMergeRequestText,
+ isLoading: this.isLoadingDetails,
+ },
+ {
+ icon: 'clock',
+ text: this.lastReleaseText,
+ isLoading: this.isLoadingSharedData,
+ },
+ ];
+ },
+ },
+ i18n: {
+ projectLink: s__('CiCatalog|Go to the project'),
+ lastRelease: s__('CiCatalog|Last release at %{date}'),
+ lastReleaseMissing: s__('CiCatalog|No release available'),
+ },
+};
+</script>
+
+<template>
+ <div class="gl-py-2 gl-sm-display-flex gl-gap-5">
+ <span
+ v-for="item in projectInfoItems"
+ :key="`${item.icon}`"
+ class="gl-display-flex gl-align-items-center gl-xs-mb-3"
+ >
+ <gl-icon class="gl-text-primary gl-mr-2" :name="item.icon" />
+ <div
+ v-if="item.isLoading"
+ class="gl-animate-skeleton-loader gl-h-4 gl-rounded-base gl-w-15"
+ data-testid="skeleton-loading-line"
+ ></div>
+ <template v-else>
+ <gl-link v-if="item.link" :href="item.link"> {{ item.text }} </gl-link>
+ <span v-else class="gl-text-secondary">
+ {{ item.text }}
+ </span>
+ </template>
+ </span>
+ </div>
+</template>
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
new file mode 100644
index 00000000000..85dfa12c756
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
@@ -0,0 +1,103 @@
+<script>
+import { GlLoadingIcon, GlTableLite } from '@gitlab/ui';
+import { createAlert } from '~/alert';
+import { __, s__ } from '~/locale';
+import getCiCatalogResourceComponents from '../../graphql/queries/get_ci_catalog_resource_components.query.graphql';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlTableLite,
+ },
+ props: {
+ resourceId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ components: [],
+ };
+ },
+ apollo: {
+ components: {
+ query: getCiCatalogResourceComponents,
+ variables() {
+ return {
+ id: this.resourceId,
+ };
+ },
+ update(data) {
+ return data?.ciCatalogResource?.components?.nodes || [];
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.fetchError });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.components.loading;
+ },
+ },
+ methods: {
+ generateSnippet(componentPath) {
+ // This is not to be translated because it is our CI yaml syntax.
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `include:
+ - component: ${componentPath}`;
+ },
+ humanizeBoolean(bool) {
+ return bool ? __('Yes') : __('No');
+ },
+ },
+ fields: [
+ {
+ key: 'name',
+ label: s__('CiCatalogComponent|Parameters'),
+ thClass: 'gl-w-40p',
+ },
+ {
+ key: 'defaultValue',
+ label: s__('CiCatalogComponent|Default Value'),
+ thClass: 'gl-w-40p',
+ },
+ {
+ key: 'required',
+ label: s__('CiCatalogComponent|Mandatory'),
+ thClass: 'gl-w-20p',
+ },
+ ],
+ i18n: {
+ inputTitle: s__('CiCatalogComponent|Inputs'),
+ fetchError: s__("CiCatalogComponent|There was an error fetching this resource's components"),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-loading-icon v-if="isLoading" size="lg" />
+ <template v-else>
+ <div
+ v-for="component in components"
+ :key="component.id"
+ class="gl-mb-8"
+ 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>
+ <pre class="gl-w-85p gl-py-4">{{ generateSnippet(component.path) }}</pre>
+ <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">
+ <template #cell(required)="{ item }">
+ {{ humanizeBoolean(item.required) }}
+ </template>
+ </gl-table-lite>
+ </div>
+ </div>
+ </template>
+ </div>
+</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
new file mode 100644
index 00000000000..c0feb52c185
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
@@ -0,0 +1,41 @@
+<script>
+import { GlTab, GlTabs } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import CiResourceComponents from './ci_resource_components.vue';
+import CiResourceReadme from './ci_resource_readme.vue';
+
+export default {
+ components: {
+ CiResourceReadme,
+ CiResourceComponents,
+ GlTab,
+ GlTabs,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ props: {
+ resourceId: {
+ type: String,
+ required: true,
+ },
+ },
+ i18n: {
+ tabs: {
+ components: s__('CiCatalog|Components'),
+ readme: s__('CiCatalog|Readme'),
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-tabs>
+ <gl-tab v-if="glFeatures.ciCatalogComponentsTab" :title="$options.i18n.tabs.components" lazy>
+ <ci-resource-components :resource-id="resourceId"
+ /></gl-tab>
+ <gl-tab :title="$options.i18n.tabs.readme" lazy>
+ <ci-resource-readme :resource-id="resourceId" />
+ </gl-tab>
+ </gl-tabs>
+</template>
+<style></style>
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
new file mode 100644
index 00000000000..6673785ffd2
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
@@ -0,0 +1,130 @@
+<script>
+import { GlAvatar, GlAvatarLink, GlBadge } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { isNumeric } from '~/lib/utils/number_utils';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiResourceAbout from './ci_resource_about.vue';
+import CiResourceHeaderSkeletonLoader from './ci_resource_header_skeleton_loader.vue';
+
+export default {
+ components: {
+ CiBadgeLink,
+ CiResourceAbout,
+ CiResourceHeaderSkeletonLoader,
+ GlAvatar,
+ GlAvatarLink,
+ GlBadge,
+ },
+ props: {
+ isLoadingDetails: {
+ type: Boolean,
+ required: true,
+ },
+ isLoadingSharedData: {
+ type: Boolean,
+ required: true,
+ },
+ openIssuesCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ openMergeRequestsCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ pipelineStatus: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ resource: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ entityId() {
+ return getIdFromGraphQLId(this.resource.id);
+ },
+ fullPath() {
+ return `${this.rootNamespace.fullPath}/${this.rootNamespace.name}`;
+ },
+ hasLatestVersion() {
+ return this.latestVersion?.tagName;
+ },
+ hasPipelineStatus() {
+ return this.pipelineStatus?.text;
+ },
+ latestVersion() {
+ return this.resource.latestVersion;
+ },
+ rootNamespace() {
+ return this.resource.rootNamespace;
+ },
+ versionBadgeText() {
+ return isNumeric(this.latestVersion.tagName)
+ ? `v${this.latestVersion.tagName}`
+ : this.latestVersion.tagName;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <ci-resource-header-skeleton-loader v-if="isLoadingSharedData" class="gl-py-5" />
+ <div v-else class="gl-display-flex gl-py-5">
+ <gl-avatar-link :href="resource.webPath">
+ <gl-avatar
+ class="gl-mr-4"
+ :entity-id="entityId"
+ :entity-name="resource.name"
+ shape="rect"
+ :size="64"
+ :src="resource.icon"
+ />
+ </gl-avatar-link>
+ <div
+ 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 }}
+ </div>
+ <span class="gl-display-flex">
+ <div class="gl-font-lg gl-font-weight-bold">{{ resource.name }}</div>
+ <gl-badge
+ v-if="hasLatestVersion"
+ size="sm"
+ class="gl-ml-3 gl-my-1"
+ :href="latestVersion.tagPath"
+ >
+ {{ versionBadgeText }}
+ </gl-badge>
+ </span>
+ <ci-badge-link
+ v-if="hasPipelineStatus"
+ class="gl-mt-2"
+ :status="pipelineStatus"
+ size="sm"
+ show-text
+ />
+ </div>
+ </div>
+ <ci-resource-about
+ :is-loading-details="isLoadingDetails"
+ :is-loading-shared-data="isLoadingSharedData"
+ :open-issues-count="openIssuesCount"
+ :open-merge-requests-count="openMergeRequestsCount"
+ :latest-version="latestVersion"
+ :web-path="resource.webPath"
+ />
+ <div
+ v-if="isLoadingSharedData"
+ class="gl-animate-skeleton-loader gl-h-4 gl-rounded-base gl-my-3 gl-max-w-20!"
+ ></div>
+ <p v-else class="gl-mt-3">
+ {{ resource.description }}
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_header_skeleton_loader.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header_skeleton_loader.vue
new file mode 100644
index 00000000000..83ea224d772
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header_skeleton_loader.vue
@@ -0,0 +1,13 @@
+<script>
+export default {};
+</script>
+
+<template>
+ <div class="gl-display-flex">
+ <div class="gl-animate-skeleton-loader gl-h-11 gl-rounded-base gl-w-11"></div>
+ <div class="gl-pl-4 gl--flex-center gl-flex-direction-column">
+ <div class="gl-animate-skeleton-loader gl-h-4 gl-rounded-base gl-mb-3 gl-w-20"></div>
+ <div class="gl-animate-skeleton-loader gl-h-4 gl-rounded-base gl-w-20"></div>
+ </div>
+ </div>
+</template>
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
new file mode 100644
index 00000000000..d473833869d
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import { createAlert } from '~/alert';
+import { __ } from '~/locale';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import getCiCatalogResourceReadme from '../../graphql/queries/get_ci_catalog_resource_readme.query.graphql';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ },
+ directives: { SafeHtml },
+ props: {
+ resourceId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ readmeHtml: null,
+ };
+ },
+ apollo: {
+ readmeHtml: {
+ query: getCiCatalogResourceReadme,
+ variables() {
+ return {
+ id: this.resourceId,
+ };
+ },
+ update(data) {
+ return data?.ciCatalogResource?.readmeHtml || null;
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.loadingError });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.readmeHtml.loading;
+ },
+ },
+ i18n: {
+ loadingError: __("There was a problem loading this project's readme content."),
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
+ <div v-else v-safe-html="readmeHtml"></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
new file mode 100644
index 00000000000..487215875c0
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
@@ -0,0 +1,59 @@
+<script>
+import { GlBanner, GlLink } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { CATALOG_FEEDBACK_DISMISSED_KEY } from '../../constants';
+
+export default {
+ components: {
+ GlBanner,
+ GlLink,
+ },
+ inject: ['pageTitle', 'pageDescription'],
+ data() {
+ return {
+ isFeedbackBannerDismissed: localStorage.getItem(CATALOG_FEEDBACK_DISMISSED_KEY) === 'true',
+ };
+ },
+ methods: {
+ handleDismissBanner() {
+ localStorage.setItem(CATALOG_FEEDBACK_DISMISSED_KEY, 'true');
+ this.isFeedbackBannerDismissed = true;
+ },
+ },
+ i18n: {
+ banner: {
+ title: __('Your feedback is important to us đź‘‹'),
+ description: s__(
+ "CiCatalog|We want to help you create and manage pipeline component repositories, while also making it easier to reuse pipeline configurations. Let us know how we're doing!",
+ ),
+ btnText: __('Give us some feedback'),
+ },
+ learnMore: __('Learn more'),
+ },
+ learnMorePath: helpPagePath('ci/components/index'),
+};
+</script>
+<template>
+ <div class="gl-border-b-1 gl-border-gray-100 gl-border-b-solid">
+ <gl-banner
+ v-if="!isFeedbackBannerDismissed"
+ class="gl-mt-5"
+ :title="$options.i18n.banner.title"
+ :button-text="$options.i18n.banner.btnText"
+ button-link="https://gitlab.com/gitlab-org/gitlab/-/issues/407556"
+ @close="handleDismissBanner"
+ >
+ <p>
+ {{ $options.i18n.banner.description }}
+ </p>
+ </gl-banner>
+ <h1 class="gl-font-size-h-display">{{ pageTitle }}</h1>
+ <p>
+ <span>{{ pageDescription }}</span>
+ <gl-link :href="$options.learnMorePath" target="_blank">{{
+ $options.i18n.learnMore
+ }}</gl-link>
+ </p>
+ </div>
+</template>
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
new file mode 100644
index 00000000000..3722b8e6c59
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue
@@ -0,0 +1,57 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+ data() {
+ return {
+ coordinates: {
+ statsX: 0,
+ releaseDateX: 0,
+ },
+ width: 0,
+ };
+ },
+ mounted() {
+ this.setSvgSize();
+ },
+ methods: {
+ setSvgSize() {
+ this.width = this.$el.offsetWidth;
+ this.coordinates.releaseDateX = this.width - 200;
+ this.coordinates.statsX = this.width - 90;
+ },
+ },
+ skeletonLoadItems: new Array(5),
+};
+</script>
+<template>
+ <div class="gl-w-full">
+ <gl-skeleton-loader
+ v-for="(_, index) in $options.skeletonLoadItems"
+ :key="index"
+ :height="60"
+ :width="width"
+ >
+ <!-- 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" />
+ <!-- Project description -->
+ <rect x="60" y="30" 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" />
+ </gl-skeleton-loader>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list.vue b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list.vue
new file mode 100644
index 00000000000..d1fd9fe977b
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list.vue
@@ -0,0 +1,74 @@
+<script>
+import { GlKeysetPagination } from '@gitlab/ui';
+
+import { s__, sprintf } from '~/locale';
+import { ciCatalogResourcesItemsCount } from '../../graphql/settings';
+import CiResourcesListItem from './ci_resources_list_item.vue';
+
+export default {
+ components: {
+ CiResourcesListItem,
+ GlKeysetPagination,
+ },
+ props: {
+ currentPage: {
+ type: Number,
+ required: true,
+ },
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ resources: {
+ type: Array,
+ required: true,
+ },
+ totalCount: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ showPageCount() {
+ return typeof this.totalPageCount === 'number' && this.totalPageCount > 0;
+ },
+ totalPageCount() {
+ return Math.ceil(this.totalCount / ciCatalogResourcesItemsCount);
+ },
+ pageText() {
+ return sprintf(this.$options.i18n.pageText, {
+ currentPage: this.currentPage,
+ totalPage: this.totalPageCount,
+ });
+ },
+ },
+ i18n: {
+ pageText: s__('CiCatalog|Page %{currentPage} of %{totalPage}'),
+ },
+};
+</script>
+<template>
+ <div>
+ <ul class="gl-p-0" data-testId="catalog-list-container">
+ <ci-resources-list-item
+ v-for="resource in resources"
+ :key="resource.id"
+ :resource="resource"
+ />
+ </ul>
+ <div class="gl-display-flex gl-justify-content-center">
+ <gl-keyset-pagination
+ v-bind="pageInfo"
+ @prev="$emit('onPrevPage')"
+ @next="$emit('onNextPage')"
+ />
+ </div>
+ <div
+ v-if="showPageCount"
+ class="gl-display-flex gl-justify-content-center gl-mt-3"
+ data-testid="pageCount"
+ >
+ {{ pageText }}
+ </div>
+ </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
new file mode 100644
index 00000000000..63243539575
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
@@ -0,0 +1,144 @@
+<script>
+import {
+ GlAvatar,
+ GlBadge,
+ GlButton,
+ GlIcon,
+ GlLink,
+ GlSprintf,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
+import { CI_RESOURCE_DETAILS_PAGE_NAME } from '../../router/constants';
+
+export default {
+ i18n: {
+ unreleased: s__('CiCatalog|Unreleased'),
+ releasedMessage: s__('CiCatalog|Released %{timeAgo} by %{author}'),
+ },
+ components: {
+ GlAvatar,
+ GlBadge,
+ GlButton,
+ GlIcon,
+ GlLink,
+ GlSprintf,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ resource: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ authorName() {
+ return this.latestVersion.author.name;
+ },
+ authorProfileUrl() {
+ return this.latestVersion.author.webUrl;
+ },
+ entityId() {
+ return getIdFromGraphQLId(this.resource.id);
+ },
+ starCount() {
+ return this.resource?.starCount || 0;
+ },
+ forksCount() {
+ return this.resource?.forksCount || 0;
+ },
+ hasReleasedVersion() {
+ return Boolean(this.latestVersion?.releasedAt);
+ },
+ formattedDate() {
+ return formatDate(this.latestVersion?.releasedAt);
+ },
+ latestVersion() {
+ return this.resource?.latestVersion || {};
+ },
+ 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;
+ },
+ },
+ methods: {
+ navigateToDetailsPage() {
+ this.$router.push({
+ name: CI_RESOURCE_DETAILS_PAGE_NAME,
+ params: { id: this.entityId },
+ });
+ },
+ },
+};
+</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"
+ data-testid="catalog-resource-item"
+ >
+ <gl-avatar
+ class="gl-mr-4"
+ :entity-id="entityId"
+ :entity-name="resource.name"
+ shape="rect"
+ :size="48"
+ :src="resource.icon"
+ @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"
+ class="gl-text-gray-900! gl-mr-1"
+ data-testid="ci-resource-link"
+ @click="navigateToDetailsPage"
+ >
+ {{ resourcePath }} <b> {{ resource.name }}</b>
+ </gl-button>
+ <div class="gl-display-flex gl-flex-grow-1 gl-md-justify-content-space-between">
+ <gl-badge size="sm">{{ 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 class="gl-mr-3">{{ starCount }}</span>
+ </span>
+ <span class="gl--flex-center" data-testid="stats-forks">
+ <gl-icon name="fork" :size="14" class="gl-mr-1" />
+ <span>{{ forksCount }}</span>
+ </span>
+ </span>
+ </div>
+ </div>
+ <div class="gl-display-flex gl-sm-flex-direction-column gl-justify-content-space-between">
+ <span class="gl-display-flex gl-flex-basis-two-thirds gl-font-sm">{{
+ 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">
+ {{ releasedAt }}
+ </span>
+ </template>
+ <template #author>
+ <gl-link :href="authorProfileUrl" data-testid="user-link">
+ <span>{{ authorName }}</span>
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ </div>
+ </div>
+ </div>
+ </li>
+</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
new file mode 100644
index 00000000000..a53ddefaa50
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/list/empty_state.vue
@@ -0,0 +1,22 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+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: {
+ GlEmptyState,
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-empty-state :title="$options.i18n.title" :description="$options.i18n.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
new file mode 100644
index 00000000000..da2c73be900
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue
@@ -0,0 +1,109 @@
+<script>
+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 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';
+import CiResourceHeader from '../details/ci_resource_header.vue';
+
+export default {
+ components: {
+ CiResourceDetails,
+ CiResourceHeader,
+ GlEmptyState,
+ },
+ inject: ['ciCatalogPath'],
+ data() {
+ return {
+ isEmpty: false,
+ resourceSharedData: {},
+ resourceAdditionalDetails: {},
+ };
+ },
+ apollo: {
+ resourceSharedData: {
+ query: getCatalogCiResourceSharedData,
+ variables() {
+ return {
+ id: this.graphQLId,
+ };
+ },
+ update(data) {
+ return data.ciCatalogResource;
+ },
+ error(e) {
+ this.isEmpty = true;
+ createAlert({ message: e.message });
+ },
+ },
+ resourceAdditionalDetails: {
+ query: getCatalogCiResourceDetails,
+ variables() {
+ return {
+ id: this.graphQLId,
+ };
+ },
+ update(data) {
+ return data.ciCatalogResource;
+ },
+ error(e) {
+ this.isEmpty = true;
+ createAlert({ message: e.message });
+ },
+ },
+ },
+ computed: {
+ graphQLId() {
+ return convertToGraphQLId(CI_CATALOG_RESOURCE_TYPE, this.$route.params.id);
+ },
+ isLoadingDetails() {
+ return this.$apollo.queries.resourceAdditionalDetails.loading;
+ },
+ isLoadingSharedData() {
+ return this.$apollo.queries.resourceSharedData.loading;
+ },
+ versions() {
+ return this.resourceAdditionalDetails?.versions?.nodes || [];
+ },
+ pipelineStatus() {
+ return (
+ this.resourceAdditionalDetails?.versions?.nodes[0]?.commit?.pipelines?.nodes[0]
+ ?.detailedStatus || null
+ );
+ },
+ },
+ i18n: {
+ emptyStateTitle: s__('CiCatalog|No component available'),
+ emptyStateDescription: s__(
+ 'CiCatalog|Component ID not found, or you do not have permission to access component.',
+ ),
+ emptyStateButtonText: s__('CiCatalog|Back to the CI/CD Catalog'),
+ },
+};
+</script>
+<template>
+ <div>
+ <div v-if="isEmpty" class="gl-display-flex">
+ <gl-empty-state
+ :title="$options.i18n.emptyStateTitle"
+ :description="$options.i18n.emptyStateDescription"
+ :primary-button-text="$options.i18n.emptyStateButtonText"
+ :primary-button-link="ciCatalogPath"
+ />
+ </div>
+ <div v-else>
+ <ci-resource-header
+ :open-issues-count="resourceAdditionalDetails.openIssuesCount"
+ :open-merge-requests-count="resourceAdditionalDetails.openMergeRequestsCount"
+ :is-loading-details="isLoadingDetails"
+ :is-loading-shared-data="isLoadingSharedData"
+ :pipeline-status="pipelineStatus"
+ :resource="resourceSharedData"
+ />
+ <ci-resource-details :resource-id="graphQLId" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/constants.js b/app/assets/javascripts/ci/catalog/constants.js
new file mode 100644
index 00000000000..ab067f991cd
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/constants.js
@@ -0,0 +1,35 @@
+// We disable this for the entire file until the mock data is cleanup
+/* eslint-disable @gitlab/require-i18n-strings */
+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 }] },
+ },
+ ],
+};
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
new file mode 100644
index 00000000000..f4d1bb0eaaf
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
@@ -0,0 +1,25 @@
+fragment CatalogResourceFields on CiCatalogResource {
+ id
+ icon
+ name
+ description
+ starCount
+ forksCount
+ latestVersion {
+ id
+ tagName
+ tagPath
+ releasedAt
+ author {
+ id
+ name
+ webUrl
+ }
+ }
+ rootNamespace {
+ id
+ fullPath
+ name
+ }
+ webPath
+}
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
new file mode 100644
index 00000000000..6aef5dcc4e7
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql
@@ -0,0 +1,20 @@
+query getCiCatalogResourceComponents($id: CiCatalogResourceID!) {
+ ciCatalogResource(id: $id) {
+ id
+ components @client {
+ nodes {
+ id
+ name
+ description
+ path
+ inputs {
+ nodes {
+ name
+ defaultValue
+ required
+ }
+ }
+ }
+ }
+ }
+}
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
new file mode 100644
index 00000000000..382d3866795
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql
@@ -0,0 +1,29 @@
+query getCiCatalogResourceDetails($id: CiCatalogResourceID!) {
+ ciCatalogResource(id: $id) {
+ id
+ openIssuesCount
+ openMergeRequestsCount
+ versions(first: 1) {
+ nodes {
+ id
+ commit {
+ id
+ pipelines(first: 1) {
+ nodes {
+ id
+ detailedStatus {
+ id
+ detailsPath
+ icon
+ text
+ group
+ }
+ }
+ }
+ }
+ tagName
+ releasedAt
+ }
+ }
+ }
+}
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
new file mode 100644
index 00000000000..6b3d0cdcfc7
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql
@@ -0,0 +1,6 @@
+query getCiCatalogResourceReadme($id: CiCatalogResourceID!) {
+ ciCatalogResource(id: $id) {
+ id
+ 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
new file mode 100644
index 00000000000..4ac4cb0e394
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql
@@ -0,0 +1,7 @@
+#import "../fragments/catalog_resource.fragment.graphql"
+
+query getCiCatalogResourceSharedData($id: CiCatalogResourceID!) {
+ ciCatalogResource(id: $id) {
+ ...CatalogResourceFields
+ }
+}
diff --git a/app/assets/javascripts/ci/catalog/graphql/settings.js b/app/assets/javascripts/ci/catalog/graphql/settings.js
new file mode 100644
index 00000000000..a87b26ca4fc
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/settings.js
@@ -0,0 +1,32 @@
+import { componentsMockData } from '../constants';
+
+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,
+ },
+ },
+ },
+ },
+ },
+};
+
+export const resolvers = {
+ CiCatalogResource: {
+ components() {
+ return componentsMockData;
+ },
+ },
+};
diff --git a/app/assets/javascripts/ci/catalog/router/constants.js b/app/assets/javascripts/ci/catalog/router/constants.js
new file mode 100644
index 00000000000..2d9462ef403
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/router/constants.js
@@ -0,0 +1,2 @@
+export const CI_RESOURCES_PAGE_NAME = 'ci_resources';
+export const CI_RESOURCE_DETAILS_PAGE_NAME = 'ci_resources_details';
diff --git a/app/assets/javascripts/ci/catalog/router/index.js b/app/assets/javascripts/ci/catalog/router/index.js
new file mode 100644
index 00000000000..0b2b3dd3aa3
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/router/index.js
@@ -0,0 +1,13 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import { createRoutes } from './routes';
+
+Vue.use(VueRouter);
+
+export const createRouter = (base, listComponent) => {
+ return new VueRouter({
+ base,
+ mode: 'history',
+ routes: createRoutes(listComponent),
+ });
+};
diff --git a/app/assets/javascripts/ci/catalog/router/routes.js b/app/assets/javascripts/ci/catalog/router/routes.js
new file mode 100644
index 00000000000..ccfb0673c83
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/router/routes.js
@@ -0,0 +1,9 @@
+import CiResourceDetailsPage from '../components/pages/ci_resource_details_page.vue';
+import { CI_RESOURCES_PAGE_NAME, CI_RESOURCE_DETAILS_PAGE_NAME } from './constants';
+
+export const createRoutes = (listComponent) => {
+ return [
+ { name: CI_RESOURCES_PAGE_NAME, path: '', component: listComponent },
+ { name: CI_RESOURCE_DETAILS_PAGE_NAME, path: '/:id', component: CiResourceDetailsPage },
+ ];
+};
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue
index a25f871ac92..77af643cbb3 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue
@@ -24,10 +24,6 @@ export default {
type: Array,
required: true,
},
- hasEnvScopeQuery: {
- type: Boolean,
- required: true,
- },
selectedEnvironmentScope: {
type: String,
required: false,
@@ -36,6 +32,7 @@ export default {
},
data() {
return {
+ customEnvScope: null,
isDropdownShown: false,
selectedEnvironment: '',
searchTerm: '',
@@ -45,42 +42,38 @@ export default {
composedCreateButtonLabel() {
return sprintf(__('Create wildcard: %{searchTerm}'), { searchTerm: this.searchTerm });
},
- filteredEnvironments() {
- const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
- return this.environments.filter((environment) => {
- return environment.toLowerCase().includes(lowerCasedSearchTerm);
- });
- },
isDropdownLoading() {
- return this.areEnvironmentsLoading && this.hasEnvScopeQuery && !this.isDropdownShown;
+ return this.areEnvironmentsLoading && !this.isDropdownShown;
},
isDropdownSearching() {
- return this.areEnvironmentsLoading && this.hasEnvScopeQuery && this.isDropdownShown;
+ return this.areEnvironmentsLoading && this.isDropdownShown;
},
searchedEnvironments() {
- // If hasEnvScopeQuery (applies only to projects for now), search query will be fired so this
- // component will already receive filtered environments during the refetch.
- // Otherwise (applies to groups), search the existing list of environments in the frontend
- let filtered = this.hasEnvScopeQuery ? this.environments : this.filteredEnvironments;
+ let filtered = this.environments;
// If there is no search term, make sure to include *
- if (this.hasEnvScopeQuery && !this.searchTerm) {
+ if (!this.searchTerm) {
filtered = uniq([...filtered, '*']);
}
+ // add custom env scope if it matches the search term
+ if (this.customEnvScope && this.customEnvScope.startsWith(this.searchTerm)) {
+ filtered = uniq([...filtered, this.customEnvScope]);
+ }
+
return filtered.sort().map((environment) => ({
value: environment,
text: environment,
}));
},
shouldRenderCreateButton() {
- return this.searchTerm && !this.environments.includes(this.searchTerm);
- },
- shouldRenderDivider() {
return (
- (this.hasEnvScopeQuery || this.shouldRenderCreateButton) && !this.areEnvironmentsLoading
+ this.searchTerm && ![...this.environments, this.customEnvScope].includes(this.searchTerm)
);
},
+ shouldRenderDivider() {
+ return !this.areEnvironmentsLoading;
+ },
environmentScopeLabel() {
return convertEnvironmentScope(this.selectedEnvironmentScope);
},
@@ -89,16 +82,14 @@ export default {
debouncedSearch: debounce(function debouncedSearch(searchTerm) {
const newSearchTerm = searchTerm.trim();
this.searchTerm = newSearchTerm;
- if (this.hasEnvScopeQuery) {
- this.$emit('search-environment-scope', newSearchTerm);
- }
+ this.$emit('search-environment-scope', newSearchTerm);
}, 500),
selectEnvironment(selected) {
this.$emit('select-environment', selected);
this.selectedEnvironment = selected;
},
createEnvironmentScope() {
- this.$emit('create-environment-scope', this.searchTerm);
+ this.customEnvScope = this.searchTerm;
this.selectEnvironment(this.searchTerm);
},
toggleDropdownShown(isShown) {
@@ -129,7 +120,7 @@ export default {
>
<template #footer>
<gl-dropdown-divider v-if="shouldRenderDivider" />
- <div v-if="hasEnvScopeQuery" data-testid="max-envs-notice">
+ <div data-testid="max-envs-notice">
<gl-dropdown-item class="gl-list-style-none" disabled>
<gl-sprintf :message="$options.i18n.maxEnvsNote" class="gl-font-sm">
<template #limit>
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 c609e05bbb7..a32c5f476fb 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
@@ -11,9 +11,11 @@ import {
GlFormTextarea,
GlIcon,
GlLink,
+ GlModal,
+ GlModalDirective,
GlSprintf,
} from '@gitlab/ui';
-import { __, s__ } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import { helpPagePath } from '~/helpers/help_page_helper';
@@ -36,10 +38,11 @@ import { awsTokenList } from './ci_variable_autocomplete_tokens';
const trackingMixin = Tracking.mixin({ label: DRAWER_EVENT_LABEL });
export const i18n = {
- addVariable: s__('CiVariables|Add Variable'),
+ addVariable: s__('CiVariables|Add variable'),
cancel: __('Cancel'),
defaultScope: allEnvironments.text,
- editVariable: s__('CiVariables|Edit Variable'),
+ deleteVariable: s__('CiVariables|Delete variable'),
+ editVariable: s__('CiVariables|Edit variable'),
environments: __('Environments'),
environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
expandedField: s__('CiVariables|Expand variable reference'),
@@ -51,6 +54,7 @@ export const i18n = {
maskedDescription: s__(
'CiVariables|Variable will be masked in job logs. Requires values to meet regular expression requirements.',
),
+ 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.',
@@ -86,8 +90,12 @@ export default {
GlFormTextarea,
GlIcon,
GlLink,
+ GlModal,
GlSprintf,
},
+ directives: {
+ GlModalDirective,
+ },
mixins: [trackingMixin],
inject: ['environmentScopeLink', 'isProtectedByDefault', 'maskableRawRegex', 'maskableRegex'],
props: {
@@ -170,6 +178,9 @@ export default {
modalActionText() {
return this.isEditing ? this.$options.i18n.editVariable : this.$options.i18n.addVariable;
},
+ removeVariableMessage() {
+ return sprintf(this.$options.i18n.modalDeleteMessage, { key: this.variable.key });
+ },
},
watch: {
variable: {
@@ -188,6 +199,13 @@ export default {
close() {
this.$emit('close-form');
},
+ deleteVariable() {
+ this.$emit('delete-variable', this.variable);
+ this.close();
+ },
+ setEnvironmentScope(scope) {
+ this.variable = { ...this.variable, environmentScope: scope };
+ },
getTrackingErrorProperty() {
if (this.isValueEmpty) {
return null;
@@ -225,164 +243,206 @@ export default {
}),
i18n,
variableOptions,
+ deleteModal: {
+ actionPrimary: {
+ text: __('Delete'),
+ attributes: {
+ variant: 'danger',
+ },
+ },
+ actionSecondary: {
+ text: __('Cancel'),
+ attributes: {
+ variant: 'default',
+ },
+ },
+ },
};
</script>
<template>
- <gl-drawer
- open
- data-testid="ci-variable-drawer"
- :header-height="getDrawerHeaderHeight"
- :z-index="$options.DRAWER_Z_INDEX"
- @close="close"
- >
- <template #title>
- <h2 class="gl-m-0">{{ modalActionText }}</h2>
- </template>
- <gl-form-group
- :label="$options.i18n.type"
- label-for="ci-variable-type"
- class="gl-border-none"
- :class="{
- 'gl-mb-n5': !hideEnvironmentScope,
- 'gl-mb-n1': hideEnvironmentScope,
- }"
+ <div>
+ <gl-drawer
+ open
+ data-testid="ci-variable-drawer"
+ :header-height="getDrawerHeaderHeight"
+ :z-index="$options.DRAWER_Z_INDEX"
+ @close="close"
>
- <gl-form-select
- id="ci-variable-type"
- v-model="variable.variableType"
- :options="$options.variableOptions"
- />
- </gl-form-group>
- <gl-form-group
- v-if="!hideEnvironmentScope"
- class="gl-border-none gl-mb-n5"
- label-for="ci-variable-env"
- data-testid="environment-scope"
- >
- <template #label>
- <div class="gl-display-flex gl-align-items-center">
- <span class="gl-mr-2">
- {{ $options.i18n.environments }}
- </span>
- <gl-link
- class="gl-display-flex"
- :title="$options.i18n.environmentScopeLinkTitle"
- :href="environmentScopeLink"
- target="_blank"
- data-testid="environment-scope-link"
- >
- <gl-icon name="question-o" :size="14" />
- </gl-link>
- </div>
+ <template #title>
+ <h2 class="gl-m-0">{{ modalActionText }}</h2>
</template>
- <ci-environments-dropdown
- v-if="areScopedVariablesAvailable"
- class="gl-mb-5"
- has-env-scope-query
- :are-environments-loading="areEnvironmentsLoading"
- :environments="environments"
- :selected-environment-scope="variable.environmentScope"
- />
- <gl-form-input
- v-else
- :value="$options.i18n.defaultScope"
- class="gl-w-full gl-mb-5"
- readonly
+ <gl-form-group
+ :label="$options.i18n.type"
+ label-for="ci-variable-type"
+ class="gl-border-none"
+ :class="{
+ 'gl-mb-n5': !hideEnvironmentScope,
+ 'gl-mb-n1': hideEnvironmentScope,
+ }"
+ >
+ <gl-form-select
+ id="ci-variable-type"
+ v-model="variable.variableType"
+ :options="$options.variableOptions"
+ />
+ </gl-form-group>
+ <gl-form-group
+ v-if="!hideEnvironmentScope"
+ class="gl-border-none gl-mb-n5"
+ label-for="ci-variable-env"
+ data-testid="environment-scope"
+ >
+ <template #label>
+ <div class="gl-display-flex gl-align-items-center">
+ <span class="gl-mr-2">
+ {{ $options.i18n.environments }}
+ </span>
+ <gl-link
+ class="gl-display-flex"
+ :title="$options.i18n.environmentScopeLinkTitle"
+ :href="environmentScopeLink"
+ target="_blank"
+ data-testid="environment-scope-link"
+ >
+ <gl-icon name="question-o" :size="14" />
+ </gl-link>
+ </div>
+ </template>
+ <ci-environments-dropdown
+ v-if="areScopedVariablesAvailable"
+ class="gl-mb-5"
+ :are-environments-loading="areEnvironmentsLoading"
+ :environments="environments"
+ :selected-environment-scope="variable.environmentScope"
+ @select-environment="setEnvironmentScope"
+ @search-environment-scope="$emit('search-environment-scope', $event)"
+ />
+ <gl-form-input
+ v-else
+ :value="$options.i18n.defaultScope"
+ class="gl-w-full gl-mb-5"
+ readonly
+ />
+ </gl-form-group>
+ <gl-form-group class="gl-border-none gl-mb-n8">
+ <template #label>
+ <div class="gl-display-flex gl-align-items-center gl-mb-n3">
+ <span class="gl-mr-2">
+ {{ $options.i18n.flags }}
+ </span>
+ <gl-link
+ class="gl-display-flex"
+ :title="$options.i18n.flagsLinkTitle"
+ :href="$options.flagLink"
+ target="_blank"
+ >
+ <gl-icon name="question-o" :size="14" />
+ </gl-link>
+ </div>
+ </template>
+ <gl-form-checkbox v-model="variable.protected" data-testid="ci-variable-protected-checkbox">
+ {{ $options.i18n.protectedField }}
+ <p class="gl-text-secondary">
+ {{ $options.i18n.protectedDescription }}
+ </p>
+ </gl-form-checkbox>
+ <gl-form-checkbox v-model="variable.masked" data-testid="ci-variable-masked-checkbox">
+ {{ $options.i18n.maskedField }}
+ <p class="gl-text-secondary">{{ $options.i18n.maskedDescription }}</p>
+ </gl-form-checkbox>
+ <gl-form-checkbox
+ data-testid="ci-variable-expanded-checkbox"
+ :checked="isExpanded"
+ @change="setRaw"
+ >
+ {{ $options.i18n.expandedField }}
+ <p class="gl-text-secondary">
+ <gl-sprintf :message="$options.i18n.expandedDescription" class="gl-text-secondary">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ </gl-form-checkbox>
+ </gl-form-group>
+ <gl-form-combobox
+ v-model="variable.key"
+ :token-list="$options.awsTokenList"
+ :label-text="$options.i18n.key"
+ class="gl-border-none gl-pb-0! gl-mb-n5"
+ data-testid="ci-variable-key"
+ data-qa-selector="ci_variable_key_field"
/>
- </gl-form-group>
- <gl-form-group class="gl-border-none gl-mb-n8">
- <template #label>
- <div class="gl-display-flex gl-align-items-center gl-mb-n3">
- <span class="gl-mr-2">
- {{ $options.i18n.flags }}
- </span>
- <gl-link
- class="gl-display-flex"
- :title="$options.i18n.flagsLinkTitle"
- :href="$options.flagLink"
- target="_blank"
- >
- <gl-icon name="question-o" :size="14" />
- </gl-link>
- </div>
- </template>
- <gl-form-checkbox v-model="variable.protected" data-testid="ci-variable-protected-checkbox">
- {{ $options.i18n.protectedField }}
- <p class="gl-text-secondary">
- {{ $options.i18n.protectedDescription }}
- </p>
- </gl-form-checkbox>
- <gl-form-checkbox v-model="variable.masked" data-testid="ci-variable-masked-checkbox">
- {{ $options.i18n.maskedField }}
- <p class="gl-text-secondary">{{ $options.i18n.maskedDescription }}</p>
- </gl-form-checkbox>
- <gl-form-checkbox
- data-testid="ci-variable-expanded-checkbox"
- :checked="isExpanded"
- @change="setRaw"
+ <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"
+ :state="isValueValid"
>
- {{ $options.i18n.expandedField }}
- <p class="gl-text-secondary">
- <gl-sprintf :message="$options.i18n.expandedDescription" class="gl-text-secondary">
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- </gl-sprintf>
+ <gl-form-textarea
+ id="ci-variable-value"
+ v-model="variable.value"
+ class="gl-border-none gl-font-monospace!"
+ rows="3"
+ max-rows="10"
+ data-testid="ci-variable-value"
+ data-qa-selector="ci_variable_value_field"
+ spellcheck="false"
+ />
+ <p
+ v-if="variable.raw"
+ class="gl-mt-2 gl-mb-0 text-secondary"
+ data-testid="raw-variable-tip"
+ >
+ {{ $options.i18n.valueFeedback.rawHelpText }}
</p>
- </gl-form-checkbox>
- </gl-form-group>
- <gl-form-combobox
- v-model="variable.key"
- :token-list="$options.awsTokenList"
- :label-text="$options.i18n.key"
- class="gl-border-none gl-pb-0! gl-mb-n5"
- data-testid="ci-variable-key"
- data-qa-selector="ci_variable_key_field"
- />
- <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"
- :state="isValueValid"
- >
- <gl-form-textarea
- id="ci-variable-value"
- v-model="variable.value"
- class="gl-border-none gl-font-monospace!"
- rows="3"
- max-rows="10"
- data-testid="ci-variable-value"
- data-qa-selector="ci_variable_value_field"
- spellcheck="false"
- />
- <p v-if="variable.raw" class="gl-mt-2 gl-mb-0 text-secondary" data-testid="raw-variable-tip">
- {{ $options.i18n.valueFeedback.rawHelpText }}
- </p>
- </gl-form-group>
- <gl-alert
- v-if="hasVariableReference"
- :title="$options.i18n.variableReferenceTitle"
- :dismissible="false"
- variant="warning"
- class="gl-mx-4 gl-pl-9! gl-border-bottom-0"
- data-testid="has-variable-reference-alert"
+ </gl-form-group>
+ <gl-alert
+ v-if="hasVariableReference"
+ :title="$options.i18n.variableReferenceTitle"
+ :dismissible="false"
+ variant="warning"
+ class="gl-mx-4 gl-pl-9! gl-border-bottom-0"
+ data-testid="has-variable-reference-alert"
+ >
+ {{ $options.i18n.variableReferenceDescription }}
+ </gl-alert>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button category="secondary" class="gl-mr-3" data-testid="cancel-button" @click="close"
+ >{{ $options.i18n.cancel }}
+ </gl-button>
+ <gl-button
+ v-if="isEditing"
+ v-gl-modal-directive="`delete-variable-${variable.key}`"
+ variant="danger"
+ category="secondary"
+ class="gl-mr-3"
+ data-testid="ci-variable-delete-btn"
+ >{{ $options.i18n.deleteVariable }}</gl-button
+ >
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :disabled="!canSubmit"
+ data-testid="ci-variable-confirm-btn"
+ data-qa-selector="ci_variable_save_button"
+ @click="submit"
+ >{{ modalActionText }}
+ </gl-button>
+ </div>
+ </gl-drawer>
+ <gl-modal
+ ref="modal"
+ :modal-id="`delete-variable-${variable.key}`"
+ :title="$options.i18n.deleteVariable"
+ :action-primary="$options.deleteModal.actionPrimary"
+ :action-secondary="$options.deleteModal.actionSecondary"
+ data-testid="ci-variable-drawer-confirm-delete-modal"
+ @primary="deleteVariable"
>
- {{ $options.i18n.variableReferenceDescription }}
- </gl-alert>
- <div class="gl-display-flex gl-justify-content-end">
- <gl-button category="secondary" class="gl-mr-3" data-testid="cancel-button" @click="close"
- >{{ $options.i18n.cancel }}
- </gl-button>
- <gl-button
- category="primary"
- variant="confirm"
- :disabled="!canSubmit"
- data-testid="ci-variable-confirm-btn"
- @click="submit"
- >{{ modalActionText }}
- </gl-button>
- </div>
- </gl-drawer>
+ {{ removeVariableMessage }}
+ </gl-modal>
+ </div>
</template>
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
index 86c0f34215e..cc664d76267 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
@@ -38,7 +38,6 @@ import {
VARIABLE_ACTIONS,
variableOptions,
} from '../constants';
-import { createJoinedEnvironments } from '../utils';
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
@@ -90,10 +89,6 @@ export default {
required: false,
default: false,
},
- hasEnvScopeQuery: {
- type: Boolean,
- required: true,
- },
mode: {
type: String,
required: true,
@@ -147,13 +142,6 @@ export default {
isTipVisible() {
return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
},
- environmentsList() {
- if (this.hasEnvScopeQuery) {
- return this.environments;
- }
-
- return createJoinedEnvironments(this.variables, this.environments, this.newEnvironments);
- },
maskedFeedback() {
return this.displayMaskedError
? __('This variable value does not meet the masking requirements.')
@@ -211,9 +199,6 @@ export default {
addVariable() {
this.$emit('add-variable', this.variable);
},
- createEnvironmentScope(env) {
- this.newEnvironments.push(env);
- },
deleteVariable() {
this.$emit('delete-variable', this.variable);
},
@@ -407,11 +392,9 @@ export default {
<ci-environments-dropdown
v-if="areScopedVariablesAvailable"
:are-environments-loading="areEnvironmentsLoading"
- :has-env-scope-query="hasEnvScopeQuery"
:selected-environment-scope="variable.environmentScope"
- :environments="environmentsList"
+ :environments="environments"
@select-environment="setEnvironmentScope"
- @create-environment-scope="createEnvironmentScope"
@search-environment-scope="$emit('search-environment-scope', $event)"
/>
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
index 482f6da5617..f2d81b3f271 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
@@ -37,10 +37,6 @@ export default {
required: false,
default: false,
},
- hasEnvScopeQuery: {
- type: Boolean,
- required: true,
- },
isLoading: {
type: Boolean,
required: false,
@@ -125,7 +121,6 @@ export default {
:are-environments-loading="areEnvironmentsLoading"
:are-scoped-variables-available="areScopedVariablesAvailable"
:environments="environments"
- :has-env-scope-query="hasEnvScopeQuery"
:hide-environment-scope="hideEnvironmentScope"
:variables="variables"
:mode="mode"
@@ -144,8 +139,11 @@ export default {
:hide-environment-scope="hideEnvironmentScope"
:selected-variable="selectedVariable"
:mode="mode"
- v-on="$listeners"
+ @add-variable="addVariable"
+ @delete-variable="deleteVariable"
@close-form="closeForm"
+ @update-variable="updateVariable"
+ @search-environment-scope="$emit('search-environment-scope', $event)"
/>
</div>
</div>
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
index 3d5ed327dc7..011a424b6c2 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
@@ -2,7 +2,7 @@
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { reportMessageToSentry } from '~/ci/utils';
+import { reportToSentry } from '~/ci/utils';
import { mapEnvironmentNames } from '../utils';
import {
ADD_MUTATION_ACTION,
@@ -140,7 +140,7 @@ export default {
this.loadingCounter += 1;
} else {
createAlert({ message: this.$options.tooManyCallsError });
- reportMessageToSentry(this.componentName, this.$options.tooManyCallsError, {});
+ reportToSentry(this.componentName, new Error(this.$options.tooManyCallsError));
}
}
},
@@ -285,7 +285,6 @@ export default {
:are-scoped-variables-available="areScopedVariablesAvailable"
:entity="entity"
:environments="environments"
- :has-env-scope-query="hasEnvScopeQuery"
:hide-environment-scope="hideEnvironmentScope"
:is-loading="isLoading"
:max-variable-limit="maxVariableLimit"
diff --git a/app/assets/javascripts/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql
index a28ca4eebc9..f243a1cb30b 100644
--- a/app/assets/javascripts/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql
@@ -1,5 +1,4 @@
fragment BaseCiVariable on CiVariable {
- __typename
id
key
value
diff --git a/app/assets/javascripts/ci/ci_variable_list/utils.js b/app/assets/javascripts/ci/ci_variable_list/utils.js
index 1faa97a5f73..a7e020206ea 100644
--- a/app/assets/javascripts/ci/ci_variable_list/utils.js
+++ b/app/assets/javascripts/ci/ci_variable_list/utils.js
@@ -1,29 +1,6 @@
-import { uniq } from 'lodash';
import { allEnvironments } from './constants';
/**
- * This function takes a list of variable, environments and
- * new environments added through the scope dropdown
- * and create a new Array that concatenate the environment list
- * with the environment scopes find in the variable list. This is
- * useful for variable settings so that we can render a list of all
- * environment scopes available based on the list of envs, the ones the user
- * added explictly and what is found under each variable.
- * @param {Array} variables
- * @param {Array} environments
- * @returns {Array} - Array of environments
- */
-
-export const createJoinedEnvironments = (
- variables = [],
- environments = [],
- newEnvironments = [],
-) => {
- const scopesFromVariables = variables.map((variable) => variable.environmentScope);
- return uniq([...environments, ...newEnvironments, ...scopesFromVariables]).sort();
-};
-
-/**
* This function job is to convert the * wildcard to text when applicable
* in the UI. It uses a constants to compare the incoming value to that
* of the * and then apply the corresponding label if applicable. If there
diff --git a/app/assets/javascripts/ci/common/pipelines_table.vue b/app/assets/javascripts/ci/common/pipelines_table.vue
index 807128d2341..13b5120654a 100644
--- a/app/assets/javascripts/ci/common/pipelines_table.vue
+++ b/app/assets/javascripts/ci/common/pipelines_table.vue
@@ -3,39 +3,52 @@ import { GlTableLite, GlTooltipDirective } from '@gitlab/ui';
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { TRACKING_CATEGORIES } from '~/ci/constants';
+import { PIPELINE_ID_KEY, PIPELINE_IID_KEY, TRACKING_CATEGORIES } from '~/ci/constants';
import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
import PipelineFailedJobsWidget from '~/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue';
-import eventHub from '~/ci/event_hub';
import PipelineOperations from '../pipelines_page/components/pipeline_operations.vue';
-import PipelineStopModal from '../pipelines_page/components/pipeline_stop_modal.vue';
import PipelineTriggerer from '../pipelines_page/components/pipeline_triggerer.vue';
import PipelineUrl from '../pipelines_page/components/pipeline_url.vue';
-import PipelinesStatusBadge from '../pipelines_page/components/pipelines_status_badge.vue';
+import PipelineStatusBadge from '../pipelines_page/components/pipeline_status_badge.vue';
const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!';
const DEFAULT_TH_CLASSES =
'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!';
+/**
+ * Pipelines Table
+ *
+ * Presentational component of a table of pipelines. This component does not
+ * fetch the list of pipelines and instead expects it as a prop.
+ * GraphQL actions for pipelines, such as retrying, canceling, etc.
+ * are handled within this component.
+ *
+ * Use this `legacy_pipelines_table_wrapper` if you need a fully functional REST component.
+ *
+ * IMPORTANT: When using this component, make sure to handle the following events:
+ * 1- @refresh-pipeline-table
+ * 2- @cancel-pipeline
+ * 3- @retry-pipeline
+ *
+ */
+
export default {
components: {
GlTableLite,
LegacyPipelineMiniGraph,
PipelineFailedJobsWidget,
PipelineOperations,
- PipelinesStatusBadge,
- PipelineStopModal,
+ PipelineStatusBadge,
PipelineTriggerer,
PipelineUrl,
},
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [Tracking.mixin(), glFeatureFlagMixin()],
+ mixins: [Tracking.mixin()],
inject: {
- withFailedJobsDetails: {
+ useFailedJobsWidget: {
default: false,
},
},
@@ -44,37 +57,21 @@ export default {
type: Array,
required: true,
},
- pipelineScheduleUrl: {
- type: String,
- required: false,
- default: '',
- },
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
- viewType: {
+ pipelineIdType: {
type: String,
- required: true,
- },
- pipelineKeyOption: {
- type: Object,
- required: true,
+ required: false,
+ default: PIPELINE_ID_KEY,
+ validator(value) {
+ return value === PIPELINE_IID_KEY || value === PIPELINE_ID_KEY;
+ },
},
},
- data() {
- return {
- pipelineId: 0,
- pipeline: {},
- endpoint: '',
- cancelingPipeline: null,
- };
- },
computed: {
- showFailedJobsWidget() {
- return this.glFeatures.ciJobFailuresInMr;
- },
tableFields() {
return [
{
@@ -119,10 +116,10 @@ export default {
];
},
tdClasses() {
- return this.withFailedJobsDetails ? 'gl-pb-0! gl-border-none!' : 'pl-p-5!';
+ return this.useFailedJobsWidget ? 'gl-pb-0! gl-border-none!' : 'pl-p-5!';
},
pipelinesWithDetails() {
- if (this.withFailedJobsDetails) {
+ if (this.useFailedJobsWidget) {
return this.pipelines.map((p) => {
return { ...p, _showDetails: true };
});
@@ -131,17 +128,6 @@ export default {
return this.pipelines;
},
},
- watch: {
- pipelines() {
- this.cancelingPipeline = null;
- },
- },
- created() {
- eventHub.$on('openConfirmationModal', this.setModalData);
- },
- beforeDestroy() {
- eventHub.$off('openConfirmationModal', this.setModalData);
- },
methods: {
getDownstreamPipelines(pipeline) {
const downstream = pipeline.triggered;
@@ -151,16 +137,19 @@ export default {
return cleanLeadingSeparator(item.project.full_path);
},
failedJobsCount(pipeline) {
- return pipeline?.failed_builds?.length || 0;
+ // Remove `pipeline?.failed_builds?.length` when we remove `ci_fix_performance_pipelines_json_endpoint`.
+ return pipeline?.failed_builds_count || pipeline?.failed_builds?.length || 0;
},
- setModalData(data) {
- this.pipelineId = data.pipeline.id;
- this.pipeline = data.pipeline;
- this.endpoint = data.endpoint;
+ onRefreshPipelinesTable() {
+ this.$emit('refresh-pipelines-table');
},
- onSubmit() {
- eventHub.$emit('postAction', this.endpoint);
- this.cancelingPipeline = this.pipelineId;
+ onRetryPipeline(pipeline) {
+ // This emit is only used by the `legacy_pipelines_table_wrapper`.
+ this.$emit('retry-pipeline', pipeline);
+ },
+ onCancelPipeline(pipeline) {
+ // This emit is only used by the `legacy_pipelines_table_wrapper`.
+ this.$emit('cancel-pipeline', pipeline);
},
trackPipelineMiniGraph() {
this.track('click_minigraph', { label: TRACKING_CATEGORIES.table });
@@ -168,7 +157,6 @@ export default {
},
TBODY_TR_ATTR: {
'data-testid': 'pipeline-table-row',
- 'data-qa-selector': 'pipeline_row_container',
},
};
</script>
@@ -191,14 +179,13 @@ export default {
</template>
<template #cell(status)="{ item }">
- <pipelines-status-badge :pipeline="item" :view-type="viewType" />
+ <pipeline-status-badge :pipeline="item" />
</template>
<template #cell(pipeline)="{ item }">
<pipeline-url
:pipeline="item"
- :pipeline-schedule-url="pipelineScheduleUrl"
- :pipeline-key="pipelineKeyOption.value"
+ :pipeline-id-type="pipelineIdType"
ref-color="gl-text-black-normal"
/>
</template>
@@ -219,12 +206,17 @@ export default {
</template>
<template #cell(actions)="{ item }">
- <pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" />
+ <pipeline-operations
+ :pipeline="item"
+ @cancel-pipeline="onCancelPipeline"
+ @refresh-pipelines-table="onRefreshPipelinesTable"
+ @retry-pipeline="onRetryPipeline"
+ />
</template>
<template #row-details="{ item }">
<pipeline-failed-jobs-widget
- v-if="showFailedJobsWidget"
+ v-if="useFailedJobsWidget"
:failed-jobs-count="failedJobsCount(item)"
:is-pipeline-active="item.active"
:pipeline-iid="item.iid"
@@ -234,7 +226,5 @@ export default {
/>
</template>
</gl-table-lite>
-
- <pipeline-stop-modal :pipeline="pipeline" @submit="onSubmit" />
</div>
</template>
diff --git a/app/assets/javascripts/ci/common/private/job_action_component.vue b/app/assets/javascripts/ci/common/private/job_action_component.vue
index f649750ce8a..b0fa724d450 100644
--- a/app/assets/javascripts/ci/common/private/job_action_component.vue
+++ b/app/assets/javascripts/ci/common/private/job_action_component.vue
@@ -120,7 +120,7 @@ export default {
:class="cssClass"
:disabled="isDisabled"
class="js-ci-action gl-ci-action-icon-container ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center"
- data-testid="ci-action-component"
+ data-testid="ci-action-button"
@click.stop="onClickAction"
>
<div
diff --git a/app/assets/javascripts/ci/constants.js b/app/assets/javascripts/ci/constants.js
index 93c2504dd5d..5b60528f521 100644
--- a/app/assets/javascripts/ci/constants.js
+++ b/app/assets/javascripts/ci/constants.js
@@ -24,19 +24,8 @@ export const SUCCESS_STATUS = 'SUCCESS';
export const PASSED_STATUS = 'passed';
export const MANUAL_STATUS = 'manual';
-// Constants for the ID and IID selection dropdown
-export const PipelineKeyOptions = [
- {
- text: __('Show Pipeline ID'),
- label: __('Pipeline ID'),
- value: 'id',
- },
- {
- text: __('Show Pipeline IID'),
- label: __('Pipeline IID'),
- value: 'iid',
- },
-];
+export const PIPELINE_ID_KEY = 'id';
+export const PIPELINE_IID_KEY = 'iid';
export const RAW_TEXT_WARNING = s__(
'Pipeline|Raw text search is not currently supported. Please use the available search tokens.',
diff --git a/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue b/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue
index f02d59af1d9..0b079ccb64f 100644
--- a/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue
+++ b/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue
@@ -3,7 +3,7 @@ import { produce } from 'immer';
import { s__ } from '~/locale';
import { createAlert } from '~/alert';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { reportMessageToSentry } from '~/ci/utils';
+import { reportToSentry } from '~/ci/utils';
import CiVariableTable from '~/ci/ci_variable_list/components/ci_variable_table.vue';
import getInheritedCiVariables from '../graphql/queries/inherited_ci_variables.query.graphql';
@@ -51,7 +51,7 @@ export default {
this.loadingCounter += 1;
} else {
createAlert({ message: this.$options.i18n.tooManyCallsError });
- reportMessageToSentry(this.$options.name, this.$options.i18n.tooManyCallsError, {});
+ reportToSentry(this.$options.name, new Error(this.$options.i18n.tooManyCallsError));
}
},
error() {
diff --git a/app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql b/app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql
index b25768632e1..9fac461a47d 100644
--- a/app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql
+++ b/app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql
@@ -8,7 +8,6 @@ query getInheritedCiVariables($after: String, $first: Int, $fullPath: ID!) {
...PageInfo
}
nodes {
- __typename
id
key
variableType
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 13f3eebd447..00d15f87064 100644
--- a/app/assets/javascripts/ci/job_details/components/job_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/job_header.vue
@@ -89,18 +89,36 @@ export default {
<template>
<header
- class="page-content-header gl-md-display-flex gl-min-h-7"
+ class="page-content-header gl-md-display-flex gl-flex-wrap gl-min-h-7 gl-pb-2! gl-w-full"
data-testid="job-header-content"
>
- <section class="header-main-content gl-mr-3">
- <ci-badge-link class="gl-mr-3" :status="status" />
+ <div
+ v-if="name"
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-w-full"
+ >
+ <h1 class="gl-font-size-h-display gl-my-0 gl-display-inline-block" data-testid="job-name">
+ {{ name }}
+ </h1>
- <strong data-testid="job-name">{{ name }}</strong>
+ <div class="gl-display-flex gl-align-self-start gl-mt-n2">
+ <div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right">
+ <gl-button
+ :aria-label="__('Toggle sidebar')"
+ category="secondary"
+ class="gl-lg-display-none gl-ml-2"
+ icon="chevron-double-lg-left"
+ @click="onClickSidebarButton"
+ />
+ </div>
+ </div>
+ </div>
+ <section class="header-main-content gl-display-flex gl-align-items-center gl-mr-3">
+ <ci-badge-link class="gl-mr-3" :status="status" />
- <template v-if="shouldRenderTriggeredLabel">{{ __('started') }}</template>
- <template v-else>{{ __('created') }}</template>
+ <template v-if="shouldRenderTriggeredLabel">{{ __('Started') }}</template>
+ <template v-else>{{ __('Created') }}</template>
- <timeago-tooltip :time="time" />
+ <timeago-tooltip :time="time" class="gl-mx-2" />
{{ __('by') }}
@@ -133,16 +151,5 @@ export default {
</gl-avatar-link>
</template>
</section>
-
- <!-- eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots -->
- <section v-if="$slots.default" data-testid="job-header-action-buttons" class="gl-display-flex">
- <slot></slot>
- </section>
- <gl-button
- class="gl-md-display-none gl-ml-auto gl-align-self-start js-sidebar-build-toggle"
- icon="chevron-double-lg-left"
- :aria-label="__('Toggle sidebar')"
- @click="onClickSidebarButton"
- />
</header>
</template>
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 419efcba46d..4a30878bec5 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
@@ -146,7 +146,7 @@ export default {
// BE returns zero based index, we need to add one to match the line numbers in the DOM
const firstSearchResult = `#L${this.searchResults[0].lineNumber + 1}`;
- const logLine = document.querySelector(`.log-line ${firstSearchResult}`);
+ const logLine = document.querySelector(`.js-log-line ${firstSearchResult}`);
if (logLine) {
setTimeout(() => scrollToElement(logLine));
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 fa4a12b3dd3..416f75372f9 100644
--- a/app/assets/javascripts/ci/job_details/components/log/line.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line.vue
@@ -56,7 +56,7 @@ export default {
if (window.location.hash) {
const hash = getLocationHash();
- const lineToMatch = `L${line.lineNumber + 1}`;
+ const lineToMatch = `L${line.lineNumber}`;
if (hash === lineToMatch) {
applyHashHighlight = true;
@@ -66,7 +66,11 @@ export default {
return h(
'div',
{
- class: ['js-line', 'log-line', { 'gl-bg-gray-700': isHighlighted || applyHashHighlight }],
+ class: [
+ 'js-log-line',
+ 'log-line',
+ { 'gl-bg-gray-700': isHighlighted || applyHashHighlight },
+ ],
},
[
h(LineNumber, {
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 e647ab4ac0b..658a94e6af4 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
@@ -46,7 +46,7 @@ export default {
},
mounted() {
const hash = getLocationHash();
- const lineToMatch = `L${this.line.lineNumber + 1}`;
+ const lineToMatch = `L${this.line.lineNumber}`;
if (hash === lineToMatch) {
this.applyHashHighlight = true;
@@ -62,7 +62,7 @@ export default {
<template>
<div
- class="log-line collapsible-line d-flex justify-content-between ws-normal gl-align-items-flex-start gl-relative"
+ class="js-log-line log-line collapsible-line d-flex justify-content-between ws-normal gl-align-items-flex-start gl-relative"
:class="{ 'gl-bg-gray-700': isHighlighted || applyHashHighlight }"
role="button"
@click="handleOnClick"
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 7ca9154d2fe..30b4c80f3fa 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
@@ -14,8 +14,7 @@ export default {
render(h, { props }) {
const { lineNumber, path } = props;
- const parsedLineNumber = lineNumber + 1;
- const lineId = `L${parsedLineNumber}`;
+ const lineId = `L${lineNumber}`;
const lineHref = `${path}#${lineId}`;
return h(
@@ -27,7 +26,7 @@ export default {
href: lineHref,
},
},
- parsedLineNumber,
+ lineNumber,
);
},
};
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 1232ffffb57..7f419a249cf 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
@@ -18,7 +18,7 @@ import { JOB_GRAPHQL_ERRORS } from '~/ci/constants';
import { helpPagePath } from '~/helpers/help_page_helper';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { s__ } from '~/locale';
-import { reportMessageToSentry } from '~/ci/utils';
+import { reportToSentry } from '~/ci/utils';
import GetJob from '../graphql/queries/get_job.query.graphql';
import playJobWithVariablesMutation from '../graphql/mutations/job_play_with_variables.mutation.graphql';
import retryJobWithVariablesMutation from '../graphql/mutations/job_retry_with_variables.mutation.graphql';
@@ -57,7 +57,7 @@ export default {
},
error(error) {
createAlert({ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText });
- reportMessageToSentry(this.$options.name, error, {});
+ reportToSentry(this.$options.name, error);
},
},
},
@@ -141,7 +141,7 @@ export default {
}
} catch (error) {
createAlert({ message: JOB_GRAPHQL_ERRORS.jobMutationErrorText });
- reportMessageToSentry(this.$options.name, error, {});
+ reportToSentry(this.$options.name, error);
}
},
async retryJob() {
@@ -157,7 +157,7 @@ export default {
}
} catch (error) {
createAlert({ message: JOB_GRAPHQL_ERRORS.jobMutationErrorText });
- reportMessageToSentry(this.$options.name, error, {});
+ reportToSentry(this.$options.name, error);
}
},
addEmptyVariable() {
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue b/app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue
index 4c81a9bd033..f6d39e8e4ac 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue
@@ -78,7 +78,7 @@ export default {
<span v-if="willExpire" data-testid="artifacts-unlocked-message-content">
{{ $options.i18n.willExpireText }}
</span>
- <timeago-tooltip v-if="artifact.expire_at" :time="artifact.expire_at" />
+ <timeago-tooltip v-if="artifact.expireAt" :time="artifact.expireAt" />
<gl-link
:href="helpUrl"
target="_blank"
@@ -95,23 +95,23 @@ export default {
</p>
<gl-button-group class="gl-display-flex gl-mt-3">
<gl-button
- v-if="artifact.keep_path"
- :href="artifact.keep_path"
+ v-if="artifact.keepPath"
+ :href="artifact.keepPath"
data-method="post"
data-testid="keep-artifacts"
>{{ $options.i18n.keepText }}</gl-button
>
<gl-button
- v-if="artifact.download_path"
- :href="artifact.download_path"
+ v-if="artifact.downloadPath"
+ :href="artifact.downloadPath"
rel="nofollow"
data-testid="download-artifacts"
download
>{{ $options.i18n.downloadText }}</gl-button
>
<gl-button
- v-if="artifact.browse_path"
- :href="artifact.browse_path"
+ v-if="artifact.browsePath"
+ :href="artifact.browsePath"
data-testid="browse-artifacts-button"
>{{ $options.i18n.browseText }}</gl-button
>
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue b/app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue
index 95616a4c706..5e826efbefb 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue
@@ -25,11 +25,7 @@ export default {
<p class="gl-display-flex gl-flex-wrap gl-align-items-baseline gl-gap-2 gl-mb-0">
<span class="gl-display-flex gl-font-weight-bold">{{ __('Commit') }}</span>
- <gl-link
- :href="commit.commit_path"
- class="gl-text-blue-500! gl-font-monospace"
- data-testid="commit-sha"
- >
+ <gl-link :href="commit.commit_path" class="commit-sha-container" data-testid="commit-sha">
{{ commit.short_id }}
</gl-link>
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 7f2f4fc0331..231f45d7ae6 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
@@ -4,6 +4,8 @@ import { isEmpty } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import { forwardDeploymentFailureModalId } from '~/ci/constants';
import { filterAnnotations } from '~/ci/job_details/utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
import ArtifactsBlock from './artifacts_block.vue';
import CommitBlock from './commit_block.vue';
import ExternalLinksBlock from './external_links_block.vue';
@@ -15,6 +17,9 @@ import StagesDropdown from './stages_dropdown.vue';
import TriggerBlock from './trigger_block.vue';
export default {
+ i18n: {
+ toggleSidebar: __('Toggle Sidebar'),
+ },
name: 'JobSidebar',
forwardDeploymentFailureModalId,
components: {
@@ -42,6 +47,9 @@ export default {
// the artifact object will always have a locked property
return Object.keys(this.job.artifact).length > 1;
},
+ artifact() {
+ return convertObjectPropsToCamelCase(this.job.artifact, { deep: true });
+ },
hasExternalLinks() {
return this.externalLinks.length > 0;
},
@@ -79,36 +87,44 @@ export default {
<template>
<aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix">
<div class="sidebar-container">
- <div class="blocks-container gl-p-4">
+ <div class="blocks-container gl-p-4 gl-pt-0">
<sidebar-header
- class="block gl-pb-4! gl-mb-2"
+ class="gl-py-4 gl-border-b gl-border-gray-50"
:rest-job="job"
:job-id="job.id"
@updateVariables="$emit('updateVariables')"
/>
- <job-sidebar-details-container class="block gl-mb-2" />
+ <job-sidebar-details-container class="gl-py-4 gl-border-b gl-border-gray-50" />
<artifacts-block
v-if="hasArtifact"
- class="block gl-mb-2"
- :artifact="job.artifact"
+ class="gl-py-4 gl-border-b gl-border-gray-50"
+ :artifact="artifact"
:help-url="artifactHelpUrl"
/>
<external-links-block
v-if="hasExternalLinks"
- class="block gl-mb-2"
+ class="gl-py-4 gl-border-b gl-border-gray-50"
:external-links="externalLinks"
/>
- <trigger-block v-if="hasTriggers" class="block gl-mb-2" :trigger="job.trigger" />
+ <trigger-block
+ v-if="hasTriggers"
+ class="gl-py-4 gl-border-b gl-border-gray-50"
+ :trigger="job.trigger"
+ />
- <commit-block class="block gl-mb-2" :commit="commit" :merge-request="job.merge_request" />
+ <commit-block
+ class="gl-py-4 gl-border-b gl-border-gray-50"
+ :commit="commit"
+ :merge-request="job.merge_request"
+ />
<stages-dropdown
v-if="job.pipeline"
- class="block gl-mb-2"
+ class="gl-py-4 gl-border-b gl-border-gray-50"
:pipeline="job.pipeline"
:selected-stage="selectedStage"
:stages="stages"
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue
index 5b1bf354fd4..d7726b952de 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue
@@ -39,8 +39,8 @@ export default {
};
</script>
<template>
- <p class="build-sidebar-item gl-mb-2">
- <b v-if="hasTitle" class="gl-display-flex">{{ title }}:</b>
+ <p class="build-sidebar-item gl-line-height-normal gl-display-flex gl-mb-3">
+ <b v-if="hasTitle" class="gl-mr-3">{{ title }}:</b>
<gl-link
v-if="path"
:href="path"
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue
index 77e3ecb9b3c..f757a3bcf00 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue
@@ -6,7 +6,6 @@ import { createAlert } from '~/alert';
import { TYPENAME_COMMIT_STATUS } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { JOB_GRAPHQL_ERRORS, forwardDeploymentFailureModalId, PASSED_STATUS } from '~/ci/constants';
import GetJob from '../../graphql/queries/get_job.query.graphql';
import JobSidebarRetryButton from './job_sidebar_retry_button.vue';
@@ -20,7 +19,6 @@ export default {
eraseLogConfirmText: s__('Job|Are you sure you want to erase this job log and artifacts?'),
newIssue: __('New issue'),
retryJobLabel: s__('Job|Retry'),
- toggleSidebar: __('Toggle Sidebar'),
runAgainJobButtonLabel: s__('Job|Run again'),
},
forwardDeploymentFailureModalId,
@@ -30,7 +28,6 @@ export default {
components: {
GlButton,
JobSidebarRetryButton,
- TooltipOnTruncate,
},
inject: ['projectPath'],
apollo: {
@@ -85,6 +82,15 @@ export default {
retryButtonCategory() {
return this.restJob.status && this.restJob.recoverable ? 'primary' : 'secondary';
},
+ jobHasPath() {
+ return Boolean(
+ this.restJob.erase_path ||
+ this.restJob.new_issue_path ||
+ this.restJob.terminal_path ||
+ this.restJob.retry_path ||
+ this.restJob.cancel_path,
+ );
+ },
},
methods: {
...mapActions(['toggleSidebar']),
@@ -93,73 +99,74 @@ export default {
</script>
<template>
- <div>
- <tooltip-on-truncate :title="job.name" truncate-target="child"
- ><h4 class="gl-mt-0 gl-mb-3 gl-text-truncate" data-testid="job-name">{{ job.name }}</h4>
- </tooltip-on-truncate>
- <div class="gl-display-flex gl-gap-3">
- <gl-button
- v-if="restJob.erase_path"
- v-gl-tooltip.bottom
- :title="$options.i18n.eraseLogButtonLabel"
- :aria-label="$options.i18n.eraseLogButtonLabel"
- :href="restJob.erase_path"
- :data-confirm="$options.i18n.eraseLogConfirmText"
- data-testid="job-log-erase-link"
- data-confirm-btn-variant="danger"
- data-method="post"
- icon="remove"
- />
- <gl-button
- v-if="restJob.new_issue_path"
- v-gl-tooltip.bottom
- :href="restJob.new_issue_path"
- :title="$options.i18n.newIssue"
- :aria-label="$options.i18n.newIssue"
- category="secondary"
- variant="confirm"
- data-testid="job-new-issue"
- icon="issue-new"
- />
- <gl-button
- v-if="restJob.terminal_path"
- v-gl-tooltip.bottom
- :href="restJob.terminal_path"
- :title="$options.i18n.debug"
- :aria-label="$options.i18n.debug"
- target="_blank"
- icon="external-link"
- data-testid="terminal-link"
- />
- <job-sidebar-retry-button
- v-if="canShowJobRetryButton"
- v-gl-tooltip.bottom
- :title="buttonTitle"
- :aria-label="buttonTitle"
- :is-manual-job="isManualJob"
- :category="retryButtonCategory"
- :href="restJob.retry_path"
- :modal-id="$options.forwardDeploymentFailureModalId"
- variant="confirm"
- data-testid="retry-button"
- @updateVariablesClicked="$emit('updateVariables')"
- />
- <gl-button
- v-if="restJob.cancel_path"
- v-gl-tooltip.bottom
- :title="$options.i18n.cancelJobButtonLabel"
- :aria-label="$options.i18n.cancelJobButtonLabel"
- :href="restJob.cancel_path"
- variant="danger"
- icon="cancel"
- data-method="post"
- data-testid="cancel-button"
- rel="nofollow"
- />
+ <div class="gl-py-3!">
+ <div class="gl-display-flex gl-justify-content-space-between gl-gap-3">
+ <div class="gl-display-flex gl-gap-3">
+ <template v-if="jobHasPath">
+ <gl-button
+ v-if="restJob.erase_path"
+ v-gl-tooltip.bottom
+ :title="$options.i18n.eraseLogButtonLabel"
+ :aria-label="$options.i18n.eraseLogButtonLabel"
+ :href="restJob.erase_path"
+ :data-confirm="$options.i18n.eraseLogConfirmText"
+ data-testid="job-log-erase-link"
+ data-confirm-btn-variant="danger"
+ data-method="post"
+ icon="remove"
+ />
+ <gl-button
+ v-if="restJob.new_issue_path"
+ v-gl-tooltip.bottom
+ :href="restJob.new_issue_path"
+ :title="$options.i18n.newIssue"
+ :aria-label="$options.i18n.newIssue"
+ category="secondary"
+ variant="confirm"
+ data-testid="job-new-issue"
+ icon="issue-new"
+ />
+ <gl-button
+ v-if="restJob.terminal_path"
+ v-gl-tooltip.bottom
+ :href="restJob.terminal_path"
+ :title="$options.i18n.debug"
+ :aria-label="$options.i18n.debug"
+ target="_blank"
+ icon="external-link"
+ data-testid="terminal-link"
+ />
+ <job-sidebar-retry-button
+ v-if="canShowJobRetryButton"
+ v-gl-tooltip.bottom
+ :title="buttonTitle"
+ :aria-label="buttonTitle"
+ :is-manual-job="isManualJob"
+ :category="retryButtonCategory"
+ :href="restJob.retry_path"
+ :modal-id="$options.forwardDeploymentFailureModalId"
+ variant="confirm"
+ data-testid="retry-button"
+ @updateVariablesClicked="$emit('updateVariables')"
+ />
+ <gl-button
+ v-if="restJob.cancel_path"
+ v-gl-tooltip.bottom
+ :title="$options.i18n.cancelJobButtonLabel"
+ :aria-label="$options.i18n.cancelJobButtonLabel"
+ :href="restJob.cancel_path"
+ variant="danger"
+ icon="cancel"
+ data-method="post"
+ data-testid="cancel-button"
+ rel="nofollow"
+ />
+ </template>
+ </div>
<gl-button
:aria-label="$options.i18n.toggleSidebar"
category="secondary"
- class="gl-md-display-none gl-ml-2"
+ class="gl-lg-display-none"
icon="chevron-double-lg-right"
@click="toggleSidebar"
/>
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 ebef3ecaa3f..f04987a87b5 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
@@ -44,14 +44,10 @@ export default {
this.job.finished_at ||
this.job.erased_at ||
this.job.queued_duration ||
- this.job.id ||
this.job.runner ||
this.job.coverage,
);
},
- jobId() {
- return this.job?.id ? `#${this.job.id}` : '';
- },
runnerId() {
const { id, short_sha: token, description } = this.job.runner;
@@ -87,7 +83,6 @@ export default {
RUNNER: __('Runner'),
TAGS: __('Tags'),
TIMEOUT: __('Timeout'),
- ID: __('Job ID'),
},
TIMEOUT_HELP_URL: helpPagePath('/ci/pipelines/settings.md', {
anchor: 'set-a-limit-for-how-long-jobs-can-run',
@@ -113,7 +108,6 @@ export default {
data-testid="job-timeout"
:title="$options.i18n.TIMEOUT"
/>
- <detail-row v-if="job.id" :value="jobId" :title="$options.i18n.ID" />
<detail-row
v-if="job.runner"
:value="runnerId"
diff --git a/app/assets/javascripts/ci/job_details/components/stuck_block.vue b/app/assets/javascripts/ci/job_details/components/stuck_block.vue
index 8c73f09daea..b8ff0b032cc 100644
--- a/app/assets/javascripts/ci/job_details/components/stuck_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/stuck_block.vue
@@ -78,7 +78,7 @@ export default {
</template>
</gl-sprintf>
<template v-if="stuckData.showTags">
- <gl-badge v-for="tag in tags" :key="tag" variant="info">
+ <gl-badge v-for="tag in tags" :key="tag" size="sm" variant="info">
{{ tag }}
</gl-badge>
</template>
diff --git a/app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql
index 7fb887b2dd4..3a27a9a62a3 100644
--- a/app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql
@@ -7,5 +7,4 @@ fragment BaseCiJob on CiJob {
...ManualCiVariable
}
}
- __typename
}
diff --git a/app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql
index 0479df7bc4c..e560a2f29b6 100644
--- a/app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql
@@ -1,5 +1,4 @@
fragment ManualCiVariable on CiVariable {
- __typename
id
key
value
diff --git a/app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql b/app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql
index cd66a30ce63..b7c93c2830a 100644
--- a/app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql
@@ -1,6 +1,6 @@
#import "~/ci/job_details/graphql/fragments/ci_job.fragment.graphql"
-mutation retryJobWithVariables($id: CiBuildID!, $variables: [CiVariableInput!]) {
+mutation retryJobWithVariables($id: CiProcessableID!, $variables: [CiVariableInput!]) {
jobRetry(input: { id: $id, variables: $variables }) {
job {
...BaseCiJob
diff --git a/app/assets/javascripts/ci/job_details/index.js b/app/assets/javascripts/ci/job_details/index.js
index 5a1ecf2fff3..20235015ce6 100644
--- a/app/assets/javascripts/ci/job_details/index.js
+++ b/app/assets/javascripts/ci/job_details/index.js
@@ -13,11 +13,11 @@ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
-const initializeJobPage = (element) => {
- const store = createStore();
-
- // Let's start initializing the store (i.e. fetching data) right away
- store.dispatch('init', element.dataset);
+export const initJobDetails = () => {
+ const el = document.getElementById('js-job-page');
+ if (!el) {
+ return null;
+ }
const {
artifactHelpUrl,
@@ -26,27 +26,27 @@ const initializeJobPage = (element) => {
subscriptionsMoreMinutesUrl,
endpoint,
pagePath,
- logState,
buildStatus,
projectPath,
retryOutdatedJobDocsUrl,
aiRootCauseAnalysisAvailable,
- } = element.dataset;
+ } = el.dataset;
+
+ // init store to start fetching log
+ const store = createStore();
+ store.dispatch('init', { endpoint, pagePath });
return new Vue({
- el: element,
+ el,
apolloProvider,
store,
- components: {
- JobApp,
- },
provide: {
projectPath,
retryOutdatedJobDocsUrl,
aiRootCauseAnalysisAvailable: parseBoolean(aiRootCauseAnalysisAvailable),
},
- render(createElement) {
- return createElement('job-app', {
+ render(h) {
+ return h(JobApp, {
props: {
artifactHelpUrl,
deploymentHelpUrl,
@@ -54,7 +54,6 @@ const initializeJobPage = (element) => {
subscriptionsMoreMinutesUrl,
endpoint,
pagePath,
- logState,
buildStatus,
projectPath,
},
@@ -62,8 +61,3 @@ const initializeJobPage = (element) => {
},
});
};
-
-export default () => {
- const jobElement = document.getElementById('js-job-page');
- initializeJobPage(jobElement);
-};
diff --git a/app/assets/javascripts/ci/job_details/job_app.vue b/app/assets/javascripts/ci/job_details/job_app.vue
index 5137ebfeaa8..119f8259be7 100644
--- a/app/assets/javascripts/ci/job_details/job_app.vue
+++ b/app/assets/javascripts/ci/job_details/job_app.vue
@@ -130,7 +130,7 @@ export default {
},
jobName() {
- return sprintf(__('Job %{jobName}'), { jobName: this.job.name });
+ return sprintf(__('%{jobName}'), { jobName: this.job.name });
},
},
watch: {
@@ -195,7 +195,7 @@ export default {
},
updateSidebar() {
const breakpoint = bp.getBreakpointSize();
- if (breakpoint === 'xs' || breakpoint === 'sm') {
+ if (breakpoint === 'xs' || breakpoint === 'sm' || breakpoint === 'md') {
this.hideSidebar();
} else if (!this.isSidebarOpen) {
this.showSidebar();
@@ -224,7 +224,7 @@ export default {
<div class="build-page" data-testid="job-content">
<!-- Header Section -->
<header>
- <div class="build-header top-area">
+ <div class="build-header gl-display-flex">
<job-header
:status="job.status"
:time="headerTime"
@@ -290,11 +290,7 @@ export default {
{{ __('This job is archived. Only the complete pipeline can be retried.') }}
</div>
<!-- job log -->
- <div
- v-if="hasJobLog && !showUpdateVariablesState"
- class="build-log-container gl-relative"
- :class="{ 'gl-mt-3': !job.archived }"
- >
+ <div v-if="hasJobLog && !showUpdateVariablesState" class="build-log-container gl-relative">
<log-top-bar
:class="{
'has-archived-block': job.archived,
@@ -332,18 +328,17 @@ export default {
<!-- EO empty state -->
<!-- EO Body Section -->
+
+ <sidebar
+ :class="{
+ 'right-sidebar-expanded': isSidebarOpen,
+ 'right-sidebar-collapsed': !isSidebarOpen,
+ }"
+ :artifact-help-url="artifactHelpUrl"
+ data-testid="job-sidebar"
+ @updateVariables="onUpdateVariables()"
+ />
</div>
</template>
-
- <sidebar
- v-if="shouldRenderContent"
- :class="{
- 'right-sidebar-expanded': isSidebarOpen,
- 'right-sidebar-collapsed': !isSidebarOpen,
- }"
- :artifact-help-url="artifactHelpUrl"
- data-testid="job-sidebar"
- @updateVariables="onUpdateVariables()"
- />
</div>
</template>
diff --git a/app/assets/javascripts/ci/job_details/store/actions.js b/app/assets/javascripts/ci/job_details/store/actions.js
index 33d83689e61..fa23589f7d6 100644
--- a/app/assets/javascripts/ci/job_details/store/actions.js
+++ b/app/assets/javascripts/ci/job_details/store/actions.js
@@ -15,17 +15,15 @@ import { __ } from '~/locale';
import { reportToSentry } from '~/ci/utils';
import * as types from './mutation_types';
-export const init = ({ dispatch }, { endpoint, logState, pagePath }) => {
- dispatch('setJobEndpoint', endpoint);
+export const init = ({ dispatch }, { endpoint, pagePath }) => {
dispatch('setJobLogOptions', {
- logState,
+ endpoint,
pagePath,
});
return dispatch('fetchJob');
};
-export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint);
export const setJobLogOptions = ({ commit }, options) => commit(types.SET_JOB_LOG_OPTIONS, options);
export const hideSidebar = ({ commit }) => commit(types.HIDE_SIDEBAR);
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 4915a826b84..e125538317d 100644
--- a/app/assets/javascripts/ci/job_details/store/mutation_types.js
+++ b/app/assets/javascripts/ci/job_details/store/mutation_types.js
@@ -1,4 +1,3 @@
-export const SET_JOB_ENDPOINT = 'SET_JOB_ENDPOINT';
export const SET_JOB_LOG_OPTIONS = 'SET_JOB_LOG_OPTIONS';
export const HIDE_SIDEBAR = 'HIDE_SIDEBAR';
diff --git a/app/assets/javascripts/ci/job_details/store/mutations.js b/app/assets/javascripts/ci/job_details/store/mutations.js
index b7d7006ee61..fe6506bf8a5 100644
--- a/app/assets/javascripts/ci/job_details/store/mutations.js
+++ b/app/assets/javascripts/ci/job_details/store/mutations.js
@@ -3,13 +3,9 @@ import * as types from './mutation_types';
import { logLinesParser, updateIncrementalJobLog } from './utils';
export default {
- [types.SET_JOB_ENDPOINT](state, endpoint) {
- state.jobEndpoint = endpoint;
- },
-
[types.SET_JOB_LOG_OPTIONS](state, options = {}) {
state.jobLogEndpoint = options.pagePath;
- state.jobLogState = options.logState;
+ state.jobEndpoint = options.endpoint;
},
[types.HIDE_SIDEBAR](state) {
diff --git a/app/assets/javascripts/ci/job_details/store/utils.js b/app/assets/javascripts/ci/job_details/store/utils.js
index bc76901026d..b18a3fa162d 100644
--- a/app/assets/javascripts/ci/job_details/store/utils.js
+++ b/app/assets/javascripts/ci/job_details/store/utils.js
@@ -19,20 +19,17 @@ export const parseLine = (line = {}, lineNumber) => ({
* @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) {
- return {
- isClosed: false,
- isHeader: true,
- line: parseLine(line, lineNumber),
- lines: [],
- };
+ isClosed = false;
}
return {
- isClosed: parseBoolean(line.section_options?.collapsed),
+ isClosed,
isHeader: true,
line: parseLine(line, lineNumber),
lines: [],
@@ -80,27 +77,28 @@ export const isCollapsibleSection = (acc = [], last = {}, section = {}) =>
section.section === last.line.section;
/**
- * Returns the lineNumber of the last line in
- * a parsed log
+ * Returns the next line number in the parsed log
*
* @param Array acc
* @returns Number
*/
-export const getIncrementalLineNumber = (acc) => {
- let lineNumberValue;
- const lastIndex = acc.length - 1;
- const lastElement = acc[lastIndex];
+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) {
- lineNumberValue = lastElement.line.lineNumber;
- } else if (lastElement.isHeader && nestedLines.length) {
- lineNumberValue = nestedLines[nestedLines.length - 1].lineNumber;
- } else {
- lineNumberValue = lastElement.lineNumber;
+ return lastElement.line.lineNumber + 1;
}
- return lineNumberValue === 0 ? 1 : lineNumberValue + 1;
+ if (lastElement.isHeader && nestedLines.length) {
+ return nestedLines[nestedLines.length - 1].lineNumber + 1;
+ }
+
+ return lastElement.lineNumber + 1;
};
/**
@@ -118,32 +116,29 @@ export const getIncrementalLineNumber = (acc) => {
* @param Array accumulator
* @returns Array parsed log lines
*/
-export const logLinesParser = (lines = [], accumulator = [], hash = '') =>
- lines.reduce(
- (acc, line, index) => {
- const lineNumber = accumulator.length > 0 ? getIncrementalLineNumber(acc) : index;
-
- const last = acc[acc.length - 1];
-
- // 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));
- }
+export const logLinesParser = (lines = [], prevLogLines = [], hash = '') =>
+ lines.reduce((acc, line) => {
+ const lineNumber = getNextLineNumber(acc);
+
+ const last = acc[acc.length - 1];
+
+ // 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));
+ }
- return acc;
- },
- [...accumulator],
- );
+ return acc;
+ }, prevLogLines);
/**
* Finds the repeated offset, removes the old one
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 609f2790869..3ad2582e36b 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
@@ -7,7 +7,7 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
-import { reportMessageToSentry } from '~/ci/utils';
+import { reportToSentry } from '~/ci/utils';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import {
@@ -133,7 +133,7 @@ export default {
variables: { id: this.job.id },
});
if (errors.length > 0) {
- reportMessageToSentry(this.$options.name, errors.join(', '), {});
+ reportToSentry(this.$options.name, new Error(errors.join(', ')));
this.showToastMessage();
} else if (redirect) {
// Retry and Play actions redirect to job detail view
@@ -143,7 +143,7 @@ export default {
eventHub.$emit('jobActionPerformed');
}
} catch (failure) {
- reportMessageToSentry(this.$options.name, failure, {});
+ reportToSentry(this.$options.name, failure);
this.showToastMessage();
}
},
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
index b435eb283fd..fbdfc7c9c6a 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
@@ -35,9 +35,6 @@ export default {
jobRef() {
return this.job?.refName;
},
- jobRefPath() {
- return this.job?.refPath;
- },
jobTags() {
return this.job.tags;
},
@@ -72,61 +69,60 @@ export default {
<template>
<div>
<div class="gl-text-truncate gl-p-3 gl-mt-n3 gl-mx-n3 gl-mb-n2">
- <gl-link
- v-if="canReadJob"
- class="gl-text-blue-600!"
- :href="jobPath"
- data-testid="job-id-link"
- >
- {{ jobId }}
- </gl-link>
-
- <span v-else data-testid="job-id-limited-access">{{ jobId }}</span>
-
<gl-icon
v-if="jobStuck"
v-gl-tooltip="$options.i18n.stuckText"
name="warning"
:size="$options.iconSize"
+ class="gl-mr-2"
data-testid="stuck-icon"
/>
- <div
- class="gl-display-flex gl-text-gray-700 gl-align-items-center gl-lg-justify-content-start gl-justify-content-end gl-mt-2"
+ <gl-link
+ v-if="canReadJob"
+ class="gl-text-blue-600!"
+ :href="jobPath"
+ data-testid="job-id-link"
>
- <div
- v-if="jobRef"
- class="gl-p-2 gl-rounded-base gl-bg-gray-50 gl-max-w-15 gl-text-truncate"
- >
- <gl-icon
- v-if="createdByTag"
- name="label"
- :size="$options.iconSize"
- data-testid="label-icon"
- />
- <gl-icon v-else name="fork" :size="$options.iconSize" data-testid="fork-icon" />
- <gl-link
- class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
- :href="job.refPath"
- data-testid="job-ref"
- >{{ job.refName }}</gl-link
- >
- </div>
+ <span class="gl-text-truncate">
+ <span data-testid="job-name">{{ jobId }}: {{ job.name }}</span>
+ </span>
+ </gl-link>
- <span v-else>{{ __('none') }}</span>
- <div class="gl-ml-2 gl-p-2 gl-rounded-base gl-bg-gray-50">
- <gl-icon class="gl-mx-2" name="commit" :size="$options.iconSize" />
- <gl-link
- class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
- :href="job.commitPath"
- data-testid="job-sha"
- >{{ job.shortSha }}</gl-link
- >
- </div>
+ <span v-else data-testid="job-id-limited-access">{{ jobId }}: {{ job.name }}</span>
+ </div>
+
+ <div
+ class="gl-display-flex gl-text-gray-700 gl-align-items-center gl-lg-justify-content-start gl-justify-content-end gl-mt-1"
+ >
+ <div v-if="jobRef" class="gl-p-2 gl-rounded-base gl-bg-gray-50 gl-max-w-26 gl-text-truncate">
+ <gl-icon
+ v-if="createdByTag"
+ name="label"
+ :size="$options.iconSize"
+ data-testid="label-icon"
+ />
+ <gl-icon v-else name="fork" :size="$options.iconSize" data-testid="fork-icon" />
+ <gl-link
+ class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
+ :href="job.refPath"
+ data-testid="job-ref"
+ >{{ job.refName }}</gl-link
+ >
+ </div>
+ <span v-else>{{ __('none') }}</span>
+ <div class="gl-ml-2 gl-p-2 gl-rounded-base gl-bg-gray-50">
+ <gl-icon class="gl-mx-2" name="commit" :size="$options.iconSize" />
+ <gl-link
+ class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
+ :href="job.commitPath"
+ data-testid="job-sha"
+ >{{ job.shortSha }}</gl-link
+ >
</div>
</div>
- <div>
+ <div class="gl-mt-2">
<gl-badge
v-for="tag in jobTags"
:key="tag"
@@ -136,7 +132,6 @@ export default {
>
{{ tag }}
</gl-badge>
-
<gl-badge
v-if="triggered"
variant="info"
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue
index 18d68ee8a29..945674153c4 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue
@@ -1,8 +1,12 @@
<script>
import { GlAvatar, GlLink } from '@gitlab/ui';
+import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
export default {
+ i18n: {
+ stageLabel: s__('Jobs|Stage'),
+ },
components: {
GlAvatar,
GlLink,
@@ -36,21 +40,22 @@ export default {
<template>
<div>
- <div class="gl-p-3 gl-mt-n3">
- <gl-link
- class="gl-text-truncate gl-ml-n3 gl-text-gray-500!"
- :href="pipelinePath"
- data-testid="pipeline-id"
- >
+ <div class="gl-p-3 gl-mt-n3 gl-mx-n3">
+ <gl-link class="gl-text-truncate" :href="pipelinePath" data-testid="pipeline-id">
{{ pipelineId }}
</gl-link>
+
+ <span class="gl-text-secondary">
+ <span>{{ __('created by') }}</span>
+ <gl-link v-if="showAvatar" :href="userPath" data-testid="pipeline-user-link">
+ <gl-avatar :src="pipelineUserAvatar" :size="16" />
+ </gl-link>
+ <span v-else>{{ __('API') }}</span>
+ </span>
</div>
- <div class="gl-font-sm gl-text-secondary gl-mt-n2">
- <span>{{ __('created by') }}</span>
- <gl-link v-if="showAvatar" :href="userPath" data-testid="pipeline-user-link">
- <gl-avatar :src="pipelineUserAvatar" :size="16" />
- </gl-link>
- <span v-else>{{ __('API') }}</span>
+
+ <div v-if="job.stage" class="gl-text-truncate gl-font-sm gl-text-secondary gl-mt-1">
+ <span data-testid="job-stage-name">{{ $options.i18n.stageLabel }}: {{ job.stage.name }}</span>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
index dbf1dfe7a29..a2b6a430138 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
@@ -1,12 +1,14 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { formatTime } from '~/lib/utils/datetime_utility';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
iconSize: 12,
components: {
+ CiBadgeLink,
GlIcon,
TimeAgoTooltip,
},
@@ -36,17 +38,16 @@ export default {
<template>
<div>
- <div v-if="duration" data-testid="job-duration">
- <gl-icon name="timer" :size="$options.iconSize" data-testid="duration-icon" />
- {{ durationFormatted }}
- </div>
- <div
- v-if="finishedTime"
- :class="{ 'gl-mt-2': hasDurationAndFinishedTime }"
- data-testid="job-finished-time"
- >
- <gl-icon name="calendar" :size="$options.iconSize" data-testid="finished-time-icon" />
- <time-ago-tooltip :time="finishedTime" />
+ <ci-badge-link :status="job.detailedStatus" />
+ <div class="gl-font-sm gl-text-secondary gl-mt-2 gl-ml-3">
+ <div v-if="duration" data-testid="job-duration">
+ <gl-icon name="timer" :size="$options.iconSize" data-testid="duration-icon" />
+ {{ durationFormatted }}
+ </div>
+ <div v-if="finishedTime" data-testid="job-finished-time">
+ <gl-icon name="calendar" :size="$options.iconSize" data-testid="finished-time-icon" />
+ <time-ago-tooltip :time="finishedTime" />
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue b/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue
index 23100a3f3db..d81d19cfd52 100644
--- a/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue
@@ -1,12 +1,11 @@
<script>
import { GlTable } from '@gitlab/ui';
import { s__ } from '~/locale';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import ProjectCell from '~/ci/admin/jobs_table/components/cells/project_cell.vue';
import RunnerCell from '~/ci/admin/jobs_table/components/cells/runner_cell.vue';
-import { DEFAULT_FIELDS } from '../constants';
+import { JOBS_DEFAULT_FIELDS } from '../constants';
import ActionsCell from './job_cells/actions_cell.vue';
-import DurationCell from './job_cells/duration_cell.vue';
+import StatusCell from './job_cells/status_cell.vue';
import JobCell from './job_cells/job_cell.vue';
import PipelineCell from './job_cells/pipeline_cell.vue';
@@ -16,13 +15,12 @@ export default {
},
components: {
ActionsCell,
- CiBadgeLink,
- DurationCell,
- GlTable,
+ StatusCell,
JobCell,
PipelineCell,
ProjectCell,
RunnerCell,
+ GlTable,
},
props: {
jobs: {
@@ -32,7 +30,7 @@ export default {
tableFields: {
type: Array,
required: false,
- default: () => DEFAULT_FIELDS,
+ default: () => JOBS_DEFAULT_FIELDS,
},
admin: {
type: Boolean,
@@ -64,7 +62,7 @@ export default {
</template>
<template #cell(status)="{ item }">
- <ci-badge-link :status="item.detailedStatus" />
+ <status-cell :job="item" />
</template>
<template #cell(job)="{ item }">
@@ -75,28 +73,20 @@ export default {
<pipeline-cell :job="item" />
</template>
- <template v-if="admin" #cell(project)="{ item }">
- <project-cell :job="item" />
- </template>
-
- <template v-if="admin" #cell(runner)="{ item }">
- <runner-cell :job="item" />
- </template>
-
<template #cell(stage)="{ item }">
<div class="gl-text-truncate">
- <span v-if="item.stage" data-testid="job-stage-name">{{ item.stage.name }}</span>
+ <span v-if="item.stage" data-testid="job-stage-name" class="gl-text-secondary">{{
+ item.stage.name
+ }}</span>
</div>
</template>
- <template #cell(name)="{ item }">
- <div class="gl-text-truncate">
- <span data-testid="job-name">{{ item.name }}</span>
- </div>
+ <template v-if="admin" #cell(project)="{ item }">
+ <project-cell :job="item" />
</template>
- <template #cell(duration)="{ item }">
- <duration-cell :job="item" />
+ <template v-if="admin" #cell(runner)="{ item }">
+ <runner-cell :job="item" />
</template>
<template #cell(coverage)="{ item }">
diff --git a/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue b/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue
index d2cd27be034..7effb8fe239 100644
--- a/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue
@@ -29,6 +29,7 @@ export default {
:title="$options.i18n.title"
:description="$options.i18n.description"
:svg-path="emptyStateSvgPath"
+ :svg-height="null"
:primary-button-link="pipelineEditorPath"
:primary-button-text="$options.i18n.buttonText"
data-testid="jobs-empty-state"
diff --git a/app/assets/javascripts/ci/jobs_page/constants.js b/app/assets/javascripts/ci/jobs_page/constants.js
index 1b572e60c58..dec355ddff6 100644
--- a/app/assets/javascripts/ci/jobs_page/constants.js
+++ b/app/assets/javascripts/ci/jobs_page/constants.js
@@ -29,6 +29,7 @@ export const PLAY_JOB_CONFIRMATION_MESSAGE = s__(
export const RUN_JOB_NOW_HEADER_TITLE = s__('DelayedJobs|Run the delayed job now?');
/* Table constants */
+/* There is another field list based on this one in app/assets/javascripts/ci/admin/jobs_table/constants.js */
export const DEFAULT_FIELDS = [
{
key: 'status',
@@ -38,7 +39,7 @@ export const DEFAULT_FIELDS = [
{
key: 'job',
label: __('Job'),
- columnClass: 'gl-w-20p',
+ columnClass: 'gl-w-quarter',
},
{
key: 'pipeline',
@@ -51,16 +52,6 @@ export const DEFAULT_FIELDS = [
columnClass: 'gl-w-10p',
},
{
- key: 'name',
- label: __('Name'),
- columnClass: 'gl-w-15p',
- },
- {
- key: 'duration',
- label: __('Duration'),
- columnClass: 'gl-w-15p',
- },
- {
key: 'coverage',
label: __('Coverage'),
tdClass: 'gl-display-none! gl-lg-display-table-cell!',
@@ -69,8 +60,10 @@ export const DEFAULT_FIELDS = [
{
key: 'actions',
label: '',
+ tdClass: 'gl-text-right',
columnClass: 'gl-w-10p',
},
];
+export const JOBS_DEFAULT_FIELDS = DEFAULT_FIELDS.filter((field) => field.key !== 'stage');
export const JOBS_TAB_FIELDS = DEFAULT_FIELDS.filter((field) => field.key !== 'pipeline');
diff --git a/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql
index 6e51f9a20fa..077c8e31749 100644
--- a/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql
@@ -1,6 +1,6 @@
#import "../fragments/job.fragment.graphql"
-mutation retryJob($id: CiBuildID!) {
+mutation retryJob($id: CiProcessableID!) {
jobRetry(input: { id: $id }) {
job {
...Job
diff --git a/app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql b/app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql
index 022d461dbec..f6de6cde9d0 100644
--- a/app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql
+++ b/app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql
@@ -1,4 +1,4 @@
-mutation retryMrFailedJob($id: CiBuildID!) {
+mutation retryMrFailedJob($id: CiProcessableID!) {
jobRetry(input: { id: $id }) {
errors
}
diff --git a/app/assets/javascripts/ci/pipeline_details/constants.js b/app/assets/javascripts/ci/pipeline_details/constants.js
index bf312e66144..70b758ae6b0 100644
--- a/app/assets/javascripts/ci/pipeline_details/constants.js
+++ b/app/assets/javascripts/ci/pipeline_details/constants.js
@@ -23,8 +23,6 @@ export const PARSE_FAILURE = 'parse_failure';
export const POST_FAILURE = 'post_failure';
export const UNSUPPORTED_DATA = 'unsupported_data';
-export const CHILD_VIEW = 'child';
-
// Pipeline tabs
export const pipelineTabName = 'graph';
diff --git a/app/assets/javascripts/ci/pipeline_details/dag/dag.vue b/app/assets/javascripts/ci/pipeline_details/dag/dag.vue
index 5415340c956..fb8e5d679b7 100644
--- a/app/assets/javascripts/ci/pipeline_details/dag/dag.vue
+++ b/app/assets/javascripts/ci/pipeline_details/dag/dag.vue
@@ -220,6 +220,7 @@ export default {
<gl-empty-state
v-else-if="hasNoDependentJobs"
:svg-path="emptyDagSvgPath"
+ :svg-height="null"
:title="$options.emptyStateTexts.title"
>
<template #description>
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue
index 7538ad87af8..ec8f30e94b4 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue
@@ -65,7 +65,7 @@ export default {
<div
:id="computedJobId"
class="ci-job-dropdown-container dropdown dropright"
- data-qa-selector="job_dropdown_container"
+ data-testid="job-dropdown-container"
>
<button
type="button"
@@ -90,7 +90,7 @@ export default {
<ul
class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown"
- data-qa-selector="jobs_dropdown_menu"
+ data-testid="jobs-dropdown-menu"
>
<li class="scrollable-menu">
<ul>
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 4298052d1c0..bb36ac8b6ab 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 CiBadgeLink from '~/vue_shared/components/ci_badge_link.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';
@@ -58,7 +58,7 @@ export default {
hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500',
components: {
ActionComponent,
- CiIcon,
+ CiBadgeLink,
GlBadge,
GlForm,
GlFormCheckbox,
@@ -312,7 +312,6 @@ export default {
<div
:id="computedJobId"
class="ci-job-component gl-display-flex gl-justify-content-space-between gl-pipeline-job-width"
- data-qa-selector="job_item_container"
>
<component
:is="nameComponent"
@@ -326,12 +325,11 @@ export default {
:href="detailsPath"
class="js-pipeline-graph-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none gl-w-full"
:data-testid="testId"
- data-qa-selector="job_link"
@click="jobItemClick"
@mouseout="hideTooltips"
>
<div class="gl-display-flex gl-align-items-center gl-flex-grow-1">
- <ci-icon :size="24" :status="job.status" class="gl-line-height-0" />
+ <ci-badge-link :status="job.status" size="md" :show-text="false" :use-link="false" />
<div class="gl-pl-3 gl-pr-3 gl-display-flex gl-flex-direction-column gl-pipeline-job-width">
<div class="gl-text-truncate gl-pr-9 gl-line-height-normal">{{ job.name }}</div>
<div
@@ -343,7 +341,13 @@ export default {
</div>
</div>
</div>
- <gl-badge v-if="isBridge" class="gl-mt-3" variant="info" size="sm">
+ <gl-badge
+ v-if="isBridge"
+ class="gl-mt-3"
+ variant="info"
+ size="sm"
+ data-testid="job-bridge-badge"
+ >
{{ $options.i18n.bridgeBadgeText }}
</gl-badge>
</component>
@@ -356,7 +360,6 @@ export default {
class="gl-mr-1"
:should-trigger-click="shouldTriggerActionClick"
:with-confirmation-modal="withConfirmationModal"
- data-qa-selector="job_action_button"
@actionButtonClicked="handleConfirmationModalPreferences"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
@showActionConfirmationModal="showActionConfirmationModal"
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 d6adaf78da4..5960eea5b4f 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
@@ -13,7 +13,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 CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { reportToSentry } from '~/ci/utils';
import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from '../constants';
@@ -22,7 +22,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- CiIcon,
+ CiBadgeLink,
GlBadge,
GlButton,
GlLink,
@@ -233,7 +233,7 @@ export default {
ref="linkedPipeline"
class="gl-h-full gl-display-flex! gl-px-2"
:class="flexDirection"
- data-qa-selector="linked_pipeline_container"
+ data-testid="linked-pipeline-container"
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
>
@@ -242,16 +242,19 @@ export default {
</gl-tooltip>
<div class="gl-bg-white gl-border gl-p-3 gl-rounded-lg gl-w-full" :class="cardClasses">
<div class="gl-display-flex gl-gap-x-3">
- <ci-icon v-if="!pipelineIsLoading" :status="pipelineStatus" :size="24" />
+ <ci-badge-link
+ v-if="!pipelineIsLoading"
+ :status="pipelineStatus"
+ size="md"
+ :show-text="false"
+ :use-link="false"
+ class="gl-align-self-start"
+ />
<div v-else class="gl-pr-3"><gl-loading-icon size="sm" inline /></div>
<div
class="gl-display-flex gl-downstream-pipeline-job-width gl-flex-direction-column gl-line-height-normal"
>
- <span
- class="gl-text-truncate"
- data-testid="downstream-title"
- data-qa-selector="downstream_title_content"
- >
+ <span class="gl-text-truncate" data-testid="downstream-title-content">
{{ downstreamTitle }}
</span>
<div class="gl-text-truncate">
@@ -294,7 +297,6 @@ export default {
:icon="expandedIcon"
:aria-label="expandBtnText"
data-testid="expand-pipeline-button"
- data-qa-selector="expand_linked_pipeline_button"
@mouseover="setExpandBtnActiveState(true)"
@mouseout="setExpandBtnActiveState(false)"
@focus="setExpandBtnActiveState(true)"
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
index 1401bdba5ca..6030adc96ad 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
@@ -179,6 +179,7 @@ export default {
{ 'gl-opacity-3': isFadedOut(group.name) },
'gl-transition-duration-slow gl-transition-timing-function-ease',
]"
+ data-testid="job-item-container"
@pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
@setSkipRetryModal="$emit('setSkipRetryModal')"
/>
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue b/app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue
index bd7325f7925..a6e7a645442 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue
@@ -6,7 +6,7 @@ import { __, s__ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '~/ci/pipeline_details/constants';
import getPipelineQuery from '~/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql';
-import { reportToSentry, reportMessageToSentry } from '~/ci/utils';
+import { reportToSentry } from '~/ci/utils';
import DismissPipelineGraphCallout from './graphql/mutations/dismiss_pipeline_notification.graphql';
import {
ACTION_FAILURE,
@@ -156,17 +156,7 @@ export default {
error(err) {
this.reportFailure({ type: LOAD_FAILURE, skipSentry: true });
- reportMessageToSentry(
- this.$options.name,
- `| type: ${LOAD_FAILURE} , info: ${JSON.stringify(err)}`,
- {
- graphViewType: this.graphViewType,
- graphqlResourceEtag: this.graphqlResourceEtag,
- metricsPath: this.metricsPath,
- projectPath: this.pipelineProjectPath,
- pipelineIid: this.pipelineIid,
- },
- );
+ reportToSentry(this.$options.name, new Error(err));
},
result({ data, error }) {
const stages = data?.project?.pipeline?.stages?.nodes || [];
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 3a6a655bfa6..51a68f6619a 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
@@ -396,18 +396,14 @@ export default {
</div>
</gl-alert>
<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"
- data-qa-selector="pipeline_details_header"
- >
+ <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-else class="gl-mt-0 gl-mb-3" data-testid="pipeline-commit-title">
{{ commitTitle }}
</h3>
<div>
- <ci-badge-link :status="detailedStatus" />
+ <ci-badge-link :status="detailedStatus" class="gl-display-inline-block gl-mb-3" />
<div class="gl-ml-2 gl-mb-3 gl-display-inline-block gl-h-6">
<gl-link
v-if="user"
@@ -423,7 +419,7 @@ export default {
<template #link="{ content }">
<gl-link
:href="commitPath"
- class="gl-bg-blue-50 gl-rounded-base gl-px-2 gl-mx-2"
+ class="commit-sha-container"
data-testid="commit-link"
target="_blank"
>
@@ -431,6 +427,8 @@ export default {
</gl-link>
</template>
</gl-sprintf>
+ </div>
+ <div class="gl-display-inline-block gl-mb-3">
<clipboard-button
:text="shortId"
category="tertiary"
@@ -449,123 +447,127 @@ export default {
</div>
<div v-safe-html="refText" class="gl-mb-3" data-testid="pipeline-ref-text"></div>
<div>
- <gl-badge
- v-if="badges.schedule"
- v-gl-tooltip
- :title="$options.i18n.scheduleBadgeTooltip"
- variant="info"
- size="sm"
- >
- {{ $options.i18n.scheduleBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.child"
- v-gl-tooltip
- :title="$options.i18n.childBadgeTooltip"
- variant="info"
- size="sm"
- >
- <gl-sprintf :message="$options.i18n.childBadgeText">
- <template #link="{ content }">
- <gl-link :href="paths.triggeredByPath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </gl-badge>
- <gl-badge
- v-if="badges.latest"
- v-gl-tooltip
- :title="$options.i18n.latestBadgeTooltip"
- variant="success"
- size="sm"
- >
- {{ $options.i18n.latestBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.mergeTrainPipeline"
- v-gl-tooltip
- :title="$options.i18n.mergeTrainBadgeTooltip"
- variant="info"
- size="sm"
- >
- {{ $options.i18n.mergeTrainBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.invalid"
- v-gl-tooltip
- :title="yamlErrors"
- variant="danger"
- size="sm"
- >
- {{ $options.i18n.invalidBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.failed"
- v-gl-tooltip
- :title="failureReason"
- variant="danger"
- size="sm"
- >
- {{ $options.i18n.failedBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.autoDevops"
- v-gl-tooltip
- :title="$options.i18n.autoDevopsBadgeTooltip"
- variant="info"
- size="sm"
- >
- {{ $options.i18n.autoDevopsBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.detached"
- v-gl-tooltip
- :title="$options.i18n.detachedBadgeTooltip"
- variant="info"
- size="sm"
- >
- {{ $options.i18n.detachedBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.stuck"
- v-gl-tooltip
- :title="$options.i18n.stuckBadgeTooltip"
- variant="warning"
- size="sm"
- >
- {{ $options.i18n.stuckBadgeText }}
- </gl-badge>
- <span
- v-gl-tooltip
- :title="$options.i18n.totalJobsTooltip"
- class="gl-ml-2"
- data-testid="total-jobs"
- >
- <gl-icon name="pipeline" />
- {{ totalJobsText }}
- </span>
- <span
- v-if="showComputeMinutes"
- v-gl-tooltip
- :title="$options.i18n.computeMinutesTooltip"
- class="gl-ml-2"
- data-testid="compute-minutes"
- >
- <gl-icon name="quota" />
- {{ computeMinutes }}
- </span>
- <span v-if="inProgress" class="gl-ml-2" data-testid="pipeline-running-text">
- <gl-icon name="timer" />
- {{ inProgressText }}
- </span>
- <span v-if="showDuration" class="gl-ml-2" data-testid="pipeline-duration-text">
- <gl-icon name="timer" />
- {{ durationText }}
- </span>
+ <div class="gl-display-inline-block gl-mb-3">
+ <gl-badge
+ v-if="badges.schedule"
+ v-gl-tooltip
+ :title="$options.i18n.scheduleBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.scheduleBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.child"
+ v-gl-tooltip
+ :title="$options.i18n.childBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ <gl-sprintf :message="$options.i18n.childBadgeText">
+ <template #link="{ content }">
+ <gl-link :href="paths.triggeredByPath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-badge>
+ <gl-badge
+ v-if="badges.latest"
+ v-gl-tooltip
+ :title="$options.i18n.latestBadgeTooltip"
+ variant="success"
+ size="sm"
+ >
+ {{ $options.i18n.latestBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.mergeTrainPipeline"
+ v-gl-tooltip
+ :title="$options.i18n.mergeTrainBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.mergeTrainBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.invalid"
+ v-gl-tooltip
+ :title="yamlErrors"
+ variant="danger"
+ size="sm"
+ >
+ {{ $options.i18n.invalidBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.failed"
+ v-gl-tooltip
+ :title="failureReason"
+ variant="danger"
+ size="sm"
+ >
+ {{ $options.i18n.failedBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.autoDevops"
+ v-gl-tooltip
+ :title="$options.i18n.autoDevopsBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.autoDevopsBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.detached"
+ v-gl-tooltip
+ :title="$options.i18n.detachedBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.detachedBadgeText }}
+ </gl-badge>
+ <gl-badge
+ v-if="badges.stuck"
+ v-gl-tooltip
+ :title="$options.i18n.stuckBadgeTooltip"
+ variant="warning"
+ size="sm"
+ >
+ {{ $options.i18n.stuckBadgeText }}
+ </gl-badge>
+ </div>
+ <div class="gl-display-inline-block">
+ <span
+ v-gl-tooltip
+ :title="$options.i18n.totalJobsTooltip"
+ class="gl-ml-2"
+ data-testid="total-jobs"
+ >
+ <gl-icon name="pipeline" />
+ {{ totalJobsText }}
+ </span>
+ <span
+ v-if="showComputeMinutes"
+ v-gl-tooltip
+ :title="$options.i18n.computeMinutesTooltip"
+ class="gl-ml-2"
+ data-testid="compute-minutes"
+ >
+ <gl-icon name="quota" />
+ {{ computeMinutes }}
+ </span>
+ <span v-if="inProgress" class="gl-ml-2" data-testid="pipeline-running-text">
+ <gl-icon name="timer" />
+ {{ inProgressText }}
+ </span>
+ <span v-if="showDuration" class="gl-ml-2" data-testid="pipeline-duration-text">
+ <gl-icon name="timer" />
+ {{ durationText }}
+ </span>
+ </div>
</div>
</div>
- <div class="gl-mt-5 gl-lg-mt-0">
+ <div class="gl-mt-5 gl-lg-mt-0 gl-display-flex gl-align-items-flex-start gl-gap-3">
<gl-button
v-if="canRetryPipeline"
v-gl-tooltip
@@ -588,7 +590,6 @@ export default {
:title="$options.BUTTON_TOOLTIP_CANCEL"
:loading="isCanceling"
:disabled="isCanceling"
- class="gl-ml-3"
variant="danger"
data-testid="cancel-pipeline"
@click="cancelPipeline()"
@@ -601,7 +602,6 @@ export default {
v-gl-modal="$options.modal.id"
:loading="isDeleting"
:disabled="isDeleting"
- class="gl-ml-3"
variant="danger"
category="secondary"
data-testid="delete-pipeline"
diff --git a/app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql b/app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql
index 1955cc9b0ac..b60afe51dd2 100644
--- a/app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql
@@ -1,4 +1,4 @@
-mutation retryFailedJob($id: CiBuildID!) {
+mutation retryFailedJob($id: CiProcessableID!) {
jobRetry(input: { id: $id }) {
job {
id
diff --git a/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js b/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js
index 53f755fda37..5d1f1ac770c 100644
--- a/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js
+++ b/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js
@@ -52,14 +52,12 @@ export default {
});
eventHub.$on('postAction', this.postAction);
- eventHub.$on('retryPipeline', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable);
eventHub.$on('updateTable', this.updateTable);
eventHub.$on('runMergeRequestPipeline', this.runMergeRequestPipeline);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
- eventHub.$off('retryPipeline', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable);
eventHub.$off('updateTable', this.updateTable);
eventHub.$off('runMergeRequestPipeline', this.runMergeRequestPipeline);
@@ -68,6 +66,15 @@ export default {
this.poll.stop();
},
methods: {
+ onCancelPipeline(pipeline) {
+ this.postAction(pipeline.cancel_path);
+ },
+ onRefreshPipelinesTable() {
+ this.updateTable();
+ },
+ onRetryPipeline(pipeline) {
+ this.postAction(pipeline.retry_path);
+ },
updateInternalState(parameters) {
this.poll.stop();
diff --git a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
index d38397e7479..8a7c3367fc1 100644
--- a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
@@ -31,10 +31,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
endpoint,
artifactsEndpoint,
artifactsEndpointPlaceholder,
- pipelineScheduleUrl,
- emptyStateSvgPath,
- errorStateSvgPath,
- noPipelinesSvgPath,
+ pipelineSchedulesPath,
newPipelinePath,
pipelineEditorPath,
suggestedCiTemplates,
@@ -55,13 +52,14 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
el,
apolloProvider,
provide: {
- pipelineEditorPath,
artifactsEndpoint,
artifactsEndpointPlaceholder,
- suggestedCiTemplates: JSON.parse(suggestedCiTemplates),
- iosRunnersAvailable: parseBoolean(iosRunnersAvailable),
fullPath,
+ iosRunnersAvailable: parseBoolean(iosRunnersAvailable),
manualActionsLimit: 50,
+ pipelineEditorPath,
+ pipelineSchedulesPath,
+ suggestedCiTemplates: JSON.parse(suggestedCiTemplates),
},
data() {
return {
@@ -77,22 +75,18 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
render(createElement) {
return createElement(Pipelines, {
props: {
- store: this.store,
- endpoint,
- pipelineScheduleUrl,
- emptyStateSvgPath,
- errorStateSvgPath,
- noPipelinesSvgPath,
- newPipelinePath,
canCreatePipeline: parseBoolean(canCreatePipeline),
- hasGitlabCi: parseBoolean(hasGitlabCi),
ciLintPath,
- resetCachePath,
- projectId,
defaultBranchName,
+ defaultVisibilityPipelineIdType: visibilityPipelineIdType,
+ endpoint,
+ hasGitlabCi: parseBoolean(hasGitlabCi),
+ newPipelinePath,
params: JSON.parse(params),
+ projectId,
registrationToken,
- defaultVisibilityPipelineIdType: visibilityPipelineIdType,
+ resetCachePath,
+ store: this.store,
},
});
},
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 58b5c0004e0..44cf11acfe2 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 CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import PipelineMiniGraph from '~/ci/pipeline_mini_graph/pipeline_mini_graph.vue';
import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
@@ -25,7 +25,7 @@ export const i18n = {
export default {
i18n,
components: {
- CiIcon,
+ CiBadgeLink,
GlButton,
GlIcon,
GlLink,
@@ -156,7 +156,12 @@ export default {
<template v-else>
<div class="gl-text-truncate gl-md-max-w-50p gl-mr-1">
<a :href="status.detailsPath" class="gl-mr-auto">
- <ci-icon :status="status" :size="16" data-testid="pipeline-status-icon" class="gl-mr-2" />
+ <ci-badge-link
+ :status="status"
+ size="md"
+ :show-text="false"
+ data-testid="pipeline-status-icon"
+ />
</a>
<span class="gl-font-weight-bold">
<gl-sprintf :message="$options.i18n.pipelineInfo">
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 bbe0f1fbefc..34640d49b80 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 CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { createAlert } from '~/alert';
import eventHub from '~/ci/event_hub';
import axios from '~/lib/utils/axios_utils';
@@ -33,7 +33,7 @@ export default {
positionFixed: true,
},
components: {
- CiIcon,
+ CiBadgeLink,
GlLoadingIcon,
GlDropdown,
LegacyJobItem,
@@ -126,14 +126,13 @@ export default {
@show="onShowDropdown"
>
<template #button-content>
- <ci-icon
- is-borderless
- is-interactive
- css-classes="gl-rounded-full"
- :is-active="isDropdownOpen"
- :size="24"
+ <ci-badge-link
:status="stage.status"
- class="gl-display-inline-flex gl-align-items-center gl-border gl-z-index-1"
+ size="md"
+ :show-text="false"
+ :show-tooltip="false"
+ :use-link="false"
+ class="gl-mb-0!"
/>
</template>
<div v-if="isLoading" class="gl--flex-center gl-p-2" data-testid="pipeline-stage-loading-state">
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 8567654a89e..cc703d29e23 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 CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { accessValue } from './accessors/linked_pipelines_accessors';
/**
* Renders the upstream/downstream portions of the pipeline mini graph.
@@ -11,7 +11,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- CiIcon,
+ CiBadgeLink,
},
inject: {
dataMethod: {
@@ -99,24 +99,18 @@ export default {
}"
class="linked-pipeline-mini-list gl-display-inline gl-vertical-align-middle"
>
- <a
+ <ci-badge-link
v-for="pipeline in linkedPipelinesTrimmed"
:key="pipeline.id"
v-gl-tooltip="{ title: pipelineTooltipText(pipeline) }"
- :href="pipeline.path"
+ :status="pipelineStatus(pipeline)"
+ size="md"
+ :show-text="false"
+ :show-tooltip="false"
:class="triggerButtonClass(pipeline)"
- class="linked-pipeline-mini-item gl-display-inline-flex gl-mr-2 gl-my-2 gl-rounded-full gl-vertical-align-middle"
+ class="linked-pipeline-mini-item gl-mb-0!"
data-testid="linked-pipeline-mini-item"
- >
- <ci-icon
- is-borderless
- is-interactive
- css-classes="gl-rounded-full"
- :size="24"
- :status="pipelineStatus(pipeline)"
- class="gl-align-items-center gl-border gl-display-inline-flex"
- />
- </a>
+ />
<a
v-if="shouldRenderCounter"
diff --git a/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
index cc7d9bd2340..2f06b82bac0 100644
--- a/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
@@ -438,8 +438,7 @@ export default {
v-for="(variable, index) in variables"
:key="variable.uniqueId"
class="gl-mb-3 gl-pb-2"
- data-testid="ci-variable-row"
- data-qa-selector="ci_variable_row_container"
+ data-testid="ci-variable-row-container"
>
<div
class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row"
@@ -461,8 +460,7 @@ export default {
v-model="variable.key"
:placeholder="s__('CiVariables|Input variable key')"
:class="$options.formElementClasses"
- data-testid="pipeline-form-ci-variable-key"
- data-qa-selector="ci_variable_key_field"
+ data-testid="pipeline-form-ci-variable-key-field"
@change="addEmptyVariable(refFullName)"
/>
<gl-dropdown
@@ -471,12 +469,11 @@ export default {
:class="$options.formElementClasses"
class="gl-flex-grow-1 gl-mr-0!"
data-testid="pipeline-form-ci-variable-value-dropdown"
- data-qa-selector="ci_variable_value_dropdown"
>
<gl-dropdown-item
v-for="option in configVariablesWithDescription.options[variable.key]"
:key="option"
- data-qa-selector="ci_variable_value_dropdown_item"
+ data-testid="ci-variable-value-dropdown-item"
@click="setVariableAttribute(variable.key, 'value', option)"
>
{{ option }}
@@ -489,8 +486,7 @@ export default {
class="gl-mb-3"
:style="$options.textAreaStyle"
:no-resize="false"
- data-testid="pipeline-form-ci-variable-value"
- data-qa-selector="ci_variable_value_field"
+ data-testid="pipeline-form-ci-variable-value-field"
/>
<template v-if="variables.length > 1">
@@ -542,8 +538,7 @@ export default {
category="primary"
variant="confirm"
class="js-no-auto-disable gl-mr-3"
- data-qa-selector="run_pipeline_button"
- data-testid="run_pipeline_button"
+ data-testid="run-pipeline-button"
:disabled="submitted"
>{{ s__('Pipeline|Run pipeline') }}</gl-button
>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
index c993b65f6c0..386835d21d4 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
@@ -4,6 +4,7 @@ import {
GlBadge,
GlButton,
GlLoadingIcon,
+ GlPagination,
GlTabs,
GlTab,
GlSprintf,
@@ -16,12 +17,20 @@ import deletePipelineScheduleMutation from '../graphql/mutations/delete_pipeline
import playPipelineScheduleMutation from '../graphql/mutations/play_pipeline_schedule.mutation.graphql';
import takeOwnershipMutation from '../graphql/mutations/take_ownership.mutation.graphql';
import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql';
-import { ALL_SCOPE } from '../constants';
+import { ALL_SCOPE, SCHEDULES_PER_PAGE } from '../constants';
import PipelineSchedulesTable from './table/pipeline_schedules_table.vue';
import TakeOwnershipModal from './take_ownership_modal.vue';
import DeletePipelineScheduleModal from './delete_pipeline_schedule_modal.vue';
import PipelineScheduleEmptyState from './pipeline_schedules_empty_state.vue';
+const defaultPagination = {
+ first: SCHEDULES_PER_PAGE,
+ last: null,
+ prevPageCursor: '',
+ nextPageCursor: '',
+ currentPage: 1,
+};
+
export default {
i18n: {
schedulesFetchError: s__('PipelineSchedules|There was a problem fetching pipeline schedules.'),
@@ -44,6 +53,7 @@ export default {
GlBadge,
GlButton,
GlLoadingIcon,
+ GlPagination,
GlTabs,
GlTab,
GlSprintf,
@@ -72,16 +82,22 @@ export default {
// we need to ensure we send null to the API when
// the scope is 'ALL'
status: this.scope === ALL_SCOPE ? null : this.scope,
+ first: this.pagination.first,
+ last: this.pagination.last,
+ prevPageCursor: this.pagination.prevPageCursor,
+ nextPageCursor: this.pagination.nextPageCursor,
};
},
update(data) {
- const { pipelineSchedules: { nodes: list = [], count } = {} } = data.project || {};
+ const { pipelineSchedules: { nodes: list = [], count, pageInfo = {} } = {} } =
+ data.project || {};
const currentUser = data.currentUser || {};
return {
list,
count,
currentUser,
+ pageInfo,
};
},
error() {
@@ -104,6 +120,9 @@ export default {
showDeleteModal: false,
showTakeOwnershipModal: false,
count: 0,
+ pagination: {
+ ...defaultPagination,
+ },
};
},
computed: {
@@ -144,6 +163,15 @@ export default {
showEmptyState() {
return !this.isLoading && this.schedulesCount === 0 && this.onAllTab;
},
+ showPagination() {
+ return this.schedules?.pageInfo?.hasNextPage || this.schedules?.pageInfo?.hasPreviousPage;
+ },
+ prevPage() {
+ return Number(this.schedules?.pageInfo?.hasPreviousPage);
+ },
+ nextPage() {
+ return Number(this.schedules?.pageInfo?.hasNextPage);
+ },
},
watch: {
// this watcher ensures that the count on the all tab
@@ -245,10 +273,36 @@ export default {
this.reportError(this.$options.i18n.schedulePlayError);
}
},
+ resetPagination() {
+ this.pagination = {
+ ...defaultPagination,
+ };
+ },
fetchPipelineSchedulesByStatus(scope) {
this.scope = scope;
+ this.resetPagination();
this.$apollo.queries.schedules.refetch();
},
+ handlePageChange(page) {
+ const { startCursor, endCursor } = this.schedules.pageInfo;
+
+ if (page > this.pagination.currentPage) {
+ this.pagination = {
+ first: SCHEDULES_PER_PAGE,
+ last: null,
+ prevPageCursor: '',
+ nextPageCursor: endCursor,
+ currentPage: page,
+ };
+ } else {
+ this.pagination = {
+ last: SCHEDULES_PER_PAGE,
+ first: null,
+ prevPageCursor: startCursor,
+ currentPage: page,
+ };
+ }
+ },
},
};
</script>
@@ -296,14 +350,25 @@ export default {
<gl-loading-icon v-if="isLoading" size="lg" />
- <pipeline-schedules-table
- v-else
- :schedules="schedules.list"
- :current-user="schedules.currentUser"
- @showTakeOwnershipModal="setTakeOwnershipModal"
- @showDeleteModal="setDeleteModal"
- @playPipelineSchedule="playPipelineSchedule"
- />
+ <template v-else>
+ <pipeline-schedules-table
+ :schedules="schedules.list"
+ :current-user="schedules.currentUser"
+ @showTakeOwnershipModal="setTakeOwnershipModal"
+ @showDeleteModal="setDeleteModal"
+ @playPipelineSchedule="playPipelineSchedule"
+ />
+
+ <gl-pagination
+ v-if="showPagination"
+ :value="pagination.currentPage"
+ :prev-page="prevPage"
+ :next-page="nextPage"
+ align="center"
+ class="gl-mt-5"
+ @input="handlePageChange"
+ />
+ </template>
</gl-tab>
<template #tabs-end>
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 0c3ede47015..cd1d9a97ef3 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
@@ -370,7 +370,7 @@ export default {
/>
</gl-form-group>
<!--Variable List-->
- <gl-form-group class="gl-mb-2" :label="$options.i18n.variables">
+ <gl-form-group class="gl-mb-0" :label="$options.i18n.variables">
<div
v-for="(variable, index) in variables"
:key="`var-${index}`"
@@ -456,13 +456,23 @@ export default {
<gl-form-checkbox id="schedule-active" v-model="activated" class="gl-mb-3">
{{ $options.i18n.activated }}
</gl-form-checkbox>
-
- <gl-button variant="confirm" data-testid="schedule-submit-button" @click="scheduleHandler">
- {{ buttonText }}
- </gl-button>
- <gl-button :href="schedulesPath" data-testid="schedule-cancel-button">
- {{ $options.i18n.cancel }}
- </gl-button>
+ <div class="gl-display-flex gl-gap-3 gl-flex-wrap">
+ <gl-button
+ variant="confirm"
+ data-testid="schedule-submit-button"
+ class="gl-w-full gl-sm-w-auto"
+ @click="scheduleHandler"
+ >
+ {{ buttonText }}
+ </gl-button>
+ <gl-button
+ :href="schedulesPath"
+ data-testid="schedule-cancel-button"
+ class="gl-w-full gl-sm-w-auto"
+ >
+ {{ $options.i18n.cancel }}
+ </gl-button>
+ </div>
</gl-form>
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/constants.js b/app/assets/javascripts/ci/pipeline_schedules/constants.js
index 16dab33ce29..be3feeb6623 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/constants.js
+++ b/app/assets/javascripts/ci/pipeline_schedules/constants.js
@@ -1,3 +1,4 @@
export const VARIABLE_TYPE = 'ENV_VAR';
export const FILE_TYPE = 'FILE';
export const ALL_SCOPE = 'ALL';
+export const SCHEDULES_PER_PAGE = 50;
diff --git a/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql b/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
index 29a26be0344..8fe9fbc5e24 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
@@ -1,7 +1,13 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+
query getPipelineSchedulesQuery(
$projectPath: ID!
$status: PipelineScheduleStatus
$ids: [ID!] = null
+ $first: Int
+ $last: Int
+ $prevPageCursor: String = ""
+ $nextPageCursor: String = ""
) {
currentUser {
id
@@ -9,7 +15,14 @@ query getPipelineSchedulesQuery(
}
project(fullPath: $projectPath) {
id
- pipelineSchedules(status: $status, ids: $ids) {
+ pipelineSchedules(
+ status: $status
+ ids: $ids
+ first: $first
+ last: $last
+ after: $nextPageCursor
+ before: $prevPageCursor
+ ) {
count
nodes {
id
@@ -56,6 +69,9 @@ query getPipelineSchedulesQuery(
adminPipelineSchedule
}
}
+ pageInfo {
+ ...PageInfo
+ }
}
}
}
diff --git a/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
index 6e7d6908cd9..728e8541ae3 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
@@ -47,6 +47,7 @@ export default {
v-else
title=""
:svg-path="emptyStateSvgPath"
+ :svg-height="null"
:description="$options.i18n.noCiDescription"
/>
</div>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue b/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue
index 235126fea0c..0165bbfe69d 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue
@@ -7,28 +7,25 @@ export default {
GlButton,
},
props: {
- newPipelinePath: {
+ ciLintPath: {
type: String,
required: false,
default: null,
},
-
- resetCachePath: {
- type: String,
+ isResetCacheButtonLoading: {
+ type: Boolean,
required: false,
- default: null,
+ default: false,
},
-
- ciLintPath: {
+ newPipelinePath: {
type: String,
required: false,
default: null,
},
-
- isResetCacheButtonLoading: {
- type: Boolean,
+ resetCachePath: {
+ type: String,
required: false,
- default: false,
+ default: null,
},
},
methods: {
@@ -61,7 +58,6 @@ export default {
category="primary"
class="js-run-pipeline"
data-testid="run-pipeline-button"
- data-qa-selector="run_pipeline_button"
>
{{ s__('Pipeline|Run pipeline') }}
</gl-button>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
index 082ede60244..8f45094eb74 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
@@ -17,16 +17,15 @@ export default {
targetProjectFullPath: {
default: '',
},
+ pipelineSchedulesPath: {
+ default: '',
+ },
},
props: {
pipeline: {
type: Object,
required: true,
},
- pipelineScheduleUrl: {
- type: String,
- required: true,
- },
},
computed: {
isScheduled() {
@@ -38,6 +37,13 @@ export default {
this.pipeline?.project?.full_path !== `/${this.targetProjectFullPath}`,
);
},
+ showMergedResultsBadge() {
+ // A merge train pipeline is technically also a merged results pipeline,
+ // but we want the badges to be mutually exclusive.
+ return (
+ this.pipeline.flags.merged_result_pipeline && !this.pipeline.flags.merge_train_pipeline
+ );
+ },
autoDevopsTagId() {
return `pipeline-url-autodevops-${this.pipeline.id}`;
},
@@ -52,7 +58,7 @@ export default {
<gl-badge
v-if="isScheduled"
v-gl-tooltip
- :href="pipelineScheduleUrl"
+ :href="pipelineSchedulesPath"
target="__blank"
:title="__('This pipeline was created by a schedule.')"
variant="info"
@@ -74,7 +80,7 @@ export default {
v-gl-tooltip
:title="
s__(
- 'Pipeline|This pipeline ran on the contents of this merge request combined with the contents of all other merge requests queued for merging into the target branch.',
+ 'Pipeline|This pipeline ran on the contents of the merge request combined with the contents of all other merge requests queued for merging into the target branch.',
)
"
variant="info"
@@ -149,7 +155,7 @@ export default {
v-gl-tooltip
:title="
s__(
- `Pipeline|This pipeline ran on the contents of this merge request's source branch, not the target branch.`,
+ `Pipeline|This pipeline ran on the contents of the merge request's source branch, not the target branch.`,
)
"
variant="info"
@@ -158,6 +164,19 @@ export default {
>{{ s__('Pipeline|merge request') }}</gl-badge
>
<gl-badge
+ v-if="showMergedResultsBadge"
+ v-gl-tooltip
+ :title="
+ s__(
+ `Pipeline|This pipeline ran on the contents of the merge request combined with the contents of the target branch.`,
+ )
+ "
+ variant="info"
+ size="sm"
+ data-testid="pipeline-url-merged-results"
+ >{{ s__('Pipeline|merged results') }}</gl-badge
+ >
+ <gl-badge
v-if="isInFork"
v-gl-tooltip
:title="__('Pipeline ran in fork of project')"
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue
index b05bdae65c4..8945bb06862 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue
@@ -1,22 +1,22 @@
<script>
-import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import Tracking from '~/tracking';
import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL, TRACKING_CATEGORIES } from '~/ci/constants';
-import eventHub from '../../event_hub';
import PipelineMultiActions from './pipeline_multi_actions.vue';
import PipelinesManualActions from './pipelines_manual_actions.vue';
+import PipelineStopModal from './pipeline_stop_modal.vue';
export default {
BUTTON_TOOLTIP_RETRY,
BUTTON_TOOLTIP_CANCEL,
directives: {
GlTooltip: GlTooltipDirective,
- GlModalDirective,
},
components: {
GlButton,
PipelineMultiActions,
PipelinesManualActions,
+ PipelineStopModal,
},
mixins: [Tracking.mixin()],
props: {
@@ -24,15 +24,12 @@ export default {
type: Object,
required: true,
},
- cancelingPipeline: {
- type: Number,
- required: false,
- default: null,
- },
},
data() {
return {
+ isCanceling: false,
isRetrying: false,
+ showConfirmationModal: false,
};
},
computed: {
@@ -41,27 +38,36 @@ export default {
this.pipeline?.details?.has_manual_actions || this.pipeline?.details?.has_scheduled_actions
);
},
- isCancelling() {
- return this.cancelingPipeline === this.pipeline.id;
- },
},
watch: {
pipeline() {
- this.isRetrying = false;
+ if (this.isCanceling || this.isRetrying) {
+ this.isCanceling = false;
+ this.isRetrying = false;
+ }
},
},
methods: {
+ onCloseModal() {
+ this.showConfirmationModal = false;
+ },
+ onConfirmCancelPipeline() {
+ this.isCanceling = true;
+ this.showConfirmationModal = false;
+
+ this.$emit('cancel-pipeline', this.pipeline);
+ },
handleCancelClick() {
+ this.showConfirmationModal = true;
+
this.trackClick('click_cancel_button');
- eventHub.$emit('openConfirmationModal', {
- pipeline: this.pipeline,
- endpoint: this.pipeline.cancel_path,
- });
},
handleRetryClick() {
this.isRetrying = true;
+
this.trackClick('click_retry_button');
- eventHub.$emit('retryPipeline', this.pipeline.retry_path);
+
+ this.$emit('retry-pipeline', this.pipeline);
},
trackClick(action) {
this.track(action, { label: TRACKING_CATEGORIES.table });
@@ -72,8 +78,19 @@ export default {
<template>
<div class="gl-text-right">
+ <pipeline-stop-modal
+ :pipeline="pipeline"
+ :show-confirmation-modal="showConfirmationModal"
+ @submit="onConfirmCancelPipeline"
+ @close-modal="onCloseModal"
+ />
+
<div class="btn-group">
- <pipelines-manual-actions v-if="hasActions" :iid="pipeline.iid" />
+ <pipelines-manual-actions
+ v-if="hasActions"
+ :iid="pipeline.iid"
+ @refresh-pipeline-table="$emit('refresh-pipelines-table')"
+ />
<gl-button
v-if="pipeline.flags.retryable"
@@ -83,7 +100,6 @@ export default {
:disabled="isRetrying"
:loading="isRetrying"
class="js-pipelines-retry-button"
- data-qa-selector="pipeline_retry_button"
data-testid="pipelines-retry-button"
icon="retry"
variant="default"
@@ -94,11 +110,10 @@ export default {
<gl-button
v-if="pipeline.flags.cancelable"
v-gl-tooltip.hover
- v-gl-modal-directive="'confirmation-modal'"
:aria-label="$options.BUTTON_TOOLTIP_CANCEL"
:title="$options.BUTTON_TOOLTIP_CANCEL"
- :loading="isCancelling"
- :disabled="isCancelling"
+ :loading="isCanceling"
+ :disabled="isCanceling"
icon="cancel"
variant="danger"
category="primary"
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipelines_status_badge.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
index 2da9141df8e..20e2c7e9dce 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipelines_status_badge.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
@@ -1,6 +1,5 @@
<script>
import { TRACKING_CATEGORIES } from '~/ci/constants';
-import { CHILD_VIEW } from '~/ci/pipeline_details/constants';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import Tracking from '~/tracking';
import PipelinesTimeago from './time_ago.vue';
@@ -16,18 +15,11 @@ export default {
type: Object,
required: true,
},
- viewType: {
- type: String,
- required: true,
- },
},
computed: {
pipelineStatus() {
return this.pipeline?.details?.status ?? {};
},
- isChildView() {
- return this.viewType === CHILD_VIEW;
- },
},
methods: {
trackClick() {
@@ -39,13 +31,7 @@ export default {
<template>
<div>
- <ci-badge-link
- class="gl-mb-3"
- :status="pipelineStatus"
- :show-text="!isChildView"
- data-qa-selector="pipeline_commit_status"
- @ciStatusBadgeClick="trackClick"
- />
+ <ci-badge-link class="gl-mb-3" :status="pipelineStatus" @ciStatusBadgeClick="trackClick" />
<pipelines-timeago :pipeline="pipeline" />
</div>
</template>
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 9f38be668f2..d62a68f0dcc 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
@@ -7,7 +7,7 @@ import CiIcon from '~/vue_shared/components/ci_icon.vue';
/**
* Pipeline Stop Modal.
*
- * Renders the modal used to confirm stopping a pipeline.
+ * Renders the modal used to confirm cancelling a pipeline.
*/
export default {
components: {
@@ -22,8 +22,15 @@ export default {
required: true,
deep: true,
},
+ showConfirmationModal: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
+ hasRef() {
+ return !isEmpty(this.pipeline.ref);
+ },
modalTitle() {
return sprintf(
s__('Pipeline|Stop pipeline #%{pipelineId}?'),
@@ -34,10 +41,7 @@ export default {
);
},
modalText() {
- return s__(`Pipeline|You’re about to stop pipeline #%{pipelineId}.`);
- },
- hasRef() {
- return !isEmpty(this.pipeline.ref);
+ return s__(`Pipeline|You're about to stop pipeline #%{pipelineId}.`);
},
primaryProps() {
return {
@@ -45,10 +49,13 @@ export default {
attributes: { variant: 'danger' },
};
},
- cancelProps() {
- return {
- text: __('Cancel'),
- };
+ showModal: {
+ get() {
+ return this.showConfirmationModal;
+ },
+ set() {
+ this.$emit('close-modal');
+ },
},
},
methods: {
@@ -56,14 +63,16 @@ export default {
this.$emit('submit', event);
},
},
+ cancelProps: { text: __('Cancel') },
};
</script>
<template>
<gl-modal
+ v-model="showModal"
modal-id="confirmation-modal"
:title="modalTitle"
:action-primary="primaryProps"
- :action-cancel="cancelProps"
+ :action-cancel="$options.cancelProps"
@primary="emitSubmit($event)"
>
<p>
@@ -74,7 +83,7 @@ export default {
</gl-sprintf>
</p>
- <p v-if="pipeline">
+ <p>
<ci-icon
v-if="pipeline.details"
:status="pipeline.details.status"
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue
index edaeb481d7b..9a49eefbf98 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue
@@ -4,7 +4,7 @@ import { __ } from '~/locale';
import Tracking from '~/tracking';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import { ICONS, TRACKING_CATEGORIES } from '~/ci/constants';
+import { ICONS, PIPELINE_ID_KEY, PIPELINE_IID_KEY, TRACKING_CATEGORIES } from '~/ci/constants';
import PipelineLabels from './pipeline_labels.vue';
export default {
@@ -24,13 +24,13 @@ export default {
type: Object,
required: true,
},
- pipelineScheduleUrl: {
+ pipelineIdType: {
type: String,
- required: true,
- },
- pipelineKey: {
- type: String,
- required: true,
+ required: false,
+ default: PIPELINE_ID_KEY,
+ validator(value) {
+ return value === PIPELINE_IID_KEY || value === PIPELINE_ID_KEY;
+ },
},
refClass: {
type: String,
@@ -173,9 +173,8 @@ export default {
:href="pipeline.path"
class="gl-mr-1 gl-text-blue-500!"
data-testid="pipeline-url-link"
- data-qa-selector="pipeline_url_link"
@click="trackClick('click_pipeline_id')"
- >#{{ pipeline[pipelineKey] }}</gl-link
+ >#{{ pipeline[pipelineIdType] }}</gl-link
>
<!--Commit row-->
<div class="gl-display-inline-flex gl-rounded-base gl-px-2 gl-bg-gray-50 gl-text-gray-700">
@@ -237,6 +236,6 @@ export default {
/>
<!--End of commit row-->
</div>
- <pipeline-labels :pipeline-schedule-url="pipelineScheduleUrl" :pipeline="pipeline" />
+ <pipeline-labels :pipeline="pipeline" />
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue b/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue
index 4dacd474bde..ebf1744aee2 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue
@@ -6,7 +6,6 @@ import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_m
import { s__, __, sprintf } from '~/locale';
import Tracking from '~/tracking';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
-import eventHub from '../../event_hub';
import { TRACKING_CATEGORIES } from '../../constants';
import getPipelineActionsQuery from '../graphql/queries/get_pipeline_actions.query.graphql';
@@ -94,7 +93,7 @@ export default {
.post(`${action.playPath}.json`)
.then(() => {
this.isLoading = false;
- eventHub.$emit('updateTable');
+ this.$emit('refresh-pipeline-table');
})
.catch(() => {
this.isLoading = false;
diff --git a/app/assets/javascripts/ci/pipelines_page/pipelines.vue b/app/assets/javascripts/ci/pipelines_page/pipelines.vue
index 87ee5463bb0..faa013079be 100644
--- a/app/assets/javascripts/ci/pipelines_page/pipelines.vue
+++ b/app/assets/javascripts/ci/pipelines_page/pipelines.vue
@@ -1,5 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
+import NO_PIPELINES_SVG from '@gitlab/svgs/dist/illustrations/empty-state/empty-pipeline-md.svg?url';
+import ERROR_STATE_SVG from '@gitlab/svgs/dist/illustrations/pipelines_failed.svg?url';
import { GlEmptyState, GlIcon, GlLoadingIcon, GlCollapsibleListbox } from '@gitlab/ui';
import { isEqual } from 'lodash';
import * as Sentry from '@sentry/browser';
@@ -9,11 +11,12 @@ import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
import {
FILTER_TAG_IDENTIFIER,
- PipelineKeyOptions,
+ PIPELINE_ID_KEY,
+ PIPELINE_IID_KEY,
RAW_TEXT_WARNING,
TRACKING_CATEGORIES,
} from '~/ci/constants';
-import PipelinesTableComponent from '~/ci/common/pipelines_table.vue';
+import PipelinesTable from '~/ci/common/pipelines_table.vue';
import PipelinesMixin from '~/ci/pipeline_details/mixins/pipelines_mixin';
import { validateParams } from '~/ci/pipeline_details/utils';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
@@ -27,7 +30,6 @@ import NavigationControls from './components/nav_controls.vue';
import PipelinesFilteredSearch from './components/pipelines_filtered_search.vue';
export default {
- PipelineKeyOptions,
components: {
NoCiEmptyState,
GlCollapsibleListbox,
@@ -37,7 +39,7 @@ export default {
NavigationTabs,
NavigationControls,
PipelinesFilteredSearch,
- PipelinesTableComponent,
+ PipelinesTable,
TablePagination,
},
mixins: [PipelinesMixin, Tracking.mixin()],
@@ -46,36 +48,10 @@ export default {
type: Object,
required: true,
},
- // Can be rendered in 3 different places, with some visual differences
- // Accepts root | child
- // `root` -> main view
- // `child` -> rendered inside MR or Commit View
- viewType: {
- type: String,
- required: false,
- default: 'root',
- },
endpoint: {
type: String,
required: true,
},
- pipelineScheduleUrl: {
- type: String,
- required: false,
- default: '',
- },
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- errorStateSvgPath: {
- type: String,
- required: true,
- },
- noPipelinesSvgPath: {
- type: String,
- required: true,
- },
hasGitlabCi: {
type: Boolean,
required: true,
@@ -243,8 +219,9 @@ export default {
},
selectedPipelineKeyOption() {
return (
- this.$options.PipelineKeyOptions.find((e) => this.visibilityPipelineIdType === e.value) ||
- this.$options.PipelineKeyOptions[0]
+ this.$options.pipelineKeyOptions.find(
+ (option) => this.visibilityPipelineIdType === option.value,
+ ) || this.$options.pipelineKeyOptions[0]
);
},
},
@@ -334,11 +311,12 @@ export default {
},
changeVisibilityPipelineIDType(idType) {
this.visibilityPipelineIdType = idType;
- this.saveVisibilityPipelineIDType(idType);
+
+ if (isLoggedIn()) {
+ this.saveVisibilityPipelineIDType(idType);
+ }
},
saveVisibilityPipelineIDType(idType) {
- if (!isLoggedIn()) return;
-
this.$apollo
.mutate({
mutation: setSortPreferenceMutation,
@@ -354,6 +332,20 @@ export default {
});
},
},
+ errorStateSvgPath: ERROR_STATE_SVG,
+ noPipelinesSvgPath: NO_PIPELINES_SVG,
+ pipelineKeyOptions: [
+ {
+ text: __('Show Pipeline ID'),
+ label: __('Pipeline ID'),
+ value: PIPELINE_ID_KEY,
+ },
+ {
+ text: __('Show Pipeline IID'),
+ label: __('Pipeline IID'),
+ value: PIPELINE_IID_KEY,
+ },
+ ],
};
</script>
<template>
@@ -393,9 +385,8 @@ export default {
/>
<gl-collapsible-listbox
v-model="visibilityPipelineIdType"
- data-testid="pipeline-key-collapsible-box"
:toggle-text="selectedPipelineKeyOption.text"
- :items="$options.PipelineKeyOptions"
+ :items="$options.pipelineKeyOptions"
@select="changeVisibilityPipelineIDType"
/>
</div>
@@ -411,32 +402,34 @@ export default {
<no-ci-empty-state
v-else-if="stateToRender === $options.stateMap.emptyState"
- :empty-state-svg-path="emptyStateSvgPath"
+ :empty-state-svg-path="$options.noPipelinesSvgPath"
:can-set-ci="canCreatePipeline"
:registration-token="registrationToken"
/>
<gl-empty-state
v-else-if="stateToRender === $options.stateMap.error"
- :svg-path="errorStateSvgPath"
+ :svg-path="$options.errorStateSvgPath"
+ :svg-height="null"
:title="s__('Pipelines|There was an error fetching the pipelines.')"
:description="s__('Pipelines|Try again in a few moments or contact your support team.')"
/>
<gl-empty-state
v-else-if="stateToRender === $options.stateMap.emptyTab"
- :svg-path="noPipelinesSvgPath"
+ :svg-path="$options.noPipelinesSvgPath"
:svg-height="150"
:title="emptyTabMessage"
/>
<div v-else-if="stateToRender === $options.stateMap.tableList">
- <pipelines-table-component
+ <pipelines-table
:pipelines="state.pipelines"
- :pipeline-schedule-url="pipelineScheduleUrl"
:update-graph-dropdown="updateGraphDropdown"
- :view-type="viewType"
- :pipeline-key-option="selectedPipelineKeyOption"
+ :pipeline-id-type="selectedPipelineKeyOption.value"
+ @cancel-pipeline="onCancelPipeline"
+ @refresh-pipelines-table="onRefreshPipelinesTable"
+ @retry-pipeline="onRetryPipeline"
/>
</div>
diff --git a/app/assets/javascripts/ci/runner/components/registration/utils.js b/app/assets/javascripts/ci/runner/components/registration/utils.js
index c8a75506c9c..c1885be9585 100644
--- a/app/assets/javascripts/ci/runner/components/registration/utils.js
+++ b/app/assets/javascripts/ci/runner/components/registration/utils.js
@@ -3,8 +3,8 @@ import {
LINUX_PLATFORM,
MACOS_PLATFORM,
WINDOWS_PLATFORM,
- DOWNLOAD_LOCATIONS,
-} from '../../constants';
+ RUNNER_PACKAGE_HOST,
+} from 'jh_else_ce/ci/runner/constants';
import linuxInstall from './scripts/linux/install.sh?raw';
import osxInstall from './scripts/osx/install.sh?raw';
import windowsInstall from './scripts/windows/install.ps1?raw';
@@ -27,6 +27,47 @@ const OS = {
},
};
+export const DOWNLOAD_LOCATIONS = {
+ [LINUX_PLATFORM]: [
+ {
+ arch: 'amd64',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-linux-amd64`,
+ },
+ {
+ arch: '386',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-linux-386`,
+ },
+ {
+ arch: 'arm',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-linux-arm`,
+ },
+ {
+ arch: 'arm64',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-linux-arm64`,
+ },
+ ],
+ [MACOS_PLATFORM]: [
+ {
+ arch: 'amd64',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-darwin-amd64`,
+ },
+ {
+ arch: 'arm64',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-darwin-arm64`,
+ },
+ ],
+ [WINDOWS_PLATFORM]: [
+ {
+ arch: 'amd64',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-windows-amd64.exe`,
+ },
+ {
+ arch: '386',
+ url: `https://${RUNNER_PACKAGE_HOST}/latest/binaries/gitlab-runner-windows-386.exe`,
+ },
+ ],
+};
+
export const commandPrompt = ({ platform }) => {
return (OS[platform] || OS[DEFAULT_PLATFORM]).commandPrompt;
};
diff --git a/app/assets/javascripts/ci/runner/components/runner_details.vue b/app/assets/javascripts/ci/runner/components/runner_details.vue
index fac90fb0370..0ec2ef30c20 100644
--- a/app/assets/javascripts/ci/runner/components/runner_details.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_details.vue
@@ -29,10 +29,6 @@ export default {
import('ee_component/ci/runner/components/runner_maintenance_note_detail.vue'),
RunnerGroups,
RunnerProjects,
- RunnerUpgradeStatusBadge: () =>
- import('ee_component/ci/runner/components/runner_upgrade_status_badge.vue'),
- RunnerUpgradeStatusAlert: () =>
- import('ee_component/ci/runner/components/runner_upgrade_status_alert.vue'),
RunnerTags,
RunnerManagersDetail,
TimeAgo,
@@ -92,7 +88,6 @@ export default {
<template>
<div>
- <runner-upgrade-status-alert class="gl-my-4" :runner="runner" />
<div class="gl-pt-4">
<dl class="gl-mb-0 gl-display-grid runner-details-grid-template">
<runner-detail :label="s__('Runners|Description')" :value="runner.description" />
@@ -104,16 +99,6 @@ export default {
<time-ago :time="runner.contactedAt" />
</template>
</runner-detail>
- <runner-detail :label="s__('Runners|Version')">
- <template v-if="runner.version" #value>
- {{ runner.version }}
- <runner-upgrade-status-badge size="sm" :runner="runner" />
- </template>
- </runner-detail>
- <runner-detail :label="s__('Runners|IP Address')" :value="runner.ipAddress" />
- <runner-detail :label="s__('Runners|Executor')" :value="runner.executorName" />
- <runner-detail :label="s__('Runners|Architecture')" :value="runner.architectureName" />
- <runner-detail :label="s__('Runners|Platform')" :value="runner.platformName" />
<runner-detail :label="s__('Runners|Configuration')">
<template v-if="configTextProtected || configTextUntagged" #value>
<gl-intersperse>
diff --git a/app/assets/javascripts/ci/runner/components/runner_form_fields.vue b/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
index 38e36733045..b8c80986fbc 100644
--- a/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
@@ -92,9 +92,7 @@ export default {
<gl-form-group :label="__('Tags')" label-for="runner-tags">
<template #description>
<gl-sprintf
- :message="
- s__('Runners|Multiple tags must be separated by a comma. For example, %{example}.')
- "
+ :message="s__('Runners|Separate multiple tags with a comma. For example, %{example}.')"
>
<template #example>
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
@@ -106,7 +104,7 @@ export default {
<gl-sprintf
:message="
s__(
- 'Runners|Add tags for the types of jobs the runner processes to ensure that the runner only runs jobs that you intend it to. %{helpLinkStart}Learn more.%{helpLinkEnd}',
+ 'Runners|Add tags to specify jobs that the runner can run. %{helpLinkStart}Learn more.%{helpLinkEnd}',
)
"
>
@@ -191,7 +189,9 @@ export default {
)
"
label-for="runner-max-timeout"
- :description="s__('Runners|Enter the number of seconds.')"
+ :description="
+ s__('Runners|Enter the job timeout in seconds. Must be a minimum of 600 seconds.')
+ "
>
<gl-form-input
id="runner-max-timeout"
diff --git a/app/assets/javascripts/ci/runner/components/runner_header.vue b/app/assets/javascripts/ci/runner/components/runner_header.vue
index 55a33ef2074..0fa06537ed6 100644
--- a/app/assets/javascripts/ci/runner/components/runner_header.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_header.vue
@@ -13,6 +13,8 @@ export default {
TimeAgo,
RunnerTypeBadge,
RunnerStatusBadge,
+ RunnerUpgradeStatusBadge: () =>
+ import('ee_component/ci/runner/components/runner_upgrade_status_badge.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -40,6 +42,7 @@ export default {
<div class="gl-display-flex gl-align-items-flex-start gl-gap-3 gl-flex-wrap gl-mt-3">
<runner-status-badge :contacted-at="runner.contactedAt" :status="runner.status" />
<runner-type-badge :type="runner.runnerType" />
+ <runner-upgrade-status-badge :runner="runner" />
<span v-if="runner.createdAt">
<gl-sprintf :message="__('%{locked} created %{timeago}')">
<template #locked>
diff --git a/app/assets/javascripts/ci/runner/components/runner_type_icon.vue b/app/assets/javascripts/ci/runner/components/runner_type_icon.vue
new file mode 100644
index 00000000000..c56f28e10a3
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_type_icon.vue
@@ -0,0 +1,62 @@
+<script>
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import {
+ INSTANCE_TYPE,
+ GROUP_TYPE,
+ PROJECT_TYPE,
+ I18N_INSTANCE_TYPE,
+ I18N_INSTANCE_RUNNER_DESCRIPTION,
+ I18N_GROUP_TYPE,
+ I18N_GROUP_RUNNER_DESCRIPTION,
+ I18N_PROJECT_TYPE,
+ I18N_PROJECT_RUNNER_DESCRIPTION,
+} from '../constants';
+
+const ICON_DATA = {
+ [INSTANCE_TYPE]: {
+ name: 'users',
+ tooltip: `${I18N_INSTANCE_TYPE}: ${I18N_INSTANCE_RUNNER_DESCRIPTION}`,
+ },
+ [GROUP_TYPE]: {
+ name: 'group',
+ tooltip: `${I18N_GROUP_TYPE}: ${I18N_GROUP_RUNNER_DESCRIPTION}`,
+ },
+ [PROJECT_TYPE]: {
+ name: 'project',
+ tooltip: `${I18N_PROJECT_TYPE}: ${I18N_PROJECT_RUNNER_DESCRIPTION}`,
+ },
+};
+
+export default {
+ components: {
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ type: {
+ type: String,
+ required: false,
+ default: null,
+ validator(type) {
+ return Boolean(ICON_DATA[type]);
+ },
+ },
+ },
+ computed: {
+ icon() {
+ return ICON_DATA[this.type];
+ },
+ },
+};
+</script>
+<template>
+ <gl-icon
+ v-if="icon"
+ v-gl-tooltip="icon.tooltip"
+ :aria-label="icon.tooltip"
+ :name="icon.name"
+ v-bind="$attrs"
+ />
+</template>
diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js
index 3293c68ddb8..b3cc295f8e4 100644
--- a/app/assets/javascripts/ci/runner/constants.js
+++ b/app/assets/javascripts/ci/runner/constants.js
@@ -216,54 +216,8 @@ export const LINUX_PLATFORM = 'linux';
export const MACOS_PLATFORM = 'osx';
export const WINDOWS_PLATFORM = 'windows';
-export const DOWNLOAD_LOCATIONS = {
- [LINUX_PLATFORM]: [
- {
- arch: 'amd64',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64',
- },
- {
- arch: '386',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-386',
- },
- {
- arch: 'arm',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm',
- },
- {
- arch: 'arm64',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm64',
- },
- ],
- [MACOS_PLATFORM]: [
- {
- arch: 'amd64',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64',
- },
- {
- arch: 'arm64',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-arm64',
- },
- ],
- [WINDOWS_PLATFORM]: [
- {
- arch: 'amd64',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe',
- },
- {
- arch: '386',
- url:
- 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-386.exe',
- },
- ],
-};
+// About Gitlab Runner Package host
+export const RUNNER_PACKAGE_HOST = 'gitlab-runner-downloads.s3.amazonaws.com';
export const DEFAULT_PLATFORM = LINUX_PLATFORM;
diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql
index 1a2ad59650e..e2c890b3834 100644
--- a/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql
@@ -6,10 +6,6 @@ fragment RunnerDetailsShared on CiRunner {
accessLevel
runUntagged
locked
- ipAddress
- executorName
- architectureName
- platformName
description
maximumTimeout
jobCount
diff --git a/app/assets/javascripts/ci/runner/sentry_utils.js b/app/assets/javascripts/ci/runner/sentry_utils.js
index 29de1f9adae..25fecdcfa7d 100644
--- a/app/assets/javascripts/ci/runner/sentry_utils.js
+++ b/app/assets/javascripts/ci/runner/sentry_utils.js
@@ -6,15 +6,16 @@ const COMPONENT_TAG = 'vue_component';
* Captures an error in a Vue component and sends it
* to Sentry
*
- * @param {Object} options
- * @param {Error} options.error - Exception or error
- * @param {String} options.component - Component name in CamelCase format
+ * @param {Object} options Exception details
+ * @param {Object} options.error An exception-like object
+ * @param {string} [options.component=] Component name in CamelCase format
*/
export const captureException = ({ error, component }) => {
- Sentry.withScope((scope) => {
- if (component) {
- scope.setTag(COMPONENT_TAG, component);
- }
+ if (component) {
+ Sentry.captureException(error, {
+ tags: { [COMPONENT_TAG]: component },
+ });
+ } else {
Sentry.captureException(error);
- });
+ }
};
diff --git a/app/assets/javascripts/ci/utils.js b/app/assets/javascripts/ci/utils.js
index eb9e9538b75..8a4f28404c6 100644
--- a/app/assets/javascripts/ci/utils.js
+++ b/app/assets/javascripts/ci/utils.js
@@ -1,17 +1,9 @@
import * as Sentry from '@sentry/browser';
export const reportToSentry = (component, failureType) => {
- Sentry.withScope((scope) => {
- scope.setTag('component', component);
- Sentry.captureException(failureType);
- });
-};
-
-export const reportMessageToSentry = (component, message, context) => {
- Sentry.withScope((scope) => {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- scope.setContext('Vue data', context);
- scope.setTag('component', component);
- Sentry.captureMessage(message);
+ Sentry.captureException(failureType, {
+ tags: {
+ component,
+ },
});
};