From 279960cebe419f4801c2a8b939e243ecf7b80e81 Mon Sep 17 00:00:00 2001 From: mfluharty <mfluharty@gitlab.com> Date: Thu, 10 Nov 2022 17:59:16 -0700 Subject: [PATCH] Add filter by ref to artifacts management page Make main app component that displays search bar and table Modify graphql query to accept ref parameter --- .../javascripts/artifacts/components/app.vue | 30 ++++++++++ .../components/artifacts_filtered_search.vue | 44 +++++++++++++++ .../components/job_artifacts_table.vue | 7 +++ app/assets/javascripts/artifacts/constants.js | 2 + .../queries/get_job_artifacts.query.graphql | 2 + app/assets/javascripts/artifacts/index.js | 7 ++- app/views/projects/artifacts/index.html.haml | 2 +- locale/gitlab.pot | 3 + .../frontend/artifacts/components/app_spec.js | 41 ++++++++++++++ .../components/job_artifacts_table_spec.js | 55 ++++++++++++------- 10 files changed, 168 insertions(+), 25 deletions(-) create mode 100644 app/assets/javascripts/artifacts/components/app.vue create mode 100644 app/assets/javascripts/artifacts/components/artifacts_filtered_search.vue create mode 100644 spec/frontend/artifacts/components/app_spec.js diff --git a/app/assets/javascripts/artifacts/components/app.vue b/app/assets/javascripts/artifacts/components/app.vue new file mode 100644 index 0000000000000000..21f0f6581add4f83 --- /dev/null +++ b/app/assets/javascripts/artifacts/components/app.vue @@ -0,0 +1,30 @@ +<script> +import { BRANCH_TYPE } from '../constants'; +import ArtifactsFilteredSearch from './artifacts_filtered_search.vue'; +import JobArtifactsTable from './job_artifacts_table.vue'; + +export default { + name: 'ArtifactsApp', + components: { + ArtifactsFilteredSearch, + JobArtifactsTable, + }, + data() { + return { + filterByRef: '', + }; + }, + methods: { + filterArtifactsBySearch(filters) { + const branchFilter = filters.find((filter) => filter.type === BRANCH_TYPE); + this.filterByRef = branchFilter ? branchFilter.value.data : ''; + }, + }, +}; +</script> +<template> + <div> + <artifacts-filtered-search class="gl-mb-8" @filterArtifactsBySearch="filterArtifactsBySearch" /> + <job-artifacts-table :filter-by-ref="filterByRef" /> + </div> +</template> diff --git a/app/assets/javascripts/artifacts/components/artifacts_filtered_search.vue b/app/assets/javascripts/artifacts/components/artifacts_filtered_search.vue new file mode 100644 index 0000000000000000..18c8383c7cbfdeac --- /dev/null +++ b/app/assets/javascripts/artifacts/components/artifacts_filtered_search.vue @@ -0,0 +1,44 @@ +<script> +import { GlFilteredSearch } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants'; +import PipelineBranchNameToken from '~/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue'; +import { BRANCH_TYPE } from '../constants'; + +export default { + components: { + GlFilteredSearch, + }, + inject: ['projectId'], + computed: { + selectedTypes() { + return this.value.map((i) => i.type); + }, + tokens() { + return [ + { + type: BRANCH_TYPE, + icon: 'branch', + title: s__('Pipeline|Branch name'), + unique: true, + token: PipelineBranchNameToken, + operators: OPERATORS_IS, + projectId: this.projectId, + }, + ]; + }, + }, + methods: { + onSubmit(filters) { + this.$emit('filterArtifactsBySearch', filters); + }, + }, +}; +</script> +<template> + <gl-filtered-search + :placeholder="s__('Artifacts|Filter artifacts')" + :available-tokens="tokens" + @submit="onSubmit" + /> +</template> diff --git a/app/assets/javascripts/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/artifacts/components/job_artifacts_table.vue index 34e443f4e582e263..a2fd317725722625 100644 --- a/app/assets/javascripts/artifacts/components/job_artifacts_table.vue +++ b/app/assets/javascripts/artifacts/components/job_artifacts_table.vue @@ -60,6 +60,12 @@ export default { ArtifactsTableRowDetails, }, inject: ['projectPath'], + props: { + filterByRef: { + type: String, + required: true, + }, + }, apollo: { jobArtifacts: { query: getJobArtifactsQuery, @@ -101,6 +107,7 @@ export default { queryVariables() { return { projectPath: this.projectPath, + ref: this.filterByRef, firstPageSize: this.pagination.firstPageSize, lastPageSize: this.pagination.lastPageSize, prevPageCursor: this.pagination.prevPageCursor, diff --git a/app/assets/javascripts/artifacts/constants.js b/app/assets/javascripts/artifacts/constants.js index 5fcc4f2b76ea051e..cbe5130b4d2a8642 100644 --- a/app/assets/javascripts/artifacts/constants.js +++ b/app/assets/javascripts/artifacts/constants.js @@ -53,3 +53,5 @@ export const ARCHIVE_FILE_TYPE = 'ARCHIVE'; export const ARTIFACT_ROW_HEIGHT = 56; export const ARTIFACTS_SHOWN_WITHOUT_SCROLLING = 4; + +export const BRANCH_TYPE = 'ref'; 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 index 89a24d7891e7f85d..c9c76228bc477856 100644 --- a/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql +++ b/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql @@ -2,6 +2,7 @@ query getJobArtifacts( $projectPath: ID! + $ref: String $firstPageSize: Int $lastPageSize: Int $prevPageCursor: String = "" @@ -10,6 +11,7 @@ query getJobArtifacts( project(fullPath: $projectPath) { id jobs( + ref: $ref statuses: [SUCCESS, FAILED] first: $firstPageSize last: $lastPageSize diff --git a/app/assets/javascripts/artifacts/index.js b/app/assets/javascripts/artifacts/index.js index b5146e0f0e9f1e34..e653ca2cb3c6a276 100644 --- a/app/assets/javascripts/artifacts/index.js +++ b/app/assets/javascripts/artifacts/index.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; -import JobArtifactsTable from './components/job_artifacts_table.vue'; +import App from './components/app.vue'; Vue.use(VueApollo); @@ -16,14 +16,15 @@ export const initArtifactsTable = () => { return false; } - const { projectPath } = el.dataset; + const { projectPath, projectId } = el.dataset; return new Vue({ el, apolloProvider, provide: { projectPath, + projectId, }, - render: (createElement) => createElement(JobArtifactsTable), + render: (createElement) => createElement(App), }); }; diff --git a/app/views/projects/artifacts/index.html.haml b/app/views/projects/artifacts/index.html.haml index 9cbc149177c9d96c..d5aba872f6f15c0c 100644 --- a/app/views/projects/artifacts/index.html.haml +++ b/app/views/projects/artifacts/index.html.haml @@ -6,4 +6,4 @@ .gl-mb-6 %strong= s_('Artifacts|Total artifacts size') = number_to_human_size(@total_size, precicion: 2) - #js-artifact-management{ data: { "project-path" => @project.full_path } } + #js-artifact-management{ data: { "project-path" => @project.full_path, "project-id" => @project.id } } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a05204ad36244b0d..de8f599372a74a93 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5359,6 +5359,9 @@ msgstr "" msgid "Artifacts|Delete artifact" msgstr "" +msgid "Artifacts|Filter artifacts" +msgstr "" + msgid "Artifacts|This artifact will be permanently deleted. Any reports generated from this artifact will be empty." msgstr "" diff --git a/spec/frontend/artifacts/components/app_spec.js b/spec/frontend/artifacts/components/app_spec.js new file mode 100644 index 0000000000000000..72c805c790db4099 --- /dev/null +++ b/spec/frontend/artifacts/components/app_spec.js @@ -0,0 +1,41 @@ +import ArtifactsApp from '~/artifacts/components/app.vue'; +import ArtifactsFilteredSearch from '~/artifacts/components/artifacts_filtered_search.vue'; +import JobArtifactsTable from '~/artifacts/components/job_artifacts_table.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; + +describe('ArtifactsApp component', () => { + let wrapper; + + const findArtifactsFilteredSearch = () => wrapper.findComponent(ArtifactsFilteredSearch); + const findJobArtifactsTable = () => wrapper.findComponent(JobArtifactsTable); + + const createComponent = () => { + wrapper = shallowMountExtended(ArtifactsApp, { mocks: { ArtifactsFilteredSearch } }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + beforeEach(() => { + createComponent(); + }); + + it('shows the filtered search component', () => { + expect(findArtifactsFilteredSearch().exists()).toBe(true); + }); + + it('shows the job artifacts table', () => { + expect(findJobArtifactsTable().exists()).toBe(true); + }); + + it('passes the ref from the search component to the table component', async () => { + findArtifactsFilteredSearch().vm.$emit('filterArtifactsBySearch', [ + { type: 'ref', value: { data: 'main' } }, + ]); + await waitForPromises(); + + expect(findJobArtifactsTable().props('filterByRef')).toBe('main'); + }); +}); diff --git a/spec/frontend/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/artifacts/components/job_artifacts_table_spec.js index 131b4b99bb2fce61..919bc2b49a560e2b 100644 --- a/spec/frontend/artifacts/components/job_artifacts_table_spec.js +++ b/spec/frontend/artifacts/components/job_artifacts_table_spec.js @@ -74,12 +74,11 @@ describe('JobArtifactsTable component', () => { (artifact) => artifact.fileType === ARCHIVE_FILE_TYPE, ); - const createComponent = ( - handlers = { - getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse), - }, + const createComponent = ({ + handlers = { getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse) }, data = {}, - ) => { + props = { filterByRef: '' }, + } = {}) => { requestHandlers = handlers; wrapper = mountExtended(JobArtifactsTable, { apolloProvider: createMockApollo([ @@ -89,6 +88,7 @@ describe('JobArtifactsTable component', () => { data() { return data; }, + propsData: { ...props }, }); }; @@ -104,7 +104,7 @@ describe('JobArtifactsTable component', () => { it('on error, shows an alert', async () => { createComponent({ - getJobArtifactsQuery: jest.fn().mockRejectedValue(new Error('Error!')), + handlers: { getJobArtifactsQuery: jest.fn().mockRejectedValue(new Error('Error!')) }, }); await waitForPromises(); @@ -165,17 +165,17 @@ describe('JobArtifactsTable component', () => { describe('row expansion', () => { it('toggles the visibility of the row details', async () => { - expect(findDetailsRows().length).toBe(0); + expect(findDetailsRows()).toHaveLength(0); findCount().trigger('click'); await waitForPromises(); - expect(findDetailsRows().length).toBe(1); + expect(findDetailsRows()).toHaveLength(1); findCount().trigger('click'); await waitForPromises(); - expect(findDetailsRows().length).toBe(0); + expect(findDetailsRows()).toHaveLength(0); }); it('expands and collapses jobs', async () => { @@ -245,10 +245,10 @@ describe('JobArtifactsTable component', () => { archive: { downloadPath: null }, }; - createComponent( - { getJobArtifactsQuery: jest.fn() }, - { jobArtifacts: [jobWithoutDownloadPath] }, - ); + createComponent({ + handlers: { getJobArtifactsQuery: jest.fn() }, + data: { jobArtifacts: [jobWithoutDownloadPath] }, + }); await waitForPromises(); @@ -271,10 +271,10 @@ describe('JobArtifactsTable component', () => { browseArtifactsPath: null, }; - createComponent( - { getJobArtifactsQuery: jest.fn() }, - { jobArtifacts: [jobWithoutBrowsePath] }, - ); + createComponent({ + handlers: { getJobArtifactsQuery: jest.fn() }, + data: { jobArtifacts: [jobWithoutBrowsePath] }, + }); await waitForPromises(); @@ -292,19 +292,32 @@ describe('JobArtifactsTable component', () => { }); }); + describe('filtering', () => { + it('filters by ref prop', () => { + const artifactsQueryHandler = jest.fn(); + + createComponent({ + handlers: { getJobArtifactsQuery: artifactsQueryHandler }, + props: { filterByRef: 'dev' }, + }); + + expect(artifactsQueryHandler.mock.calls[0][0]).toMatchObject({ ref: 'dev' }); + }); + }); + describe('pagination', () => { const { pageInfo } = getJobArtifactsResponse.data.project.jobs; beforeEach(async () => { - createComponent( - { + createComponent({ + handlers: { getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponseThatPaginates), }, - { + data: { count: enoughJobsToPaginate.length, pageInfo, }, - ); + }); await waitForPromises(); }); -- GitLab