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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-10-20 18:10:58 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-10-20 18:10:58 +0300
commit049d16d168fdee408b78f5f38619c092fd3b2265 (patch)
tree22d1db5ab4fae0967a4da4b1a6b097ef9e5d7aa2 /app/assets/javascripts/artifacts
parentbf18f3295b550c564086efd0a32d9a25435ce216 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/artifacts')
-rw-r--r--app/assets/javascripts/artifacts/components/artifact_row.vue88
-rw-r--r--app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue89
-rw-r--r--app/assets/javascripts/artifacts/components/job_artifacts_table.vue314
-rw-r--r--app/assets/javascripts/artifacts/constants.js47
-rw-r--r--app/assets/javascripts/artifacts/graphql/cache_update.js30
-rw-r--r--app/assets/javascripts/artifacts/graphql/mutations/destroy_artifact.mutation.graphql7
-rw-r--r--app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql56
-rw-r--r--app/assets/javascripts/artifacts/index.js29
-rw-r--r--app/assets/javascripts/artifacts/utils.js26
9 files changed, 686 insertions, 0 deletions
diff --git a/app/assets/javascripts/artifacts/components/artifact_row.vue b/app/assets/javascripts/artifacts/components/artifact_row.vue
new file mode 100644
index 00000000000..92044a3641a
--- /dev/null
+++ b/app/assets/javascripts/artifacts/components/artifact_row.vue
@@ -0,0 +1,88 @@
+<script>
+import { GlButtonGroup, GlButton, GlBadge } from '@gitlab/ui';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { I18N_EXPIRED, I18N_DOWNLOAD, I18N_DELETE } from '../constants';
+
+export default {
+ name: 'ArtifactRow',
+ components: {
+ GlButtonGroup,
+ GlButton,
+ GlBadge,
+ },
+ props: {
+ artifact: {
+ type: Object,
+ required: true,
+ },
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ isLastRow: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ isExpired() {
+ if (!this.artifact.expireAt) {
+ return false;
+ }
+ return Date.now() > new Date(this.artifact.expireAt).getTime();
+ },
+ artifactSize() {
+ return numberToHumanSize(this.artifact.size);
+ },
+ },
+ i18n: {
+ expired: I18N_EXPIRED,
+ download: I18N_DOWNLOAD,
+ delete: I18N_DELETE,
+ },
+};
+</script>
+<template>
+ <div
+ class="gl-py-4"
+ :class="{ 'gl-border-b-solid gl-border-b-1 gl-border-gray-100': !isLastRow }"
+ >
+ <div class="gl-display-inline-flex gl-align-items-center gl-w-full">
+ <span class="gl-w-half gl-pl-8 gl-display-flex" data-testid="job-artifact-row-name">
+ {{ artifact.name }}
+ <gl-badge size="sm" variant="neutral" class="gl-ml-2">
+ {{ artifact.fileType.toLowerCase() }}
+ </gl-badge>
+ <gl-badge v-if="isExpired" size="sm" variant="warning" icon="expire" class="gl-ml-2">
+ {{ $options.i18n.expired }}
+ </gl-badge>
+ </span>
+
+ <span class="gl-w-quarter gl-text-right gl-pr-5" data-testid="job-artifact-row-size">
+ {{ artifactSize }}
+ </span>
+
+ <span class="gl-w-quarter gl-text-right gl-pr-5">
+ <gl-button-group>
+ <gl-button
+ category="tertiary"
+ icon="download"
+ :title="$options.i18n.download"
+ :aria-label="$options.i18n.download"
+ :href="artifact.downloadPath"
+ data-testid="job-artifact-row-download-button"
+ />
+ <gl-button
+ category="tertiary"
+ icon="remove"
+ :title="$options.i18n.delete"
+ :aria-label="$options.i18n.delete"
+ :loading="isLoading"
+ data-testid="job-artifact-row-delete-button"
+ @click="$emit('delete')"
+ />
+ </gl-button-group>
+ </span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue b/app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue
new file mode 100644
index 00000000000..089bfd80222
--- /dev/null
+++ b/app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue
@@ -0,0 +1,89 @@
+<script>
+import { createAlert } from '~/flash';
+import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
+import getJobArtifactsQuery from '../graphql/queries/get_job_artifacts.query.graphql';
+import destroyArtifactMutation from '../graphql/mutations/destroy_artifact.mutation.graphql';
+import { removeArtifactFromStore } from '../graphql/cache_update';
+import {
+ I18N_DESTROY_ERROR,
+ ARTIFACT_ROW_HEIGHT,
+ ARTIFACTS_SHOWN_WITHOUT_SCROLLING,
+} from '../constants';
+import ArtifactRow from './artifact_row.vue';
+
+export default {
+ name: 'ArtifactsTableRowDetails',
+ components: {
+ DynamicScroller,
+ DynamicScrollerItem,
+ ArtifactRow,
+ },
+ props: {
+ artifacts: {
+ type: Object,
+ required: true,
+ },
+ queryVariables: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ deletingArtifactId: null,
+ };
+ },
+ computed: {
+ scrollContainerStyle() {
+ /*
+ limit the height of the expanded artifacts container to a number of artifacts
+ if a job has more artifacts than ARTIFACTS_SHOWN_WITHOUT_SCROLLING, scroll to see the rest
+ add one pixel to row height to account for borders
+ */
+ return { maxHeight: `${ARTIFACTS_SHOWN_WITHOUT_SCROLLING * (ARTIFACT_ROW_HEIGHT + 1)}px` };
+ },
+ },
+ methods: {
+ isLastRow(index) {
+ return index === this.artifacts.nodes.length - 1;
+ },
+ destroyArtifact(id) {
+ this.deletingArtifactId = id;
+ this.$apollo
+ .mutate({
+ mutation: destroyArtifactMutation,
+ variables: { id },
+ update: (store) => {
+ removeArtifactFromStore(store, id, getJobArtifactsQuery, this.queryVariables);
+ },
+ })
+ .catch(() => {
+ createAlert({
+ message: I18N_DESTROY_ERROR,
+ });
+ this.$emit('refetch');
+ })
+ .finally(() => {
+ this.deletingArtifactId = null;
+ });
+ },
+ },
+ ARTIFACT_ROW_HEIGHT,
+};
+</script>
+<template>
+ <div :style="scrollContainerStyle">
+ <dynamic-scroller :items="artifacts.nodes" :min-item-size="$options.ARTIFACT_ROW_HEIGHT">
+ <template #default="{ item, index, active }">
+ <dynamic-scroller-item :item="item" :active="active" :class="{ active }">
+ <artifact-row
+ :artifact="item"
+ :is-last-row="isLastRow(index)"
+ :is-loading="item.id === deletingArtifactId"
+ @delete="destroyArtifact(item.id)"
+ />
+ </dynamic-scroller-item>
+ </template>
+ </dynamic-scroller>
+ </div>
+</template>
diff --git a/app/assets/javascripts/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/artifacts/components/job_artifacts_table.vue
new file mode 100644
index 00000000000..7b11e4f17f3
--- /dev/null
+++ b/app/assets/javascripts/artifacts/components/job_artifacts_table.vue
@@ -0,0 +1,314 @@
+<script>
+import {
+ GlLoadingIcon,
+ GlTable,
+ GlLink,
+ GlButtonGroup,
+ GlButton,
+ GlBadge,
+ GlIcon,
+ GlPagination,
+} from '@gitlab/ui';
+import { createAlert } from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import getJobArtifactsQuery from '../graphql/queries/get_job_artifacts.query.graphql';
+import { totalArtifactsSizeForJob, mapArchivesToJobNodes, mapBooleansToJobNodes } from '../utils';
+import {
+ STATUS_BADGE_VARIANTS,
+ I18N_DOWNLOAD,
+ I18N_BROWSE,
+ I18N_DELETE,
+ I18N_EXPIRED,
+ I18N_DESTROY_ERROR,
+ I18N_FETCH_ERROR,
+ I18N_ARTIFACTS,
+ I18N_JOB,
+ I18N_SIZE,
+ I18N_CREATED,
+ I18N_ARTIFACTS_COUNT,
+ INITIAL_CURRENT_PAGE,
+ INITIAL_PREVIOUS_PAGE_CURSOR,
+ INITIAL_NEXT_PAGE_CURSOR,
+ JOBS_PER_PAGE,
+ INITIAL_LAST_PAGE_SIZE,
+} from '../constants';
+import ArtifactsTableRowDetails from './artifacts_table_row_details.vue';
+
+const INITIAL_PAGINATION_STATE = {
+ currentPage: INITIAL_CURRENT_PAGE,
+ prevPageCursor: INITIAL_PREVIOUS_PAGE_CURSOR,
+ nextPageCursor: INITIAL_NEXT_PAGE_CURSOR,
+ firstPageSize: JOBS_PER_PAGE,
+ lastPageSize: INITIAL_LAST_PAGE_SIZE,
+};
+
+export default {
+ name: 'JobArtifactsTable',
+ components: {
+ GlLoadingIcon,
+ GlTable,
+ GlLink,
+ GlButtonGroup,
+ GlButton,
+ GlBadge,
+ GlIcon,
+ GlPagination,
+ CiIcon,
+ TimeAgo,
+ ArtifactsTableRowDetails,
+ },
+ inject: ['projectPath'],
+ apollo: {
+ jobArtifacts: {
+ query: getJobArtifactsQuery,
+ variables() {
+ return this.queryVariables;
+ },
+ update({ project: { jobs: { nodes = [], pageInfo = {}, count = 0 } = {} } }) {
+ return {
+ nodes: nodes.map(mapArchivesToJobNodes).map(mapBooleansToJobNodes),
+ count,
+ pageInfo,
+ };
+ },
+ error() {
+ createAlert({
+ message: I18N_FETCH_ERROR,
+ });
+ },
+ },
+ },
+ data() {
+ return {
+ jobArtifacts: {
+ nodes: [],
+ count: 0,
+ pageInfo: {},
+ },
+ pagination: INITIAL_PAGINATION_STATE,
+ };
+ },
+ computed: {
+ queryVariables() {
+ return {
+ projectPath: this.projectPath,
+ firstPageSize: this.pagination.firstPageSize,
+ lastPageSize: this.pagination.lastPageSize,
+ prevPageCursor: this.pagination.prevPageCursor,
+ nextPageCursor: this.pagination.nextPageCursor,
+ };
+ },
+ showPagination() {
+ return this.jobArtifacts.count > JOBS_PER_PAGE;
+ },
+ prevPage() {
+ return Number(this.jobArtifacts.pageInfo.hasPreviousPage);
+ },
+ nextPage() {
+ return Number(this.jobArtifacts.pageInfo.hasNextPage);
+ },
+ },
+ methods: {
+ refetchArtifacts() {
+ this.$apollo.queries.jobArtifacts.refetch();
+ },
+ artifactsSize(item) {
+ return totalArtifactsSizeForJob(item);
+ },
+ pipelineId(item) {
+ const id = getIdFromGraphQLId(item.pipeline.id);
+ return `#${id}`;
+ },
+ handlePageChange(page) {
+ const { startCursor, endCursor } = this.jobArtifacts.pageInfo;
+
+ if (page > this.pagination.currentPage) {
+ this.pagination = {
+ ...INITIAL_PAGINATION_STATE,
+ nextPageCursor: endCursor,
+ currentPage: page,
+ };
+ } else {
+ this.pagination = {
+ lastPageSize: JOBS_PER_PAGE,
+ firstPageSize: null,
+ prevPageCursor: startCursor,
+ currentPage: page,
+ };
+ }
+ },
+ handleRowToggle(toggleDetails, hasArtifacts) {
+ if (!hasArtifacts) return;
+ toggleDetails();
+ },
+ },
+ fields: [
+ {
+ key: 'artifacts',
+ label: I18N_ARTIFACTS,
+ thClass: 'gl-w-quarter',
+ },
+ {
+ key: 'job',
+ label: I18N_JOB,
+ thClass: 'gl-w-35p',
+ },
+ {
+ key: 'size',
+ label: I18N_SIZE,
+ thClass: 'gl-w-15p gl-text-right',
+ tdClass: 'gl-text-right',
+ },
+ {
+ key: 'created',
+ label: I18N_CREATED,
+ thClass: 'gl-w-eighth gl-text-center',
+ tdClass: 'gl-text-center',
+ },
+ {
+ key: 'actions',
+ label: '',
+ thClass: 'gl-w-eighth',
+ tdClass: 'gl-text-right',
+ },
+ ],
+ STATUS_BADGE_VARIANTS,
+ i18n: {
+ download: I18N_DOWNLOAD,
+ browse: I18N_BROWSE,
+ delete: I18N_DELETE,
+ expired: I18N_EXPIRED,
+ destroyArtifactError: I18N_DESTROY_ERROR,
+ fetchArtifactsError: I18N_FETCH_ERROR,
+ artifactsLabel: I18N_ARTIFACTS,
+ jobLabel: I18N_JOB,
+ sizeLabel: I18N_SIZE,
+ createdLabel: I18N_CREATED,
+ artifactsCount: I18N_ARTIFACTS_COUNT,
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-table
+ :items="jobArtifacts.nodes"
+ :fields="$options.fields"
+ :busy="$apollo.queries.jobArtifacts.loading"
+ stacked="sm"
+ details-td-class="gl-bg-gray-10! gl-p-0! gl-overflow-auto"
+ >
+ <template #table-busy>
+ <gl-loading-icon size="lg" />
+ </template>
+ <template
+ #cell(artifacts)="{ item: { artifacts, hasArtifacts }, toggleDetails, detailsShowing }"
+ >
+ <span
+ :class="{ 'gl-cursor-pointer': hasArtifacts }"
+ data-testid="job-artifacts-count"
+ @click="handleRowToggle(toggleDetails, hasArtifacts)"
+ >
+ <gl-icon
+ v-if="hasArtifacts"
+ :name="detailsShowing ? 'chevron-down' : 'chevron-right'"
+ class="gl-mr-2"
+ />
+ <strong>
+ {{ $options.i18n.artifactsCount(artifacts.nodes.length) }}
+ </strong>
+ </span>
+ </template>
+ <template #cell(job)="{ item }">
+ <span class="gl-display-inline-flex gl-align-items-center gl-w-full gl-mb-4">
+ <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>
+ </span>
+ <gl-link :href="item.webPath" class="gl-font-weight-bold">
+ {{ item.name }}
+ </gl-link>
+ </span>
+ <span class="gl-display-inline-flex">
+ <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"
+ >
+ {{ 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>
+ </template>
+ <template #cell(size)="{ item }">
+ <span data-testid="job-artifacts-size">{{ artifactsSize(item) }}</span>
+ </template>
+ <template #cell(created)="{ item }">
+ <time-ago data-testid="job-artifacts-created" :time="item.finishedAt" />
+ </template>
+ <template #cell(actions)="{ item }">
+ <gl-button-group>
+ <gl-button
+ icon="download"
+ :disabled="!item.archive.downloadPath"
+ :href="item.archive.downloadPath"
+ :title="$options.i18n.download"
+ :aria-label="$options.i18n.download"
+ data-testid="job-artifacts-download-button"
+ />
+ <gl-button
+ icon="folder-open"
+ :title="$options.i18n.browse"
+ :aria-label="$options.i18n.browse"
+ data-testid="job-artifacts-browse-button"
+ disabled
+ />
+ <gl-button
+ icon="remove"
+ :title="$options.i18n.delete"
+ :aria-label="$options.i18n.delete"
+ data-testid="job-artifacts-delete-button"
+ disabled
+ />
+ </gl-button-group>
+ </template>
+ <template #row-details="{ item: { artifacts } }">
+ <artifacts-table-row-details
+ :artifacts="artifacts"
+ :query-variables="queryVariables"
+ @refetch="refetchArtifacts"
+ />
+ </template>
+ </gl-table>
+ <gl-pagination
+ v-if="showPagination"
+ :value="pagination.currentPage"
+ :prev-page="prevPage"
+ :next-page="nextPage"
+ align="center"
+ class="gl-mt-3"
+ @input="handlePageChange"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/artifacts/constants.js b/app/assets/javascripts/artifacts/constants.js
new file mode 100644
index 00000000000..9ed0821ac2d
--- /dev/null
+++ b/app/assets/javascripts/artifacts/constants.js
@@ -0,0 +1,47 @@
+import { __, s__, n__ } from '~/locale';
+
+export const JOB_STATUS_GROUP_SUCCESS = 'success';
+
+export const STATUS_BADGE_VARIANTS = {
+ success: 'success',
+ passed: 'success',
+ error: 'danger',
+ failed: 'danger',
+ pending: 'warning',
+ 'waiting-for-resource': 'warning',
+ 'failed-with-warnings': 'warning',
+ 'success-with-warnings': 'warning',
+ running: 'info',
+ canceled: 'neutral',
+ disabled: 'neutral',
+ scheduled: 'neutral',
+ manual: 'neutral',
+ notification: 'muted',
+ preparing: 'muted',
+ created: 'muted',
+ skipped: 'muted',
+ notfound: 'muted',
+};
+
+export const I18N_DOWNLOAD = __('Download');
+export const I18N_BROWSE = s__('Artifacts|Browse');
+export const I18N_DELETE = __('Delete');
+export const I18N_EXPIRED = __('Expired');
+export const I18N_DESTROY_ERROR = s__('Artifacts|An error occurred while deleting the artifact');
+export const I18N_FETCH_ERROR = s__('Artifacts|An error occurred while retrieving job artifacts');
+export const I18N_ARTIFACTS = __('Artifacts');
+export const I18N_JOB = __('Job');
+export const I18N_SIZE = __('Size');
+export const I18N_CREATED = __('Created');
+export const I18N_ARTIFACTS_COUNT = (count) => n__('%d file', '%d files', count);
+
+export const INITIAL_CURRENT_PAGE = 1;
+export const INITIAL_PREVIOUS_PAGE_CURSOR = '';
+export const INITIAL_NEXT_PAGE_CURSOR = '';
+export const JOBS_PER_PAGE = 20;
+export const INITIAL_LAST_PAGE_SIZE = null;
+
+export const ARCHIVE_FILE_TYPE = 'ARCHIVE';
+
+export const ARTIFACT_ROW_HEIGHT = 56;
+export const ARTIFACTS_SHOWN_WITHOUT_SCROLLING = 4;
diff --git a/app/assets/javascripts/artifacts/graphql/cache_update.js b/app/assets/javascripts/artifacts/graphql/cache_update.js
new file mode 100644
index 00000000000..c620e03c80d
--- /dev/null
+++ b/app/assets/javascripts/artifacts/graphql/cache_update.js
@@ -0,0 +1,30 @@
+import produce from 'immer';
+
+export const hasErrors = ({ errors = [] }) => errors?.length;
+
+export function removeArtifactFromStore(store, deletedArtifactId, query, variables) {
+ if (!hasErrors(deletedArtifactId)) {
+ const sourceData = store.readQuery({
+ query,
+ variables,
+ });
+
+ const data = produce(sourceData, (draftData) => {
+ draftData.project.jobs.nodes = draftData.project.jobs.nodes.map((jobNode) => {
+ return {
+ ...jobNode,
+ artifacts: {
+ ...jobNode.artifacts,
+ nodes: jobNode.artifacts.nodes.filter(({ id }) => id !== deletedArtifactId),
+ },
+ };
+ });
+ });
+
+ store.writeQuery({
+ query,
+ variables,
+ data,
+ });
+ }
+}
diff --git a/app/assets/javascripts/artifacts/graphql/mutations/destroy_artifact.mutation.graphql b/app/assets/javascripts/artifacts/graphql/mutations/destroy_artifact.mutation.graphql
new file mode 100644
index 00000000000..529224b47e6
--- /dev/null
+++ b/app/assets/javascripts/artifacts/graphql/mutations/destroy_artifact.mutation.graphql
@@ -0,0 +1,7 @@
+mutation destroyArtifact($id: CiJobArtifactID!) {
+ artifactDestroy(input: { id: $id }) {
+ artifact {
+ id
+ }
+ }
+}
diff --git a/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql b/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql
new file mode 100644
index 00000000000..685196e28d5
--- /dev/null
+++ b/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql
@@ -0,0 +1,56 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+
+query getJobArtifacts(
+ $projectPath: ID!
+ $firstPageSize: Int
+ $lastPageSize: Int
+ $prevPageCursor: String = ""
+ $nextPageCursor: String = ""
+) {
+ project(fullPath: $projectPath) {
+ id
+ jobs(
+ statuses: [SUCCESS, FAILED]
+ first: $firstPageSize
+ last: $lastPageSize
+ after: $nextPageCursor
+ before: $prevPageCursor
+ ) {
+ count
+ nodes {
+ id
+ name
+ webPath
+ detailedStatus {
+ id
+ group
+ icon
+ label
+ }
+ pipeline {
+ id
+ iid
+ path
+ }
+ refName
+ refPath
+ shortSha
+ commitPath
+ finishedAt
+ artifacts {
+ nodes {
+ id
+ name
+ fileType
+ downloadPath
+ size
+ expireAt
+ }
+ }
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/artifacts/index.js b/app/assets/javascripts/artifacts/index.js
new file mode 100644
index 00000000000..b5146e0f0e9
--- /dev/null
+++ b/app/assets/javascripts/artifacts/index.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import JobArtifactsTable from './components/job_artifacts_table.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export const initArtifactsTable = () => {
+ const el = document.querySelector('#js-artifact-management');
+
+ if (!el) {
+ return false;
+ }
+
+ const { projectPath } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ projectPath,
+ },
+ render: (createElement) => createElement(JobArtifactsTable),
+ });
+};
diff --git a/app/assets/javascripts/artifacts/utils.js b/app/assets/javascripts/artifacts/utils.js
new file mode 100644
index 00000000000..ebcf0af8d2a
--- /dev/null
+++ b/app/assets/javascripts/artifacts/utils.js
@@ -0,0 +1,26 @@
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { ARCHIVE_FILE_TYPE, JOB_STATUS_GROUP_SUCCESS } from './constants';
+
+export const totalArtifactsSizeForJob = (job) =>
+ numberToHumanSize(
+ job.artifacts.nodes
+ .map((artifact) => artifact.size)
+ .reduce((total, artifact) => total + artifact, 0),
+ );
+
+export const mapArchivesToJobNodes = (jobNode) => {
+ return {
+ archive: {
+ ...jobNode.artifacts.nodes.find((artifact) => artifact.fileType === ARCHIVE_FILE_TYPE),
+ },
+ ...jobNode,
+ };
+};
+
+export const mapBooleansToJobNodes = (jobNode) => {
+ return {
+ succeeded: jobNode.detailedStatus.group === JOB_STATUS_GROUP_SUCCESS,
+ hasArtifacts: jobNode.artifacts.nodes.length > 0,
+ ...jobNode,
+ };
+};