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