diff options
Diffstat (limited to 'app/assets/javascripts/ci')
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, + }, }); }; |