diff options
18 files changed, 200 insertions, 53 deletions
diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml index dabf7cd9a95..bd650e7d6a2 100644 --- a/.haml-lint_todo.yml +++ b/.haml-lint_todo.yml @@ -289,7 +289,6 @@ linters: - "app/views/shared/issuable/_search_bar.html.haml" - "app/views/shared/issuable/_sidebar.html.haml" - "app/views/shared/issuable/form/_default_templates.html.haml" - - "app/views/shared/issuable/form/_issue_assignee.html.haml" - "app/views/shared/issuable/form/_template_selector.html.haml" - "app/views/shared/issuable/form/_title.html.haml" - "app/views/shared/labels/_form.html.haml" diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue index c42295792f1..ef160e954e9 100644 --- a/app/assets/javascripts/boards/components/board_content.vue +++ b/app/assets/javascripts/boards/components/board_content.vue @@ -1,13 +1,14 @@ <script> import { mapState } from 'vuex'; +import { GlAlert } from '@gitlab/ui'; import BoardColumn from 'ee_else_ce/boards/components/board_column.vue'; -import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { components: { BoardColumn, - EpicsSwimlanes, + EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'), + GlAlert, }, mixins: [glFeatureFlagMixin()], props: { @@ -42,7 +43,7 @@ export default { }, }, computed: { - ...mapState(['isShowingEpicsSwimlanes', 'boardLists']), + ...mapState(['isShowingEpicsSwimlanes', 'boardLists', 'error']), isSwimlanesOn() { return this.glFeatures.boardsWithSwimlanes && this.isShowingEpicsSwimlanes; }, @@ -52,6 +53,9 @@ export default { <template> <div> + <gl-alert v-if="error" variant="danger" :dismissible="false"> + {{ error }} + </gl-alert> <div v-if="!isSwimlanesOn" class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap" diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index ca9b911ce5b..1644c118868 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -1,4 +1,5 @@ import * as mutationTypes from './mutation_types'; +import { __ } from '~/locale'; const notImplemented = () => { /* eslint-disable-next-line @gitlab/require-i18n-strings */ @@ -62,7 +63,7 @@ export default { }, [mutationTypes.RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE]: state => { - state.listIssueFetchFailure = true; + state.error = __('An error occurred while fetching the board issues. Please reload the page.'); state.isLoadingIssues = false; }, diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js index cb6930774ed..fde9e62149c 100644 --- a/app/assets/javascripts/boards/stores/state.js +++ b/app/assets/javascripts/boards/stores/state.js @@ -5,7 +5,10 @@ export default () => ({ boardType: null, isShowingLabels: true, activeId: inactiveId, + boardLists: [], issuesByListId: {}, isLoadingIssues: false, - listIssueFetchFailure: false, + error: undefined, + // TODO: remove after ce/ee split of board_content.vue + isShowingEpicsSwimlanes: false, }); diff --git a/app/assets/javascripts/graphql_shared/fragments/epic.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/epic.fragment.graphql new file mode 100644 index 00000000000..286ebbd019e --- /dev/null +++ b/app/assets/javascripts/graphql_shared/fragments/epic.fragment.graphql @@ -0,0 +1,10 @@ +fragment EpicNode on Epic { + id + iid + title + state + reference + webUrl + createdAt + closedAt +} diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue index 712ac5eb0e5..4a9688e0eab 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue @@ -1,8 +1,8 @@ <script> import { GlDeprecatedButton, GlProgressBar } from '@gitlab/ui'; import { __ } from '~/locale'; -import { formatTime, secondsToMilliseconds } from '~/lib/utils/datetime_utility'; import Icon from '~/vue_shared/components/icon.vue'; +import { formattedTime } from '../../stores/test_reports/utils'; export default { name: 'TestSummary', @@ -39,7 +39,7 @@ export default { return 0; }, formattedDuration() { - return formatTime(secondsToMilliseconds(this.report.total_time)); + return formattedTime(this.report.total_time); }, progressBarVariant() { if (this.successPercentage < 33) { diff --git a/app/assets/javascripts/pipelines/stores/test_reports/utils.js b/app/assets/javascripts/pipelines/stores/test_reports/utils.js index 16fa6935cbe..b6d533fc8f1 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/utils.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/utils.js @@ -1,5 +1,5 @@ import { TestStatus } from '~/pipelines/constants'; -import { formatTime, secondsToMilliseconds } from '~/lib/utils/datetime_utility'; +import { __, sprintf } from '../../../locale'; export function iconForTestStatus(status) { switch (status) { @@ -12,7 +12,13 @@ export function iconForTestStatus(status) { } } -export const formattedTime = timeInSeconds => formatTime(secondsToMilliseconds(timeInSeconds)); +export const formattedTime = (seconds = 0) => { + if (seconds < 1) { + const milliseconds = seconds * 1000; + return sprintf(__('%{milliseconds}ms'), { milliseconds: milliseconds.toFixed(2) }); + } + return sprintf(__('%{seconds}s'), { seconds: seconds.toFixed(2) }); +}; export const addIconStatus = testCase => ({ ...testCase, diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index db05da0bb7f..5ed35094476 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -9,6 +9,7 @@ class Projects::BoardsController < Projects::ApplicationController before_action :assign_endpoint_vars before_action do push_frontend_feature_flag(:multi_select_board, default_enabled: true) + push_frontend_feature_flag(:boards_with_swimlanes, project, default_enabled: false) end private diff --git a/app/views/shared/issuable/form/_issue_assignee.html.haml b/app/views/shared/issuable/form/_issue_assignee.html.haml deleted file mode 100644 index 0d13fccaf3e..00000000000 --- a/app/views/shared/issuable/form/_issue_assignee.html.haml +++ /dev/null @@ -1,31 +0,0 @@ -- issue = issuable -- assignees = issue.assignees -.block.assignee - .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee_list) } - - if assignees.any? - - assignees.each do |assignee| - = link_to_member(@project, assignee, size: 24) - - else - = icon('user', 'aria-hidden': 'true') - .title.hide-collapsed - Assignee - = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - - if can_edit_issuable - = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link float-right' - .value.hide-collapsed - - if assignees.any? - - assignees.each do |assignee| - = link_to_member(@project, assignee, size: 32, extra_class: 'bold') do - %span.username - = assignee.to_reference - - else - %span.assign-yourself.no-value - No assignee - - if can_edit_issuable - \- - %a.js-assign-yourself{ href: '#' } - assign yourself - - .selectbox.hide-collapsed - = f.hidden_field 'assignee_ids', value: issuable.assignee_ids, id: 'issue_assignee_ids' - = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }) diff --git a/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml deleted file mode 100644 index 60c34094108..00000000000 --- a/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -= form.label :assignee_id, "Assignee", class: "col-form-label #{has_due_date ? "col-lg-4" : "col-sm-2"}" -.col-sm-10{ class: ("col-lg-8" if has_due_date) } - .issuable-form-select-holder - = form.hidden_field :assignee_id - - = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", - placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) - = link_to 'Assign to me', '#', class: "assign-to-me-link qa-assign-to-me-link #{'hide' if issuable.assignee_id == current_user.id}" diff --git a/changelogs/unreleased/213101-junit-report-times.yml b/changelogs/unreleased/213101-junit-report-times.yml new file mode 100644 index 00000000000..b9e295f5594 --- /dev/null +++ b/changelogs/unreleased/213101-junit-report-times.yml @@ -0,0 +1,5 @@ +--- +title: Adjust format for JUnit report duration times +merge_request: 39644 +author: +type: changed diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 055a3e7511d..7802342eb0b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -551,6 +551,9 @@ msgstr "" msgid "%{milestone} (expired)" msgstr "" +msgid "%{milliseconds}ms" +msgstr "" + msgid "%{mrText}, this issue will be closed automatically." msgstr "" @@ -663,6 +666,9 @@ msgstr "" msgid "%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}" msgstr "" +msgid "%{seconds}s" +msgstr "" + msgid "%{securityScanner} is not enabled for this project. %{linkStart}More information%{linkEnd}" msgid_plural "%{securityScanner} are not enabled for this project. %{linkStart}More information%{linkEnd}" msgstr[0] "" @@ -2657,9 +2663,15 @@ msgstr "" msgid "An error occurred while fetching the Service Desk address." msgstr "" +msgid "An error occurred while fetching the board issues. Please reload the page." +msgstr "" + msgid "An error occurred while fetching the board lists. Please try again." msgstr "" +msgid "An error occurred while fetching the board swimlanes. Please reload the page." +msgstr "" + msgid "An error occurred while fetching the builds." msgstr "" diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js new file mode 100644 index 00000000000..defd195c6fd --- /dev/null +++ b/spec/frontend/boards/components/board_content_spec.js @@ -0,0 +1,66 @@ +import Vuex from 'vuex'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { GlAlert } from '@gitlab/ui'; +import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue'; +import BoardColumn from 'ee_else_ce/boards/components/board_column.vue'; +import { mockListsWithModel } from '../mock_data'; +import BoardContent from '~/boards/components/board_content.vue'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('BoardContent', () => { + let wrapper; + + const defaultState = { + isShowingEpicsSwimlanes: false, + boardLists: mockListsWithModel, + error: undefined, + }; + + const createStore = (state = defaultState) => { + return new Vuex.Store({ + state, + actions: { + fetchIssuesForAllLists: () => {}, + }, + }); + }; + + const createComponent = state => { + const store = createStore({ + ...defaultState, + ...state, + }); + wrapper = shallowMount(BoardContent, { + localVue, + propsData: { + lists: mockListsWithModel, + canAdminList: true, + groupId: 1, + disabled: false, + issueLinkBase: '/', + rootPath: '/', + boardId: '1', + }, + store, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders a BoardColumn component per list', () => { + expect(wrapper.findAll(BoardColumn)).toHaveLength(mockListsWithModel.length); + }); + + it('does not display EpicsSwimlanes component', () => { + expect(wrapper.contains(EpicsSwimlanes)).toBe(false); + expect(wrapper.contains(GlAlert)).toBe(false); + }); +}); diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index 8ef6efe23c7..7461e9dc0e9 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -1,3 +1,5 @@ +import Vue from 'vue'; +import List from '~/boards/models/list'; import boardsStore from '~/boards/stores/boards_store'; export const boardObj = { @@ -165,3 +167,36 @@ export const setMockEndpoints = (opts = {}) => { boardId, }); }; + +export const mockLists = [ + { + id: 'gid://gitlab/List/1', + title: 'Backlog', + position: null, + listType: 'backlog', + collapsed: false, + label: null, + assignee: null, + milestone: null, + }, + { + id: 'gid://gitlab/List/2', + title: 'To Do', + position: 0, + listType: 'label', + collapsed: false, + label: { + id: 'gid://gitlab/GroupLabel/121', + title: 'To Do', + color: '#F0AD4E', + textColor: '#FFFFFF', + description: null, + }, + assignee: null, + milestone: null, + }, +]; + +export const mockListsWithModel = mockLists.map(listMock => + Vue.observable(new List({ ...listMock, doNotFetchIssues: true })), +); diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index c1f7f3dda6e..cf2f86f1e34 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -113,6 +113,23 @@ describe('Board Store Mutations', () => { expectNotImplemented(mutations.REQUEST_ADD_ISSUE); }); + describe('RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE', () => { + it('sets isLoadingIssues to false and sets error message', () => { + state = { + ...state, + isLoadingIssues: true, + error: undefined, + }; + + mutations.RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE(state); + + expect(state.isLoadingIssues).toBe(false); + expect(state.error).toEqual( + 'An error occurred while fetching the board issues. Please reload the page.', + ); + }); + }); + describe('RECEIVE_ADD_ISSUE_SUCCESS', () => { expectNotImplemented(mutations.RECEIVE_ADD_ISSUE_SUCCESS); }); diff --git a/spec/frontend/pipelines/test_reports/stores/getters_spec.js b/spec/frontend/pipelines/test_reports/stores/getters_spec.js index ca9ebb54138..58e8065033f 100644 --- a/spec/frontend/pipelines/test_reports/stores/getters_spec.js +++ b/spec/frontend/pipelines/test_reports/stores/getters_spec.js @@ -1,6 +1,6 @@ import { getJSONFixture } from 'helpers/fixtures'; import * as getters from '~/pipelines/stores/test_reports/getters'; -import { iconForTestStatus } from '~/pipelines/stores/test_reports/utils'; +import { iconForTestStatus, formattedTime } from '~/pipelines/stores/test_reports/utils'; describe('Getters TestReports Store', () => { let state; @@ -34,7 +34,7 @@ describe('Getters TestReports Store', () => { const suites = getters.getTestSuites(state); const expected = testReports.test_suites.map(x => ({ ...x, - formattedTime: '00:00:00', + formattedTime: formattedTime(x.total_time), })); expect(suites).toEqual(expected); @@ -65,7 +65,7 @@ describe('Getters TestReports Store', () => { const cases = getters.getSuiteTests(state); const expected = testReports.test_suites[0].test_cases.map(x => ({ ...x, - formattedTime: '00:00:00', + formattedTime: formattedTime(x.execution_time), icon: iconForTestStatus(x.status), })); diff --git a/spec/frontend/pipelines/test_reports/stores/utils_spec.js b/spec/frontend/pipelines/test_reports/stores/utils_spec.js new file mode 100644 index 00000000000..7e632d099fc --- /dev/null +++ b/spec/frontend/pipelines/test_reports/stores/utils_spec.js @@ -0,0 +1,26 @@ +import { formattedTime } from '~/pipelines/stores/test_reports/utils'; + +describe('Test reports utils', () => { + describe('formattedTime', () => { + describe('when time is smaller than a second', () => { + it('should return time in milliseconds fixed to 2 decimals', () => { + const result = formattedTime(0.4815162342); + expect(result).toBe('481.52ms'); + }); + }); + + describe('when time is equal to a second', () => { + it('should return time in seconds fixed to 2 decimals', () => { + const result = formattedTime(1); + expect(result).toBe('1.00s'); + }); + }); + + describe('when time is greater than a second', () => { + it('should return time in seconds fixed to 2 decimals', () => { + const result = formattedTime(4.815162342); + expect(result).toBe('4.82s'); + }); + }); + }); +}); diff --git a/spec/frontend/pipelines/test_reports/test_summary_spec.js b/spec/frontend/pipelines/test_reports/test_summary_spec.js index 79be6c168cf..dc5af7b160c 100644 --- a/spec/frontend/pipelines/test_reports/test_summary_spec.js +++ b/spec/frontend/pipelines/test_reports/test_summary_spec.js @@ -1,6 +1,7 @@ import { mount } from '@vue/test-utils'; import { getJSONFixture } from 'helpers/fixtures'; import Summary from '~/pipelines/components/test_reports/test_summary.vue'; +import { formattedTime } from '~/pipelines/stores/test_reports/utils'; describe('Test reports summary', () => { let wrapper; @@ -76,7 +77,7 @@ describe('Test reports summary', () => { }); it('displays the correctly formatted duration', () => { - expect(duration().text()).toBe('00:00:00'); + expect(duration().text()).toBe(formattedTime(testSuite.total_time)); }); }); |