From cb56015ca1ed1bb4bab8a911396aa1a7c6c6d4f6 Mon Sep 17 00:00:00 2001 From: Stanislav Lashmanov <slashmanov@gitlab.com> Date: Wed, 12 Mar 2025 12:28:31 +0400 Subject: [PATCH] Display diff stats on Rapid Diffs --- .../diffs/components/diff_app_controls.vue | 2 +- .../diffs/components/diff_stats.vue | 2 +- .../javascripts/rapid_diffs/app/index.js | 20 +++++++++++--- .../rapid_diffs/app/init_file_browser.js | 7 +---- .../rapid_diffs/app/view_settings.js | 11 +++++++- .../rapid_diffs/stores/diffs_view.js | 17 ++++++++++++ .../rapid_diffs/app_component.html.haml | 4 +-- locale/gitlab.pot | 3 +++ .../rapid_diffs/app_component_spec.rb | 2 +- spec/frontend/rapid_diffs/app/app_spec.js | 18 ++++++++++--- .../rapid_diffs/app/init_file_browser_spec.js | 14 ++-------- .../rapid_diffs/app/view_settings_spec.js | 11 ++++++++ .../rapid_diffs/stores/diffs_view_spec.js | 26 +++++++++++++++++++ 13 files changed, 107 insertions(+), 30 deletions(-) diff --git a/app/assets/javascripts/diffs/components/diff_app_controls.vue b/app/assets/javascripts/diffs/components/diff_app_controls.vue index 22935806c908b..30364fb6ab5a9 100644 --- a/app/assets/javascripts/diffs/components/diff_app_controls.vue +++ b/app/assets/javascripts/diffs/components/diff_app_controls.vue @@ -24,7 +24,7 @@ export default { required: true, }, diffsCount: { - type: String, + type: [String, Number], default: '', required: false, }, diff --git a/app/assets/javascripts/diffs/components/diff_stats.vue b/app/assets/javascripts/diffs/components/diff_stats.vue index ba97f0d5b2167..0dfb998630d63 100644 --- a/app/assets/javascripts/diffs/components/diff_stats.vue +++ b/app/assets/javascripts/diffs/components/diff_stats.vue @@ -21,7 +21,7 @@ export default { required: true, }, diffsCount: { - type: String, + type: [String, Number], required: false, default: null, }, diff --git a/app/assets/javascripts/rapid_diffs/app/index.js b/app/assets/javascripts/rapid_diffs/app/index.js index 53e63859f551f..81571e1e28241 100644 --- a/app/assets/javascripts/rapid_diffs/app/index.js +++ b/app/assets/javascripts/rapid_diffs/app/index.js @@ -5,6 +5,9 @@ import { DiffFileMounted } from '~/rapid_diffs/diff_file_mounted'; import { useDiffsList } from '~/rapid_diffs/stores/diffs_list'; import { initFileBrowser } from '~/rapid_diffs/app/init_file_browser'; import { StreamingError } from '~/rapid_diffs/streaming_error'; +import { useDiffsView } from '~/rapid_diffs/stores/diffs_view'; +import { createAlert } from '~/alert'; +import { __ } from '~/locale'; // This facade interface joins together all the bits and pieces of Rapid Diffs: DiffFile, Settings, File browser, etc. // It's a unified entrypoint for Rapid Diffs and all external communications should happen through this interface. @@ -15,9 +18,20 @@ class RapidDiffsFacade { init() { this.#registerCustomElements(); - const appElement = document.querySelector('[data-rapid-diffs]'); - initViewSettings({ pinia, streamUrl: appElement.dataset.reloadStreamUrl }); - initFileBrowser(); + const { reloadStreamUrl, metadataEndpoint } = + document.querySelector('[data-rapid-diffs]').dataset; + useDiffsView(pinia).metadataEndpoint = metadataEndpoint; + useDiffsView(pinia) + .loadMetadata() + .then(() => { + initFileBrowser(); + }) + .catch(() => { + createAlert({ + message: __('Failed to load additional diffs information. Try reloading the page.'), + }); + }); + initViewSettings({ pinia, streamUrl: reloadStreamUrl }); } // eslint-disable-next-line class-methods-use-this diff --git a/app/assets/javascripts/rapid_diffs/app/init_file_browser.js b/app/assets/javascripts/rapid_diffs/app/init_file_browser.js index f9ff48c8f6459..bee0bf06b4b2c 100644 --- a/app/assets/javascripts/rapid_diffs/app/init_file_browser.js +++ b/app/assets/javascripts/rapid_diffs/app/init_file_browser.js @@ -4,13 +4,8 @@ import { pinia } from '~/pinia/instance'; import { DiffFile } from '~/rapid_diffs/diff_file'; import FileBrowser from './file_browser.vue'; -export async function initFileBrowser() { +export function initFileBrowser() { const el = document.querySelector('[data-file-browser]'); - const { metadataEndpoint } = el.dataset; - - store.state.diffs.endpointMetadata = metadataEndpoint; - await store.dispatch('diffs/fetchDiffFilesMeta'); - // eslint-disable-next-line no-new new Vue({ el, diff --git a/app/assets/javascripts/rapid_diffs/app/view_settings.js b/app/assets/javascripts/rapid_diffs/app/view_settings.js index 3e1edf227c3db..819ad296308a0 100644 --- a/app/assets/javascripts/rapid_diffs/app/view_settings.js +++ b/app/assets/javascripts/rapid_diffs/app/view_settings.js @@ -21,7 +21,13 @@ const initSettingsApp = (el, pinia) => { pinia, computed: { ...mapState(useDiffsList, ['isLoading', 'isEmpty']), - ...mapState(useDiffsView, ['showWhitespace', 'viewType', 'fileByFileMode', 'singleFileMode']), + ...mapState(useDiffsView, [ + 'showWhitespace', + 'viewType', + 'fileByFileMode', + 'singleFileMode', + 'diffStats', + ]), }, methods: { ...mapActions(useDiffsView, ['updateViewType', 'updateShowWhitespace']), @@ -34,6 +40,9 @@ const initSettingsApp = (el, pinia) => { diffViewType: this.viewType, viewDiffsFileByFile: this.singleFileMode, isLoading: this.isLoading, + addedLines: this.diffStats?.addedLines, + removedLines: this.diffStats?.removedLines, + diffsCount: this.diffStats?.diffsCount, }, on: { updateDiffViewType: this.updateViewType, diff --git a/app/assets/javascripts/rapid_diffs/stores/diffs_view.js b/app/assets/javascripts/rapid_diffs/stores/diffs_view.js index edb64bd2ff5ff..2773624d7e124 100644 --- a/app/assets/javascripts/rapid_diffs/stores/diffs_view.js +++ b/app/assets/javascripts/rapid_diffs/stores/diffs_view.js @@ -11,6 +11,7 @@ import { queueRedisHllEvents } from '~/diffs/utils/queue_events'; import { mergeUrlParams } from '~/lib/utils/url_utility'; import axios from '~/lib/utils/axios_utils'; import { useDiffsList } from '~/rapid_diffs/stores/diffs_list'; +import store from '~/mr_notes/stores'; export const useDiffsView = defineStore('diffsView', { state() { @@ -20,9 +21,24 @@ export const useDiffsView = defineStore('diffsView', { singleFileMode: false, updateUserEndpoint: undefined, streamUrl: undefined, + metadataEndpoint: undefined, + diffStats: null, }; }, actions: { + async loadMetadata() { + // TODO: refactor this to our own Pinia stores + store.state.diffs.endpointMetadata = this.metadataEndpoint; + store.state.diffs.diffViewType = this.viewType; + store.state.diffs.showWhitespace = this.showWhitespace; + await store.dispatch('diffs/fetchDiffFilesMeta'); + this.diffStats = { + addedLines: store.state.diffs.addedLines, + removedLines: store.state.diffs.removedLines, + // we will be using a number for that after refactoring + diffsCount: parseInt(store.state.diffs.realSize, 10), + }; + }, updateDiffView() { if (this.singleFileMode) { // TODO: implement single file mode @@ -47,6 +63,7 @@ export const useDiffsView = defineStore('diffsView', { // we don't have to wait for the setting to be saved since whitespace param is passed explicitly axios.put(this.updateUserEndpoint, { show_whitespace_in_diffs: value }); } + this.loadMetadata(); this.updateDiffView(); }, }, diff --git a/app/components/rapid_diffs/app_component.html.haml b/app/components/rapid_diffs/app_component.html.haml index b4ef595f53c3d..8995d3554dd4a 100644 --- a/app/components/rapid_diffs/app_component.html.haml +++ b/app/components/rapid_diffs/app_component.html.haml @@ -1,9 +1,9 @@ -.rd-app{ data: { rapid_diffs: true, reload_stream_url: @reload_stream_url } } +.rd-app{ data: { rapid_diffs: true, reload_stream_url: @reload_stream_url, metadata_endpoint: @metadata_endpoint } } .rd-app-header .rd-app-settings %div{ data: { view_settings: true, show_whitespace: @show_whitespace.to_json, diff_view_type: @diff_view, update_user_endpoint: @update_user_endpoint } } .rd-app-body - .rd-app-sidebar{ data: { file_browser: true, metadata_endpoint: @metadata_endpoint }, style: ("width: #{initial_sidebar_width}px" if initial_sidebar_width) } + .rd-app-sidebar{ data: { file_browser: true }, style: ("width: #{initial_sidebar_width}px" if initial_sidebar_width) } .rd-app-sidebar-loading = helpers.gl_loading_icon(size: 'sm') .rd-app-content{ data: { sidebar_visible: true } } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6c26122e45a11..0fce6fe0732e7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -24724,6 +24724,9 @@ msgstr "" msgid "Failed to load Roadmap" msgstr "" +msgid "Failed to load additional diffs information. Try reloading the page." +msgstr "" + msgid "Failed to load assignees. Please try again." msgstr "" diff --git a/spec/components/rapid_diffs/app_component_spec.rb b/spec/components/rapid_diffs/app_component_spec.rb index f49697c2ed3a6..b4ce774bd2b7d 100644 --- a/spec/components/rapid_diffs/app_component_spec.rb +++ b/spec/components/rapid_diffs/app_component_spec.rb @@ -21,6 +21,7 @@ app = page.find('[data-rapid-diffs]') expect(app).not_to be_nil expect(app['data-reload-stream-url']).to eq(reload_stream_url) + expect(app['data-metadata-endpoint']).to eq(metadata_endpoint) end it "renders view settings" do @@ -36,7 +37,6 @@ render_component container = page.find("[data-file-browser]") expect(container).not_to be_nil - expect(container['data-metadata-endpoint']).to eq(metadata_endpoint) end it "sets sidebar width" do diff --git a/spec/frontend/rapid_diffs/app/app_spec.js b/spec/frontend/rapid_diffs/app/app_spec.js index df04dd4d1be69..c689fec220209 100644 --- a/spec/frontend/rapid_diffs/app/app_spec.js +++ b/spec/frontend/rapid_diffs/app/app_spec.js @@ -8,7 +8,9 @@ import { useDiffsList } from '~/rapid_diffs/stores/diffs_list'; import { pinia } from '~/pinia/instance'; import { initFileBrowser } from '~/rapid_diffs/app/init_file_browser'; import { StreamingError } from '~/rapid_diffs/streaming_error'; +import { useDiffsView } from '~/rapid_diffs/stores/diffs_view'; +jest.mock('~/mr_notes/stores'); jest.mock('~/rapid_diffs/app/view_settings'); jest.mock('~/rapid_diffs/app/init_file_browser'); @@ -23,21 +25,31 @@ describe('Rapid Diffs App', () => { createTestingPinia(); setHTMLFixture( ` - <div data-rapid-diffs data-reload-stream-url="/reload"> + <div data-rapid-diffs data-reload-stream-url="/reload" data-metadata-endpoint="/metadata"> <div id="js-stream-container" data-diffs-stream-url="/stream"></div> </div> `, ); }); - it('initializes the app', () => { + it('initializes the app', async () => { + let res; + const mock = useDiffsView().loadMetadata.mockImplementationOnce( + () => + new Promise((resolve) => { + res = resolve; + }), + ); createApp(); app.init(); + expect(useDiffsView().metadataEndpoint).toBe('/metadata'); + expect(mock).toHaveBeenCalled(); expect(initViewSettings).toHaveBeenCalledWith({ pinia, streamUrl: '/reload' }); - expect(initFileBrowser).toHaveBeenCalled(); expect(window.customElements.get('diff-file')).toBe(DiffFile); expect(window.customElements.get('diff-file-mounted')).toBe(DiffFileMounted); expect(window.customElements.get('streaming-error')).toBe(StreamingError); + await res(); + expect(initFileBrowser).toHaveBeenCalled(); }); it('streams remaining diffs', () => { diff --git a/spec/frontend/rapid_diffs/app/init_file_browser_spec.js b/spec/frontend/rapid_diffs/app/init_file_browser_spec.js index 3fe7da030b862..00f0b5626dae0 100644 --- a/spec/frontend/rapid_diffs/app/init_file_browser_spec.js +++ b/spec/frontend/rapid_diffs/app/init_file_browser_spec.js @@ -1,5 +1,4 @@ import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures'; -import store from '~/mr_notes/stores'; import { initFileBrowser } from '~/rapid_diffs/app/init_file_browser'; import createEventHub from '~/helpers/event_hub_factory'; import waitForPromises from 'helpers/wait_for_promises'; @@ -22,13 +21,9 @@ jest.mock('~/rapid_diffs/app/file_browser.vue', () => ({ })); describe('Init file browser', () => { - let dispatch; - - const getMountElement = () => document.querySelector('[data-file-browser]'); const getFileBrowser = () => document.querySelector('[data-file-browser-component]'); beforeEach(() => { - dispatch = jest.spyOn(store, 'dispatch').mockResolvedValue(); window.mrTabs = { eventHub: createEventHub() }; setHTMLFixture( ` @@ -46,14 +41,9 @@ describe('Init file browser', () => { resetHTMLFixture(); }); - it('sets metadata endpoint', () => { - initFileBrowser(); - expect(store.state.diffs.endpointMetadata).toBe(getMountElement().dataset.metadataEndpoint); - }); - - it('fetches metadata', () => { + it('mounts the component', () => { initFileBrowser(); - expect(dispatch).toHaveBeenCalledWith('diffs/fetchDiffFilesMeta'); + expect(getFileBrowser()).not.toBe(null); }); it('handles file clicks', async () => { diff --git a/spec/frontend/rapid_diffs/app/view_settings_spec.js b/spec/frontend/rapid_diffs/app/view_settings_spec.js index b3c4696cabebf..865ed154d2b52 100644 --- a/spec/frontend/rapid_diffs/app/view_settings_spec.js +++ b/spec/frontend/rapid_diffs/app/view_settings_spec.js @@ -20,6 +20,9 @@ jest.mock('~/diffs/components/diff_app_controls.vue', () => ({ 'data-show-whitespace': JSON.stringify(this.showWhitespace), 'data-diff-view-type': JSON.stringify(this.diffViewType), 'data-is-loading': JSON.stringify(this.isLoading), + 'data-added-lines': JSON.stringify(this.addedLines), + 'data-removed-lines': JSON.stringify(this.removedLines), + 'data-diffs-count': JSON.stringify(this.diffsCount), }, }); }, @@ -68,6 +71,11 @@ describe('View settings', () => { it('sets diff app controls props', () => { useDiffsList().loadedFiles = { foo: true }; + useDiffsView().diffStats = { + addedLines: 1, + removedLines: 2, + diffsCount: 3, + }; initViewSettings({ pinia, streamUrl }); const el = getDiffAppControls(); const getProp = (prop) => JSON.parse(el.dataset[prop]); @@ -75,6 +83,9 @@ describe('View settings', () => { expect(getProp('showWhitespace')).toBe(true); expect(getProp('diffViewType')).toBe('parallel'); expect(getProp('isLoading')).toBe(false); + expect(getProp('addedLines')).toBe(1); + expect(getProp('removedLines')).toBe(2); + expect(getProp('diffsCount')).toBe(3); }); it('triggers collapse all files', () => { diff --git a/spec/frontend/rapid_diffs/stores/diffs_view_spec.js b/spec/frontend/rapid_diffs/stores/diffs_view_spec.js index 0c3b2cb45115e..5fde16c506ef2 100644 --- a/spec/frontend/rapid_diffs/stores/diffs_view_spec.js +++ b/spec/frontend/rapid_diffs/stores/diffs_view_spec.js @@ -14,6 +14,7 @@ import { import { queueRedisHllEvents } from '~/diffs/utils/queue_events'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import vuexStore from '~/mr_notes/stores'; const defaultState = { updateUserEndpoint: '/update', @@ -39,6 +40,31 @@ describe('Diffs view store', () => { setActivePinia(pinia); store = useDiffsView(); useDiffsList().reloadDiffs.mockResolvedValue(); + jest.spyOn(vuexStore, 'dispatch').mockResolvedValue(); + }); + + describe('#loadMetadata', () => { + it('uses Vuex store to load metadata', () => { + const spy = jest.spyOn(vuexStore, 'dispatch'); + store.metadataEndpoint = '/metadata'; + store.loadMetadata(); + expect(vuexStore.state.diffs.endpointMetadata).toBe('/metadata'); + expect(vuexStore.state.diffs.diffViewType).toBe('inline'); + expect(vuexStore.state.diffs.showWhitespace).toBe(true); + expect(spy).toHaveBeenCalledWith('diffs/fetchDiffFilesMeta'); + }); + + it('copies values from Vuex store for diff stats', async () => { + vuexStore.state.diffs.addedLines = 1; + vuexStore.state.diffs.removedLines = 2; + vuexStore.state.diffs.realSize = '3'; + await store.loadMetadata(); + expect(store.diffStats).toStrictEqual({ + addedLines: 1, + removedLines: 2, + diffsCount: 3, + }); + }); }); describe('#updateDiffView', () => { -- GitLab