diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-28 21:08:37 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-28 21:08:37 +0300 |
commit | 1ec60cf53bc498b12c597ff0d91434a1bdb7cba9 (patch) | |
tree | f2c1b77f47cab1fb9fd11141337be67b12f2316b | |
parent | 1f1e53f43f87cada9b515571cc973e9eadcbc4e4 (diff) |
Add latest changes from gitlab-org/gitlab@master
49 files changed, 614 insertions, 468 deletions
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index e8087aebcef..b9c19b21ef3 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -46,7 +46,8 @@ static-analysis: stage: test variables: SETUP_DB: "false" - parallel: 2 + ENABLE_SPRING: "1" + parallel: 4 script: - scripts/static-analysis cache: diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index 46a281cd48f..d26685645f9 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -233,6 +233,7 @@ danger-review: - .review:rules:danger image: registry.gitlab.com/gitlab-org/gitlab-build-images:danger stage: test + allow_failure: true needs: [] script: - source scripts/utils.sh diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index 31a239092c2..1eb89b41495 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -9,7 +9,7 @@ import CommitForm from './commit_sidebar/form.vue'; import IdeReview from './ide_review.vue'; import SuccessMessage from './commit_sidebar/success_message.vue'; import IdeProjectHeader from './ide_project_header.vue'; -import { leftSidebarViews, LEFT_SIDEBAR_INIT_WIDTH } from '../constants'; +import { leftSidebarViews, SIDEBAR_INIT_WIDTH } from '../constants'; export default { components: { @@ -33,13 +33,13 @@ export default { ); }, }, - LEFT_SIDEBAR_INIT_WIDTH, + SIDEBAR_INIT_WIDTH, }; </script> <template> <resizable-panel - :initial-width="$options.LEFT_SIDEBAR_INIT_WIDTH" + :initial-width="$options.SIDEBAR_INIT_WIDTH" side="left" class="multi-file-commit-panel flex-column" > diff --git a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue index 2814844d157..4e8e1e3a470 100644 --- a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue +++ b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue @@ -2,7 +2,6 @@ import { mapActions, mapState } from 'vuex'; import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; -import ResizablePanel from '../resizable_panel.vue'; import IdeSidebarNav from '../ide_sidebar_nav.vue'; export default { @@ -12,7 +11,6 @@ export default { }, components: { Icon, - ResizablePanel, IdeSidebarNav, }, props: { @@ -25,10 +23,6 @@ export default { type: String, required: true, }, - width: { - type: Number, - required: true, - }, }, computed: { ...mapState({ @@ -75,25 +69,20 @@ export default { :data-qa-selector="`ide_${side}_sidebar`" class="multi-file-commit-panel ide-sidebar" > - <resizable-panel + <div v-show="isOpen" - :initial-width="width" - :min-size="width" :class="`ide-${side}-sidebar-${currentView}`" - :side="side" class="multi-file-commit-panel-inner" > - <div class="h-100 d-flex flex-column align-items-stretch"> - <div - v-for="tabView in aliveTabViews" - v-show="tabView.name === currentView" - :key="tabView.name" - class="flex-fill gl-overflow-hidden js-tab-view" - > - <component :is="tabView.component" /> - </div> + <div + v-for="tabView in aliveTabViews" + v-show="tabView.name === currentView" + :key="tabView.name" + class="flex-fill gl-overflow-hidden js-tab-view gl-h-full" + > + <component :is="tabView.component" /> </div> - </resizable-panel> + </div> <ide-sidebar-nav :tabs="tabs" :side="side" diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue index 4a9de9e0c03..caa05daf8bd 100644 --- a/app/assets/javascripts/ide/components/panes/right.vue +++ b/app/assets/javascripts/ide/components/panes/right.vue @@ -2,15 +2,20 @@ import { mapGetters, mapState } from 'vuex'; import { __ } from '~/locale'; import CollapsibleSidebar from './collapsible_sidebar.vue'; -import { rightSidebarViews } from '../../constants'; +import ResizablePanel from '../resizable_panel.vue'; +import { rightSidebarViews, SIDEBAR_INIT_WIDTH, SIDEBAR_NAV_WIDTH } from '../../constants'; import PipelinesList from '../pipelines/list.vue'; import JobsDetail from '../jobs/detail.vue'; import Clientside from '../preview/clientside.vue'; +// Need to add the width of the nav buttons since the resizable container contains those as well +const WIDTH = SIDEBAR_INIT_WIDTH + SIDEBAR_NAV_WIDTH; + export default { name: 'RightPane', components: { CollapsibleSidebar, + ResizablePanel, }, props: { extensionTabs: { @@ -22,6 +27,7 @@ export default { computed: { ...mapState(['currentMergeRequestId', 'clientsidePreviewEnabled']), ...mapGetters(['packageJson']), + ...mapState('rightPane', ['isOpen']), showLivePreview() { return this.packageJson && this.clientsidePreviewEnabled; }, @@ -46,9 +52,18 @@ export default { ]; }, }, + WIDTH, }; </script> <template> - <collapsible-sidebar :extension-tabs="rightExtensionTabs" side="right" :width="350" /> + <resizable-panel + class="gl-display-flex gl-overflow-hidden" + side="right" + :initial-width="$options.WIDTH" + :min-size="$options.WIDTH" + :resizable="isOpen" + > + <collapsible-sidebar class="gl-w-full" :extension-tabs="rightExtensionTabs" side="right" /> + </resizable-panel> </template> diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue index d55150f48ad..b49d743d877 100644 --- a/app/assets/javascripts/ide/components/resizable_panel.vue +++ b/app/assets/javascripts/ide/components/resizable_panel.vue @@ -1,7 +1,7 @@ <script> import { mapActions } from 'vuex'; import PanelResizer from '~/vue_shared/components/panel_resizer.vue'; -import { DEFAULT_SIDEBAR_MIN_WIDTH } from '../constants'; +import { SIDEBAR_MIN_WIDTH } from '../constants'; export default { components: { @@ -15,12 +15,17 @@ export default { minSize: { type: Number, required: false, - default: DEFAULT_SIDEBAR_MIN_WIDTH, + default: SIDEBAR_MIN_WIDTH, }, side: { type: String, required: true, }, + resizable: { + type: Boolean, + required: false, + default: true, + }, }, data() { return { @@ -29,7 +34,7 @@ export default { }, computed: { panelStyle() { - if (!this.collapsed) { + if (this.resizable) { return { width: `${this.width}px`, }; @@ -46,9 +51,10 @@ export default { </script> <template> - <div :style="panelStyle"> + <div class="gl-relative" :style="panelStyle"> <slot></slot> <panel-resizer + v-show="resizable" :size.sync="width" :start-size="initialWidth" :min-size="minSize" diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index bc2cb3fdadc..555754c7104 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -4,8 +4,9 @@ export const MAX_WINDOW_HEIGHT_COMPACT = 750; export const MAX_TITLE_LENGTH = 50; export const MAX_BODY_LENGTH = 72; -export const LEFT_SIDEBAR_INIT_WIDTH = 340; -export const DEFAULT_SIDEBAR_MIN_WIDTH = 340; +export const SIDEBAR_INIT_WIDTH = 340; +export const SIDEBAR_MIN_WIDTH = 340; +export const SIDEBAR_NAV_WIDTH = 60; // File view modes export const FILE_VIEW_MODE_EDITOR = 'editor'; diff --git a/app/assets/javascripts/operation_settings/components/external_dashboard.vue b/app/assets/javascripts/operation_settings/components/external_dashboard.vue deleted file mode 100644 index e9c7d7c5d56..00000000000 --- a/app/assets/javascripts/operation_settings/components/external_dashboard.vue +++ /dev/null @@ -1,72 +0,0 @@ -<script> -import { mapState, mapActions } from 'vuex'; -import { GlDeprecatedButton, GlFormGroup, GlFormInput, GlLink } from '@gitlab/ui'; - -export default { - components: { - GlDeprecatedButton, - GlFormGroup, - GlFormInput, - GlLink, - }, - computed: { - ...mapState([ - 'externalDashboardHelpPagePath', - 'externalDashboardUrl', - 'operationsSettingsEndpoint', - ]), - userDashboardUrl: { - get() { - return this.externalDashboardUrl; - }, - set(url) { - this.setExternalDashboardUrl(url); - }, - }, - }, - methods: { - ...mapActions(['setExternalDashboardUrl', 'updateExternalDashboardUrl']), - }, -}; -</script> - -<template> - <section class="settings no-animate"> - <div class="settings-header"> - <h3 class="js-section-header h4"> - {{ s__('ExternalMetrics|External Dashboard') }} - </h3> - <gl-deprecated-button class="js-settings-toggle">{{ __('Expand') }}</gl-deprecated-button> - <p class="js-section-sub-header"> - {{ - s__( - 'ExternalMetrics|Add a button to the metrics dashboard linking directly to your existing external dashboards.', - ) - }} - <gl-link :href="externalDashboardHelpPagePath">{{ __('Learn more') }}</gl-link> - </p> - </div> - <div class="settings-content"> - <form> - <gl-form-group - :label="s__('ExternalMetrics|Full dashboard URL')" - label-for="full-dashboard-url" - :description="s__('ExternalMetrics|Enter the URL of the dashboard you want to link to')" - > - <!-- placeholder with a url is a false positive --> - <!-- eslint-disable @gitlab/vue-require-i18n-attribute-strings --> - <gl-form-input - id="full-dashboard-url" - v-model="userDashboardUrl" - placeholder="https://my-org.gitlab.io/my-dashboards" - @keydown.enter.native.prevent="updateExternalDashboardUrl" - /> - <!-- eslint-enable @gitlab/vue-require-i18n-attribute-strings --> - </gl-form-group> - <gl-deprecated-button variant="success" @click="updateExternalDashboardUrl"> - {{ __('Save Changes') }} - </gl-deprecated-button> - </form> - </div> - </section> -</template> diff --git a/app/assets/javascripts/operation_settings/components/form_group/external_dashboard.vue b/app/assets/javascripts/operation_settings/components/form_group/external_dashboard.vue new file mode 100644 index 00000000000..8f8e7db783d --- /dev/null +++ b/app/assets/javascripts/operation_settings/components/form_group/external_dashboard.vue @@ -0,0 +1,50 @@ +<script> +import { mapState, mapActions } from 'vuex'; +import { GlLink, GlFormGroup, GlFormInput } from '@gitlab/ui'; + +export default { + components: { + GlLink, + GlFormGroup, + GlFormInput, + }, + computed: { + ...mapState(['externalDashboard']), + userDashboardUrl: { + get() { + return this.externalDashboard.url; + }, + set(url) { + this.setExternalDashboardUrl(url); + }, + }, + }, + methods: { + ...mapActions(['setExternalDashboardUrl']), + }, +}; +</script> + +<template> + <gl-form-group + :label="s__('MetricsSettings|External dashboard URL')" + label-for="external-dashboard-url" + > + <template #description> + {{ + s__( + 'MetricsSettings|Add a button to the metrics dashboard linking directly to your existing external dashboard.', + ) + }} + <gl-link :href="externalDashboard.helpPage">{{ __('Learn more') }}</gl-link> + </template> + <!-- placeholder with a url is a false positive --> + <!-- eslint-disable @gitlab/vue-require-i18n-attribute-strings --> + <gl-form-input + id="external-dashboard-url" + v-model="userDashboardUrl" + placeholder="https://my-org.gitlab.io/my-dashboards" + /> + <!-- eslint-enable @gitlab/vue-require-i18n-attribute-strings --> + </gl-form-group> +</template> diff --git a/app/assets/javascripts/operation_settings/components/metrics_settings.vue b/app/assets/javascripts/operation_settings/components/metrics_settings.vue new file mode 100644 index 00000000000..ebf486162fc --- /dev/null +++ b/app/assets/javascripts/operation_settings/components/metrics_settings.vue @@ -0,0 +1,50 @@ +<script> +import { mapState, mapActions } from 'vuex'; +import { GlDeprecatedButton, GlLink } from '@gitlab/ui'; +import ExternalDashboard from './form_group/external_dashboard.vue'; + +export default { + components: { + GlDeprecatedButton, + GlLink, + ExternalDashboard, + }, + computed: { + ...mapState(['helpPage']), + userDashboardUrl: { + get() { + return this.externalDashboard.url; + }, + set(url) { + this.setExternalDashboardUrl(url); + }, + }, + }, + methods: { + ...mapActions(['saveChanges']), + }, +}; +</script> + +<template> + <section class="settings no-animate"> + <div class="settings-header"> + <h3 class="js-section-header h4"> + {{ s__('MetricsSettings|Metrics Dashboard') }} + </h3> + <gl-deprecated-button class="js-settings-toggle">{{ __('Expand') }}</gl-deprecated-button> + <p class="js-section-sub-header"> + {{ s__('MetricsSettings|Manage Metrics Dashboard settings.') }} + <gl-link :href="helpPage">{{ __('Learn more') }}</gl-link> + </p> + </div> + <div class="settings-content"> + <form> + <external-dashboard /> + <gl-deprecated-button variant="success" @click="saveChanges"> + {{ __('Save Changes') }} + </gl-deprecated-button> + </form> + </div> + </section> +</template> diff --git a/app/assets/javascripts/operation_settings/index.js b/app/assets/javascripts/operation_settings/index.js index f075291ce98..426a060949e 100644 --- a/app/assets/javascripts/operation_settings/index.js +++ b/app/assets/javascripts/operation_settings/index.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import store from './store'; -import ExternalDashboardForm from './components/external_dashboard.vue'; +import MetricsSettingsForm from './components/metrics_settings.vue'; export default () => { const el = document.querySelector('.js-operation-settings'); @@ -9,7 +9,7 @@ export default () => { el, store: store(el.dataset), render(createElement) { - return createElement(ExternalDashboardForm); + return createElement(MetricsSettingsForm); }, }); }; diff --git a/app/assets/javascripts/operation_settings/store/actions.js b/app/assets/javascripts/operation_settings/store/actions.js index ec05b0c76cf..62d00ac50e3 100644 --- a/app/assets/javascripts/operation_settings/store/actions.js +++ b/app/assets/javascripts/operation_settings/store/actions.js @@ -7,19 +7,19 @@ import * as mutationTypes from './mutation_types'; export const setExternalDashboardUrl = ({ commit }, url) => commit(mutationTypes.SET_EXTERNAL_DASHBOARD_URL, url); -export const updateExternalDashboardUrl = ({ state, dispatch }) => +export const saveChanges = ({ state, dispatch }) => axios .patch(state.operationsSettingsEndpoint, { project: { metrics_setting_attributes: { - external_dashboard_url: state.externalDashboardUrl, + external_dashboard_url: state.externalDashboard.url, }, }, }) - .then(() => dispatch('receiveExternalDashboardUpdateSuccess')) - .catch(error => dispatch('receiveExternalDashboardUpdateError', error)); + .then(() => dispatch('receiveSaveChangesSuccess')) + .catch(error => dispatch('receiveSaveChangesError', error)); -export const receiveExternalDashboardUpdateSuccess = () => { +export const receiveSaveChangesSuccess = () => { /** * The operations_controller currently handles successful requests * by creating a flash banner messsage to notify the user. @@ -27,8 +27,8 @@ export const receiveExternalDashboardUpdateSuccess = () => { refreshCurrentPage(); }; -export const receiveExternalDashboardUpdateError = (_, error) => { - const { response } = error; +export const receiveSaveChangesError = (_, error) => { + const { response = {} } = error; const message = response.data && response.data.message ? response.data.message : ''; createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert'); diff --git a/app/assets/javascripts/operation_settings/store/mutations.js b/app/assets/javascripts/operation_settings/store/mutations.js index 64bb33bb89f..eb495839616 100644 --- a/app/assets/javascripts/operation_settings/store/mutations.js +++ b/app/assets/javascripts/operation_settings/store/mutations.js @@ -2,6 +2,6 @@ import * as types from './mutation_types'; export default { [types.SET_EXTERNAL_DASHBOARD_URL](state, url) { - state.externalDashboardUrl = url; + state.externalDashboard.url = url; }, }; diff --git a/app/assets/javascripts/operation_settings/store/state.js b/app/assets/javascripts/operation_settings/store/state.js index 72167141c48..5376f6b32e6 100644 --- a/app/assets/javascripts/operation_settings/store/state.js +++ b/app/assets/javascripts/operation_settings/store/state.js @@ -1,5 +1,8 @@ export default (initialState = {}) => ({ - externalDashboardUrl: initialState.externalDashboardUrl || '', operationsSettingsEndpoint: initialState.operationsSettingsEndpoint, - externalDashboardHelpPagePath: initialState.externalDashboardHelpPagePath, + helpPage: initialState.helpPage, + externalDashboard: { + url: initialState.externalDashboardUrl, + helpPage: initialState.externalDashboardHelpPage, + }, }); diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index ddfdf8d0553..199778a8529 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -282,7 +282,6 @@ $ide-commit-header-height: 48px; .multi-file-commit-panel { display: flex; position: relative; - width: 340px; padding: 0; background-color: var(--ide-background, $gray-light); @@ -874,13 +873,11 @@ $ide-commit-header-height: 48px; } .ide-sidebar { - width: auto; min-width: 60px; } .ide-right-sidebar { .multi-file-commit-panel-inner { - width: 350px; padding: $grid-size 0; background-color: var(--ide-highlight-background, $white); border-right: 1px solid var(--ide-border-color, $white-dark); diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index ebee8e9094e..8a8064b24c2 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -17,7 +17,9 @@ class Dashboard::TodosController < Dashboard::ApplicationController end def destroy - TodoService.new.mark_todos_as_done_by_ids(params[:id], current_user) + todo = current_user.todos.find(params[:id]) + + TodoService.new.resolve_todo(todo, current_user, resolved_by_action: :mark_done) respond_to do |format| format.html do @@ -31,7 +33,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController end def destroy_all - updated_ids = TodoService.new.mark_todos_as_done(@todos, current_user) + updated_ids = TodoService.new.resolve_todos(@todos, current_user, resolved_by_action: :mark_all_done) respond_to do |format| format.html { redirect_to dashboard_todos_path, status: :found, notice: _('Everything on your to-do list is marked as done.') } @@ -41,13 +43,13 @@ class Dashboard::TodosController < Dashboard::ApplicationController end def restore - TodoService.new.mark_todos_as_pending_by_ids(params[:id], current_user) + TodoService.new.restore_todo(current_user.todos.find(params[:id]), current_user) render json: todos_counts end def bulk_restore - TodoService.new.mark_todos_as_pending_by_ids(params[:ids], current_user) + TodoService.new.restore_todos(current_user.todos.for_ids(params[:ids]), current_user) render json: todos_counts end diff --git a/app/graphql/mutations/todos/mark_all_done.rb b/app/graphql/mutations/todos/mark_all_done.rb index 5694985717c..d30d1bcbcf0 100644 --- a/app/graphql/mutations/todos/mark_all_done.rb +++ b/app/graphql/mutations/todos/mark_all_done.rb @@ -28,7 +28,9 @@ module Mutations def mark_all_todos_done return [] unless current_user - TodoService.new.mark_all_todos_as_done_by_user(current_user) + todos = TodosFinder.new(current_user).execute + + TodoService.new.resolve_todos(todos, current_user, resolved_by_action: :api_all_done) end end end diff --git a/app/graphql/mutations/todos/mark_done.rb b/app/graphql/mutations/todos/mark_done.rb index d738e387c43..748e02d8782 100644 --- a/app/graphql/mutations/todos/mark_done.rb +++ b/app/graphql/mutations/todos/mark_done.rb @@ -30,7 +30,7 @@ module Mutations private def mark_done(todo) - TodoService.new.mark_todo_as_done(todo, current_user) + TodoService.new.resolve_todo(todo, current_user, resolved_by_action: :api_done) end end end diff --git a/app/graphql/mutations/todos/restore.rb b/app/graphql/mutations/todos/restore.rb index c4597bd84a2..a0a1772db0a 100644 --- a/app/graphql/mutations/todos/restore.rb +++ b/app/graphql/mutations/todos/restore.rb @@ -18,7 +18,7 @@ module Mutations def resolve(id:) todo = authorized_find!(id: id) - restore(todo.id) if todo.done? + restore(todo) { todo: todo.reset, @@ -28,8 +28,8 @@ module Mutations private - def restore(id) - TodoService.new.mark_todos_as_pending_by_ids([id], current_user) + def restore(todo) + TodoService.new.restore_todo(todo, current_user) end end end diff --git a/app/graphql/mutations/todos/restore_many.rb b/app/graphql/mutations/todos/restore_many.rb index 8a6265207cd..e95651b232f 100644 --- a/app/graphql/mutations/todos/restore_many.rb +++ b/app/graphql/mutations/todos/restore_many.rb @@ -68,7 +68,7 @@ module Mutations end def restore(todos) - TodoService.new.mark_todos_as_pending(todos, current_user) + TodoService.new.restore_todos(todos, current_user) end end end diff --git a/app/models/todo.rb b/app/models/todo.rb index b5ca2ec7466..3af2d16deaa 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -66,6 +66,8 @@ class Todo < ApplicationRecord scope :with_entity_associations, -> { preload(:target, :author, :note, group: :route, project: [:route, { namespace: :route }]) } scope :joins_issue_and_assignees, -> { left_joins(issue: :assignees) } + enum resolved_by_action: { system_done: 0, api_all_done: 1, api_done: 2, mark_all_done: 3, mark_done: 4 }, _prefix: :resolved_by + state_machine :state, initial: :pending do event :done do transition [:pending] => :done @@ -100,17 +102,17 @@ class Todo < ApplicationRecord state.nil? ? exists?(target: target) : exists?(target: target, state: state) end - # Updates the state of a relation of todos to the new state. + # Updates attributes of a relation of todos to the new state. # - # new_state - The new state of the todos. + # new_attributes - The new attributes of the todos. # # Returns an `Array` containing the IDs of the updated todos. - def update_state(new_state) - # Only update those that are not really on that state - base = where.not(state: new_state).except(:order) + def batch_update(**new_attributes) + # Only update those that have different state + base = where.not(state: new_attributes[:state]).except(:order) ids = base.pluck(:id) - base.update_all(state: new_state, updated_at: Time.current) + base.update_all(new_attributes.merge(updated_at: Time.current)) ids end diff --git a/app/models/user.rb b/app/models/user.rb index 4311d2e37b1..e2455e667be 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1636,10 +1636,6 @@ class User < ApplicationRecord super.presence || build_user_detail end - def todos_limited_to(ids) - todos.where(id: ids) - end - def pending_todo_for(target) todos.find_by(target: target, state: :pending) end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 18062bd60da..3fa5b60369c 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -350,7 +350,7 @@ class IssuableBaseService < BaseService todo_service.mark_todo(issuable, current_user) when 'done' todo = TodosFinder.new(current_user).find_by(target: issuable) - todo_service.mark_todos_as_done_by_ids(todo, current_user) if todo + todo_service.resolve_todo(todo, current_user) if todo end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index ee1a22634af..d59bc0cc970 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -32,7 +32,7 @@ module Issues old_assignees = old_associations.fetch(:assignees, []) if has_changes?(issue, old_labels: old_labels, old_assignees: old_assignees) - todo_service.mark_pending_todos_as_done(issue, current_user) + todo_service.resolve_todos_for_target(issue, current_user) end if issue.previous_changes.include?('title') || @@ -68,7 +68,7 @@ module Issues end def handle_task_changes(issuable) - todo_service.mark_pending_todos_as_done(issuable, current_user) + todo_service.resolve_todos_for_target(issuable, current_user) todo_service.update_issue(issuable, current_user) end diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 2d33e87bf4b..561695baeab 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -27,7 +27,7 @@ module MergeRequests old_assignees = old_associations.fetch(:assignees, []) if has_changes?(merge_request, old_labels: old_labels, old_assignees: old_assignees) - todo_service.mark_pending_todos_as_done(merge_request, current_user) + todo_service.resolve_todos_for_target(merge_request, current_user) end if merge_request.previous_changes.include?('title') || @@ -73,7 +73,7 @@ module MergeRequests end def handle_task_changes(merge_request) - todo_service.mark_pending_todos_as_done(merge_request, current_user) + todo_service.resolve_todos_for_target(merge_request, current_user) todo_service.update_merge_request(merge_request, current_user) end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 55f888d5664..9a1934b322f 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -30,7 +30,7 @@ class TodoService # * mark all pending todos related to the target for the current user as done # def close_issue(issue, current_user) - mark_pending_todos_as_done(issue, current_user) + resolve_todos_for_target(issue, current_user) end # When we destroy a todo target we should: @@ -79,7 +79,7 @@ class TodoService # * mark all pending todos related to the target for the current user as done # def close_merge_request(merge_request, current_user) - mark_pending_todos_as_done(merge_request, current_user) + resolve_todos_for_target(merge_request, current_user) end # When merge a merge request we should: @@ -87,7 +87,7 @@ class TodoService # * mark all pending todos related to the target for the current user as done # def merge_merge_request(merge_request, current_user) - mark_pending_todos_as_done(merge_request, current_user) + resolve_todos_for_target(merge_request, current_user) end # When a build fails on the HEAD of a merge request we should: @@ -105,7 +105,7 @@ class TodoService # * mark all pending todos related to the merge request for that user as done # def merge_request_push(merge_request, current_user) - mark_pending_todos_as_done(merge_request, current_user) + resolve_todos_for_target(merge_request, current_user) end # When a build is retried to a merge request we should: @@ -114,7 +114,7 @@ class TodoService # def merge_request_build_retried(merge_request) merge_request.merge_participants.each do |user| - mark_pending_todos_as_done(merge_request, user) + resolve_todos_for_target(merge_request, user) end end @@ -151,76 +151,60 @@ class TodoService # * mark all pending todos related to the awardable for the current user as done # def new_award_emoji(awardable, current_user) - mark_pending_todos_as_done(awardable, current_user) + resolve_todos_for_target(awardable, current_user) end - # When marking pending todos as done we should: - # - # * mark all pending todos related to the target for the current user as done - # - def mark_pending_todos_as_done(target, user) - attributes = attributes_for_target(target) - pending_todos(user, attributes).update_all(state: :done) - user.update_todos_count_cache + # When user marks an issue as todo + def mark_todo(issuable, current_user) + attributes = attributes_for_todo(issuable.project, issuable, current_user, Todo::MARKED) + create_todos(current_user, attributes) end - # When user marks some todos as done - def mark_todos_as_done(todos, current_user) - update_todos_state(todos, current_user, :done) + def todo_exist?(issuable, current_user) + TodosFinder.new(current_user).any_for_target?(issuable, :pending) end - def mark_todos_as_done_by_ids(ids, current_user) - todos = todos_by_ids(ids, current_user) - mark_todos_as_done(todos, current_user) - end + # Resolves all todos related to target + def resolve_todos_for_target(target, current_user) + attributes = attributes_for_target(target) - def mark_all_todos_as_done_by_user(current_user) - todos = TodosFinder.new(current_user).execute - mark_todos_as_done(todos, current_user) + resolve_todos(pending_todos(current_user, attributes), current_user) end - def mark_todo_as_done(todo, current_user) - return if todo.done? - - todo.update(state: :done) + def resolve_todos(todos, current_user, resolution: :done, resolved_by_action: :system_done) + todos_ids = todos.batch_update(state: resolution, resolved_by_action: resolved_by_action) current_user.update_todos_count_cache - end - # When user marks some todos as pending - def mark_todos_as_pending(todos, current_user) - update_todos_state(todos, current_user, :pending) + todos_ids end - def mark_todos_as_pending_by_ids(ids, current_user) - todos = todos_by_ids(ids, current_user) - mark_todos_as_pending(todos, current_user) - end + def resolve_todo(todo, current_user, resolution: :done, resolved_by_action: :system_done) + return if todo.done? - # When user marks an issue as todo - def mark_todo(issuable, current_user) - attributes = attributes_for_todo(issuable.project, issuable, current_user, Todo::MARKED) - create_todos(current_user, attributes) - end + todo.update(state: resolution, resolved_by_action: resolved_by_action) - def todo_exist?(issuable, current_user) - TodosFinder.new(current_user).any_for_target?(issuable, :pending) + current_user.update_todos_count_cache end - private + def restore_todos(todos, current_user) + todos_ids = todos.batch_update(state: :pending) - def todos_by_ids(ids, current_user) - current_user.todos_limited_to(Array(ids)) + current_user.update_todos_count_cache + + todos_ids end - def update_todos_state(todos, current_user, state) - todos_ids = todos.update_state(state) + def restore_todo(todo, current_user) + return if todo.pending? - current_user.update_todos_count_cache + todo.update(state: :pending) - todos_ids + current_user.update_todos_count_cache end + private + def create_todos(users, attributes) Array(users).map do |user| next if pending_todos(user, attributes).exists? @@ -252,9 +236,9 @@ class TodoService return unless note.can_create_todo? project = note.project - target = note.noteable + target = note.noteable - mark_pending_todos_as_done(target, author) + resolve_todos_for_target(target, author) create_mention_todos(project, target, author, note, skip_users) end diff --git a/app/views/projects/settings/operations/_external_dashboard.html.haml b/app/views/projects/settings/operations/_external_dashboard.html.haml deleted file mode 100644 index 08d50a336fd..00000000000 --- a/app/views/projects/settings/operations/_external_dashboard.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -.js-operation-settings{ data: { operations_settings_endpoint: project_settings_operations_path(@project), - external_dashboard: { url: metrics_external_dashboard_url, - help_page_path: help_page_path('user/project/operations/linking_to_an_external_dashboard') } } } diff --git a/app/views/projects/settings/operations/_metrics_dashboard.html.haml b/app/views/projects/settings/operations/_metrics_dashboard.html.haml new file mode 100644 index 00000000000..eda8a52f188 --- /dev/null +++ b/app/views/projects/settings/operations/_metrics_dashboard.html.haml @@ -0,0 +1,5 @@ +.js-operation-settings{ data: { operations_settings_endpoint: project_settings_operations_path(@project), + help_page: help_page_path('user/project/integrations/prometheus'), + external_dashboard: { url: metrics_external_dashboard_url, + help_page: help_page_path('user/project/operations/linking_to_an_external_dashboard'), + } } } diff --git a/app/views/projects/settings/operations/show.html.haml b/app/views/projects/settings/operations/show.html.haml index ee47d70171b..9e4fbf81ca4 100644 --- a/app/views/projects/settings/operations/show.html.haml +++ b/app/views/projects/settings/operations/show.html.haml @@ -5,7 +5,7 @@ = render 'projects/settings/operations/incidents' = render 'projects/settings/operations/error_tracking' = render 'projects/settings/operations/prometheus', service: prometheus_service if Feature.enabled?(:settings_operations_prometheus_service) -= render 'projects/settings/operations/external_dashboard' += render 'projects/settings/operations/metrics_dashboard' = render 'projects/settings/operations/grafana_integration' = render_if_exists 'projects/settings/operations/tracing' = render_if_exists 'projects/settings/operations/status_page' diff --git a/changelogs/unreleased/214370-extend-metrics-settings.yml b/changelogs/unreleased/214370-extend-metrics-settings.yml new file mode 100644 index 00000000000..56045e3cb5d --- /dev/null +++ b/changelogs/unreleased/214370-extend-metrics-settings.yml @@ -0,0 +1,5 @@ +--- +title: Update operations metrics settings title and description to make them general +merge_request: 32494 +author: +type: changed diff --git a/changelogs/unreleased/216045-capture-todo-resolution.yml b/changelogs/unreleased/216045-capture-todo-resolution.yml new file mode 100644 index 00000000000..cc43635a120 --- /dev/null +++ b/changelogs/unreleased/216045-capture-todo-resolution.yml @@ -0,0 +1,5 @@ +--- +title: Store Todo resolution method +merge_request: 32753 +author: +type: added diff --git a/changelogs/unreleased/docs-u2f-version-history.yml b/changelogs/unreleased/docs-u2f-version-history.yml new file mode 100644 index 00000000000..e847faa317e --- /dev/null +++ b/changelogs/unreleased/docs-u2f-version-history.yml @@ -0,0 +1,5 @@ +--- +title: Add version history information on U2F support +merge_request: 33229 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/kborges-github-import-rake-add-rate-limit-doc.yml b/changelogs/unreleased/kborges-github-import-rake-add-rate-limit-doc.yml new file mode 100644 index 00000000000..5d797b0f7a1 --- /dev/null +++ b/changelogs/unreleased/kborges-github-import-rake-add-rate-limit-doc.yml @@ -0,0 +1,5 @@ +--- +title: Document github rate limit behavior +merge_request: 33090 +author: +type: other diff --git a/db/migrate/20200520103514_add_todo_resolved_by_action.rb b/db/migrate/20200520103514_add_todo_resolved_by_action.rb new file mode 100644 index 00000000000..0aa91091451 --- /dev/null +++ b/db/migrate/20200520103514_add_todo_resolved_by_action.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddTodoResolvedByAction < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + add_column :todos, :resolved_by_action, :integer, limit: 2 + end + end + + def down + with_lock_retries do + remove_column :todos, :resolved_by_action + end + end +end diff --git a/db/structure.sql b/db/structure.sql index 0f5065719da..a8192b2f78e 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -6520,7 +6520,8 @@ CREATE TABLE public.todos ( updated_at timestamp without time zone, note_id integer, commit_id character varying, - group_id integer + group_id integer, + resolved_by_action smallint ); CREATE SEQUENCE public.todos_id_seq @@ -13949,6 +13950,7 @@ COPY "schema_migrations" (version) FROM STDIN; 20200519115908 20200519171058 20200519194042 +20200520103514 20200525114553 20200525121014 20200526120714 diff --git a/doc/.vale/gitlab/SentenceLength.yml b/doc/.vale/gitlab/SentenceLength.yml index 5894fd28f99..5894fd28f99 100755..100644 --- a/doc/.vale/gitlab/SentenceLength.yml +++ b/doc/.vale/gitlab/SentenceLength.yml diff --git a/doc/administration/raketasks/github_import.md b/doc/administration/raketasks/github_import.md index 83a3d2c8884..a46a2b34687 100644 --- a/doc/administration/raketasks/github_import.md +++ b/doc/administration/raketasks/github_import.md @@ -12,6 +12,11 @@ Bear in mind that the syntax is very specific. Remove any spaces within the argu before/after the brackets. Also, some shells (for example, `zsh`) can interpret the open/close brackets (`[]`) separately. You may need to either escape the brackets or use double quotes. +## Caveats + +If the GitHub [rate limit](https://developer.github.com/v3/#rate-limiting) is reached while importing, +the importing process will wait (`sleep()`) until it can continue importing. + ## Importing multiple projects To import a project from the list of your GitHub projects available: diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index 6a0f19d61b0..18f9fc59d7a 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -65,8 +65,11 @@ in a safe place. ### Enable 2FA via U2F device +> Introduced in [GitLab 8.9](https://about.gitlab.com/blog/2016/06/22/gitlab-adds-support-for-u2f/). + GitLab officially only supports [YubiKey](https://www.yubico.com/products/) -U2F devices, but users have successfully used [SoloKeys](https://solokeys.com/). +U2F devices, but users have successfully used [SoloKeys](https://solokeys.com/) +or [Google Titan Security Key](https://cloud.google.com/titan-security-key). The U2F workflow is [supported by](https://caniuse.com/#search=U2F) the following desktop browsers: diff --git a/lib/api/todos.rb b/lib/api/todos.rb index 43ed9c96486..e36ddf21277 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -89,16 +89,18 @@ module API requires :id, type: Integer, desc: 'The ID of the todo being marked as done' end post ':id/mark_as_done' do - TodoService.new.mark_todos_as_done_by_ids(params[:id], current_user) todo = current_user.todos.find(params[:id]) + TodoService.new.resolve_todo(todo, current_user, resolved_by_action: :api_done) + present todo, with: Entities::Todo, current_user: current_user end desc 'Mark all todos as done' post '/mark_as_done' do todos = find_todos - TodoService.new.mark_todos_as_done(todos, current_user) + + TodoService.new.resolve_todos(todos, current_user, resolved_by_action: :api_all_done) no_content! end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 20a26662b4a..1ecbda9c7f5 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9119,18 +9119,6 @@ msgstr "" msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used." msgstr "" -msgid "ExternalMetrics|Add a button to the metrics dashboard linking directly to your existing external dashboards." -msgstr "" - -msgid "ExternalMetrics|Enter the URL of the dashboard you want to link to" -msgstr "" - -msgid "ExternalMetrics|External Dashboard" -msgstr "" - -msgid "ExternalMetrics|Full dashboard URL" -msgstr "" - msgid "ExternalWikiService|External Wiki" msgstr "" @@ -13727,6 +13715,18 @@ msgstr "" msgid "Metrics::UsersStarredDashboards|You are not authorized to add star to this dashboard" msgstr "" +msgid "MetricsSettings|Add a button to the metrics dashboard linking directly to your existing external dashboard." +msgstr "" + +msgid "MetricsSettings|External dashboard URL" +msgstr "" + +msgid "MetricsSettings|Manage Metrics Dashboard settings." +msgstr "" + +msgid "MetricsSettings|Metrics Dashboard" +msgstr "" + msgid "Metrics|Add metric" msgstr "" diff --git a/scripts/static-analysis b/scripts/static-analysis index ede29b85b8d..9103a9c14af 100755 --- a/scripts/static-analysis +++ b/scripts/static-analysis @@ -5,103 +5,118 @@ require_relative '../lib/gitlab' require_relative '../lib/gitlab/popen' require_relative '../lib/gitlab/popen/runner' -def emit_warnings(static_analysis) - static_analysis.warned_results.each do |result| - puts - puts "**** #{result.cmd.join(' ')} had the following warning(s):" - puts - puts result.stderr - puts - end -end +class StaticAnalysis + ALLOWED_WARNINGS = [ + # https://github.com/browserslist/browserslist/blob/d0ec62eb48c41c218478cd3ac28684df051cc865/node.js#L329 + # warns if caniuse-lite package is older than 6 months. Ignore this + # warning message so that GitLab backports don't fail. + "Browserslist: caniuse-lite is outdated. Please run next command `yarn upgrade`" + ].freeze + + # `gettext:updated_check` and `gitlab:sidekiq:sidekiq_queues_yml:check` will fail on FOSS installations + # (e.g. gitlab-org/gitlab-foss) since they test against a single + # file that is generated by an EE installation, which can + # contain values that a FOSS installation won't find. To work + # around this we will only enable this task on EE installations. + TASKS_BY_DURATIONS_SECONDS_DESC = { + %w[bin/rake lint:haml] => 338, + (Gitlab.ee? ? %w[bin/rake gettext:updated_check] : nil) => 308, + # Most of the time, RuboCop finishes in 30 seconds, but sometimes it can take around 1200 seconds so we set a + # duration of 300 to lower the likelihood that it will run in the same job as another long task... + %w[bundle exec rubocop --parallel] => 300, + %w[yarn run eslint] => 197, + %w[yarn run prettier-all] => 124, + %w[bin/rake gettext:lint] => 96, + %w[bundle exec license_finder] => 49, + %w[bin/rake scss_lint] => 38, + %w[bin/rake lint:static_verification] => 22, + %w[bin/rake gitlab:sidekiq:all_queues_yml:check] => 13, + (Gitlab.ee? ? %w[bin/rake gitlab:sidekiq:sidekiq_queues_yml:check] : nil) => 13, + %w[bin/rake config_lint] => 11, + %w[yarn run stylelint] => 9, + %w[scripts/lint-conflicts.sh] => 0.59, + %w[yarn run block-dependencies] => 0.35, + %w[scripts/lint-rugged] => 0.23, + %w[scripts/gemfile_lock_changed.sh] => 0.02, + %w[scripts/frontend/check_no_partial_karma_jest.sh] => 0.01, + %w[scripts/lint-changelog-filenames] => 0.01 + }.reject { |k| k.nil? }.sort_by { |a| -a[1] }.to_h.keys.freeze + + def run_tasks! + tasks = tasks_to_run((ENV['CI_NODE_INDEX'] || 1).to_i, (ENV['CI_NODE_TOTAL'] || 1).to_i) + + static_analysis = Gitlab::Popen::Runner.new + + static_analysis.run(tasks) do |cmd, &run| + puts + puts "$ #{cmd.join(' ')}" + + result = run.call + + puts "==> Finished in #{result.duration} seconds" + puts + end -def emit_errors(static_analysis) - static_analysis.failed_results.each do |result| puts - puts "**** #{result.cmd.join(' ')} failed with the following error(s):" + puts '===================================================' puts - puts result.stdout - puts result.stderr puts - end -end -ALLOWED_WARNINGS = [ - # https://github.com/browserslist/browserslist/blob/d0ec62eb48c41c218478cd3ac28684df051cc865/node.js#L329 - # warns if caniuse-lite package is older than 6 months. Ignore this - # warning message so that GitLab backports don't fail. - "Browserslist: caniuse-lite is outdated. Please run next command `yarn upgrade`" -].freeze + if static_analysis.all_success_and_clean? + puts 'All static analyses passed successfully.' + elsif static_analysis.all_success? + puts 'All static analyses passed successfully, but we have warnings:' + puts -def warning_count(static_analysis) - static_analysis.warned_results - .count { |result| !ALLOWED_WARNINGS.include?(result.stderr.strip) } -end + emit_warnings(static_analysis) -def jobs_to_run(node_index, node_total) - all_tasks = [ - %w[bin/rake lint:all], - %w[bundle exec license_finder], - %w[yarn run eslint], - %w[yarn run stylelint], - %w[yarn run prettier-all], - %w[yarn run block-dependencies], - %w[bundle exec rubocop --parallel], - %w[scripts/lint-conflicts.sh], - %w[scripts/lint-rugged], - %w[scripts/frontend/check_no_partial_karma_jest.sh], - %w[scripts/lint-changelog-filenames], - %w[scripts/gemfile_lock_changed.sh] - ] - - case node_total - when 1 - all_tasks - when 2 - rake_lint_all, *rest_jobs = all_tasks - case node_index - when 1 - [rake_lint_all] + exit 2 if warning_count(static_analysis).nonzero? else - rest_jobs - end - else - raise "Parallelization > 2 (currently set to #{node_total}) isn't supported yet!" - end -end - -tasks = jobs_to_run((ENV['CI_NODE_INDEX'] || 1).to_i, (ENV['CI_NODE_TOTAL'] || 1).to_i) -static_analysis = Gitlab::Popen::Runner.new + puts 'Some static analyses failed:' -static_analysis.run(tasks) do |cmd, &run| - puts - puts "$ #{cmd.join(' ')}" + emit_warnings(static_analysis) + emit_errors(static_analysis) - result = run.call - - puts "==> Finished in #{result.duration} seconds" - puts -end + exit 1 + end + end -puts -puts '===================================================' -puts -puts + def emit_warnings(static_analysis) + static_analysis.warned_results.each do |result| + puts + puts "**** #{result.cmd.join(' ')} had the following warning(s):" + puts + puts result.stderr + puts + end + end -if static_analysis.all_success_and_clean? - puts 'All static analyses passed successfully.' -elsif static_analysis.all_success? - puts 'All static analyses passed successfully, but we have warnings:' - puts + def emit_errors(static_analysis) + static_analysis.failed_results.each do |result| + puts + puts "**** #{result.cmd.join(' ')} failed with the following error(s):" + puts + puts result.stdout + puts result.stderr + puts + end + end - emit_warnings(static_analysis) + def warning_count(static_analysis) + static_analysis.warned_results + .count { |result| !ALLOWED_WARNINGS.include?(result.stderr.strip) } + end - exit 2 if warning_count(static_analysis).nonzero? -else - puts 'Some static analyses failed:' + def tasks_to_run(node_index, node_total) + tasks = [] + TASKS_BY_DURATIONS_SECONDS_DESC.each_with_index do |task, i| + tasks << task if i % node_total == (node_index - 1) + end - emit_warnings(static_analysis) - emit_errors(static_analysis) + tasks + end +end - exit 1 +if $0 == __FILE__ + StaticAnalysis.new.run_tasks! end diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb index 63867d5796a..c4f6d9a279f 100644 --- a/spec/features/dashboard/todos/todos_spec.rb +++ b/spec/features/dashboard/todos/todos_spec.rb @@ -114,7 +114,7 @@ describe 'Dashboard Todos' do context 'todo is stale on the page' do before do todos = TodosFinder.new(user, state: :pending).execute - TodoService.new.mark_todos_as_done(todos, user) + TodoService.new.resolve_todos(todos, user) end it_behaves_like 'deleting the todo' diff --git a/spec/frontend/ide/components/resizable_panel_spec.js b/spec/frontend/ide/components/resizable_panel_spec.js new file mode 100644 index 00000000000..7368de0cee7 --- /dev/null +++ b/spec/frontend/ide/components/resizable_panel_spec.js @@ -0,0 +1,114 @@ +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import ResizablePanel from '~/ide/components/resizable_panel.vue'; +import PanelResizer from '~/vue_shared/components/panel_resizer.vue'; +import { SIDE_LEFT, SIDE_RIGHT } from '~/ide/constants'; + +const TEST_WIDTH = 500; +const TEST_MIN_WIDTH = 400; + +describe('~/ide/components/resizable_panel', () => { + const localVue = createLocalVue(); + localVue.use(Vuex); + + let wrapper; + let store; + + beforeEach(() => { + store = new Vuex.Store({}); + jest.spyOn(store, 'dispatch').mockImplementation(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const createComponent = (props = {}) => { + wrapper = shallowMount(ResizablePanel, { + propsData: { + initialWidth: TEST_WIDTH, + minSize: TEST_MIN_WIDTH, + side: SIDE_LEFT, + ...props, + }, + store, + localVue, + }); + }; + const findResizer = () => wrapper.find(PanelResizer); + const findInlineStyle = () => wrapper.element.style.cssText; + const createInlineStyle = width => `width: ${width}px;`; + + describe.each` + props | showResizer | resizerSide | expectedStyle + ${{ resizable: true, side: SIDE_LEFT }} | ${true} | ${SIDE_RIGHT} | ${createInlineStyle(TEST_WIDTH)} + ${{ resizable: true, side: SIDE_RIGHT }} | ${true} | ${SIDE_LEFT} | ${createInlineStyle(TEST_WIDTH)} + ${{ resizable: false, side: SIDE_LEFT }} | ${false} | ${SIDE_RIGHT} | ${''} + `('with props $props', ({ props, showResizer, resizerSide, expectedStyle }) => { + beforeEach(() => { + createComponent(props); + }); + + it(`show resizer is ${showResizer}`, () => { + const expectedDisplay = showResizer ? '' : 'none'; + const resizer = findResizer(); + + expect(resizer.exists()).toBe(true); + expect(resizer.element.style.display).toBe(expectedDisplay); + }); + + it(`resizer side is '${resizerSide}'`, () => { + const resizer = findResizer(); + + expect(resizer.props('side')).toBe(resizerSide); + }); + + it(`has style '${expectedStyle}'`, () => { + expect(findInlineStyle()).toBe(expectedStyle); + }); + }); + + describe('default', () => { + beforeEach(() => { + createComponent(); + }); + + it('does not dispatch anything', () => { + expect(store.dispatch).not.toHaveBeenCalled(); + }); + + it.each` + event | dispatchArgs + ${'resize-start'} | ${['setResizingStatus', true]} + ${'resize-end'} | ${['setResizingStatus', false]} + `('when resizer emits $event, dispatch $dispatchArgs', ({ event, dispatchArgs }) => { + const resizer = findResizer(); + + resizer.vm.$emit(event); + + expect(store.dispatch).toHaveBeenCalledWith(...dispatchArgs); + }); + + it('renders resizer', () => { + const resizer = findResizer(); + + expect(resizer.props()).toMatchObject({ + maxSize: window.innerWidth / 2, + minSize: TEST_MIN_WIDTH, + startSize: TEST_WIDTH, + }); + }); + + it('when resizer emits update:size, changes inline width', () => { + const newSize = TEST_WIDTH - 100; + const resizer = findResizer(); + + resizer.vm.$emit('update:size', newSize); + + return wrapper.vm.$nextTick().then(() => { + expect(findInlineStyle()).toBe(createInlineStyle(newSize)); + }); + }); + }); +}); diff --git a/spec/frontend/operation_settings/components/external_dashboard_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js index 19214d1d954..7b7cf841fb7 100644 --- a/spec/frontend/operation_settings/components/external_dashboard_spec.js +++ b/spec/frontend/operation_settings/components/metrics_settings_spec.js @@ -1,7 +1,8 @@ import { mount, shallowMount } from '@vue/test-utils'; import { GlDeprecatedButton, GlLink, GlFormGroup, GlFormInput } from '@gitlab/ui'; import { TEST_HOST } from 'helpers/test_constants'; -import ExternalDashboard from '~/operation_settings/components/external_dashboard.vue'; +import MetricsSettings from '~/operation_settings/components/metrics_settings.vue'; +import ExternalDashboard from '~/operation_settings/components/form_group/external_dashboard.vue'; import store from '~/operation_settings/store'; import axios from '~/lib/utils/axios_utils'; import { refreshCurrentPage } from '~/lib/utils/url_utility'; @@ -12,18 +13,25 @@ jest.mock('~/flash'); describe('operation settings external dashboard component', () => { let wrapper; + const operationsSettingsEndpoint = `${TEST_HOST}/mock/ops/settings/endpoint`; + const helpPage = `${TEST_HOST}/help/metrics/page/path`; const externalDashboardUrl = `http://mock-external-domain.com/external/dashboard/url`; - const externalDashboardHelpPagePath = `${TEST_HOST}/help/page/path`; + const externalDashboardHelpPage = `${TEST_HOST}/help/external/page/path`; + const mountComponent = (shallow = true) => { const config = [ - ExternalDashboard, + MetricsSettings, { store: store({ operationsSettingsEndpoint, + helpPage, externalDashboardUrl, - externalDashboardHelpPagePath, + externalDashboardHelpPage, }), + stubs: { + ExternalDashboard, + }, }, ]; wrapper = shallow ? shallowMount(...config) : mount(...config); @@ -44,7 +52,7 @@ describe('operation settings external dashboard component', () => { it('renders header text', () => { mountComponent(); - expect(wrapper.find('.js-section-header').text()).toBe('External Dashboard'); + expect(wrapper.find('.js-section-header').text()).toBe('Metrics Dashboard'); }); describe('expand/collapse button', () => { @@ -64,16 +72,14 @@ describe('operation settings external dashboard component', () => { }); it('renders descriptive text', () => { - expect(subHeader.text()).toContain( - 'Add a button to the metrics dashboard linking directly to your existing external dashboards.', - ); + expect(subHeader.text()).toContain('Manage Metrics Dashboard settings.'); }); it('renders help page link', () => { const link = subHeader.find(GlLink); expect(link.text()).toBe('Learn more'); - expect(link.attributes().href).toBe(externalDashboardHelpPagePath); + expect(link.attributes().href).toBe(helpPage); }); }); @@ -82,18 +88,17 @@ describe('operation settings external dashboard component', () => { let formGroup; beforeEach(() => { - mountComponent(); - formGroup = wrapper.find(GlFormGroup); + mountComponent(false); + formGroup = wrapper.find(ExternalDashboard).find(GlFormGroup); }); it('uses label text', () => { - expect(formGroup.attributes().label).toBe('Full dashboard URL'); + expect(formGroup.find('label').text()).toBe('External dashboard URL'); }); it('uses description text', () => { - expect(formGroup.attributes().description).toBe( - 'Enter the URL of the dashboard you want to link to', - ); + const description = formGroup.find('small'); + expect(description.find('a').attributes('href')).toBe(externalDashboardHelpPage); }); }); diff --git a/spec/frontend/operation_settings/store/mutations_spec.js b/spec/frontend/operation_settings/store/mutations_spec.js index 1854142c89a..670ba95ab03 100644 --- a/spec/frontend/operation_settings/store/mutations_spec.js +++ b/spec/frontend/operation_settings/store/mutations_spec.js @@ -13,7 +13,7 @@ describe('operation settings mutations', () => { const mockUrl = 'mockUrl'; mutations.SET_EXTERNAL_DASHBOARD_URL(localState, mockUrl); - expect(localState.externalDashboardUrl).toBe(mockUrl); + expect(localState.externalDashboard.url).toBe(mockUrl); }); }); }); diff --git a/spec/frontend_integration/ide/__snapshots__/ide_integration_spec.js.snap b/spec/frontend_integration/ide/__snapshots__/ide_integration_spec.js.snap index a76f7960d03..fe714924c2b 100644 --- a/spec/frontend_integration/ide/__snapshots__/ide_integration_spec.js.snap +++ b/spec/frontend_integration/ide/__snapshots__/ide_integration_spec.js.snap @@ -15,7 +15,7 @@ exports[`WebIDE runs 1`] = ` (jest: contents hidden) </div> <div - class="multi-file-commit-panel flex-column" + class="gl-relative multi-file-commit-panel flex-column" style="width: 340px;" > <div diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index a23103b4e58..1bb04f1bb68 100644 --- a/spec/graphql/types/query_type_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -9,23 +9,18 @@ describe GitlabSchema.types['Query'] do it 'has the expected fields' do expected_fields = %i[ - current_user - design_management - geoNode + project + namespace group echo - instanceSecurityDashboard metadata - namespace - project - projects + current_user snippets + design_management user - vulnerabilities - vulnerabilitiesCountByDayAndSeverity ] - expect(described_class).to have_graphql_fields(*expected_fields) + expect(described_class).to have_graphql_fields(*expected_fields).at_least end describe 'namespace field' do diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb index 08f0627191a..4ec93f5a408 100644 --- a/spec/models/todo_spec.rb +++ b/spec/models/todo_spec.rb @@ -393,10 +393,10 @@ describe Todo do end end - describe '.update_state' do + describe '.batch_update' do it 'updates the state of todos' do todo = create(:todo, :pending) - ids = described_class.update_state(:done) + ids = described_class.batch_update(state: :done) todo.reload @@ -407,7 +407,7 @@ describe Todo do it 'does not update todos that already have the given state' do create(:todo, :pending) - expect(described_class.update_state(:pending)).to be_empty + expect(described_class.batch_update(state: :pending)).to be_empty end it 'updates updated_at' do @@ -416,7 +416,7 @@ describe Todo do Timecop.freeze(1.day.from_now) do expected_update_date = Time.current.utc - ids = described_class.update_state(:done) + ids = described_class.batch_update(state: :done) expect(Todo.where(id: ids).map(&:updated_at)).to all(be_like_time(expected_update_date)) end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 4894cf12372..56725dd0bd8 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -274,12 +274,12 @@ describe TodoService do end end - describe '#mark_pending_todos_as_done' do + describe '#resolve_todos_for_target' do it 'marks related pending todos to the target for the user as done' do first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) - service.mark_pending_todos_as_done(issue, john_doe) + service.resolve_todos_for_target(issue, john_doe) expect(first_todo.reload).to be_done expect(second_todo.reload).to be_done @@ -293,7 +293,7 @@ describe TodoService do expect(john_doe.todos_pending_count).to eq(1) expect(john_doe).to receive(:update_todos_count_cache).and_call_original - service.mark_pending_todos_as_done(issue, john_doe) + service.resolve_todos_for_target(issue, john_doe) expect(john_doe.todos_done_count).to eq(1) expect(john_doe.todos_pending_count).to eq(0) @@ -301,59 +301,6 @@ describe TodoService do end end - shared_examples 'updating todos state' do |meth, state, new_state| - let!(:first_todo) { create(:todo, state, user: john_doe, project: project, target: issue, author: author) } - let!(:second_todo) { create(:todo, state, user: john_doe, project: project, target: issue, author: author) } - - it 'updates related todos for the user with the new_state' do - service.send(meth, collection, john_doe) - - expect(first_todo.reload.state?(new_state)).to be true - expect(second_todo.reload.state?(new_state)).to be true - end - - it 'returns the updated ids' do - expect(service.send(meth, collection, john_doe)).to match_array([first_todo.id, second_todo.id]) - end - - describe 'cached counts' do - it 'updates when todos change' do - expect(john_doe.todos.where(state: new_state).count).to eq(0) - expect(john_doe.todos.where(state: state).count).to eq(2) - expect(john_doe).to receive(:update_todos_count_cache).and_call_original - - service.send(meth, collection, john_doe) - - expect(john_doe.todos.where(state: new_state).count).to eq(2) - expect(john_doe.todos.where(state: state).count).to eq(0) - end - end - end - - describe '#mark_todos_as_done' do - it_behaves_like 'updating todos state', :mark_todos_as_done, :pending, :done do - let(:collection) { Todo.all } - end - end - - describe '#mark_todos_as_done_by_ids' do - it_behaves_like 'updating todos state', :mark_todos_as_done_by_ids, :pending, :done do - let(:collection) { [first_todo, second_todo].map(&:id) } - end - end - - describe '#mark_todos_as_pending' do - it_behaves_like 'updating todos state', :mark_todos_as_pending, :done, :pending do - let(:collection) { Todo.all } - end - end - - describe '#mark_todos_as_pending_by_ids' do - it_behaves_like 'updating todos state', :mark_todos_as_pending_by_ids, :done, :pending do - let(:collection) { [first_todo, second_todo].map(&:id) } - end - end - describe '#new_note' do let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } @@ -1000,121 +947,111 @@ describe TodoService do expect(john_doe.todos_pending_count).to eq(1) end - describe '#mark_todos_as_done' do - let(:issue) { create(:issue, project: project, author: author, assignees: [john_doe]) } - let(:another_issue) { create(:issue, project: project, author: author, assignees: [john_doe]) } + shared_examples 'updating todos state' do |state, new_state, new_resolved_by = nil| + let!(:first_todo) { create(:todo, state, user: john_doe) } + let!(:second_todo) { create(:todo, state, user: john_doe) } + let(:collection) { Todo.all } - it 'marks a relation of todos as done' do - create(:todo, :mentioned, user: john_doe, target: issue, project: project) + it 'updates related todos for the user with the new_state' do + method_call - todos = TodosFinder.new(john_doe, {}).execute - expect { described_class.new.mark_todos_as_done(todos, john_doe) } - .to change { john_doe.todos.done.count }.from(0).to(1) + expect(collection.all? { |todo| todo.reload.state?(new_state)}).to be_truthy end - it 'marks an array of todos as done' do - todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) + if new_resolved_by + it 'updates resolution mechanism' do + method_call - todos = TodosFinder.new(john_doe, {}).execute - expect { described_class.new.mark_todos_as_done(todos, john_doe) } - .to change { todo.reload.state }.from('pending').to('done') + expect(collection.all? { |todo| todo.reload.resolved_by_action == new_resolved_by }).to be_truthy + end end - it 'returns the ids of updated todos' do # Needed on API - todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) - - todos = TodosFinder.new(john_doe, {}).execute - expect(described_class.new.mark_todos_as_done(todos, john_doe)).to eq([todo.id]) + it 'returns the updated ids' do + expect(method_call).to match_array([first_todo.id, second_todo.id]) end - context 'when some of the todos are done already' do - let!(:first_todo) { create(:todo, :mentioned, user: john_doe, target: issue, project: project) } - let!(:second_todo) { create(:todo, :mentioned, user: john_doe, target: another_issue, project: project) } + describe 'cached counts' do + it 'updates when todos change' do + expect(john_doe.todos.where(state: new_state).count).to eq(0) + expect(john_doe.todos.where(state: state).count).to eq(2) + expect(john_doe).to receive(:update_todos_count_cache).and_call_original - it 'returns the ids of those still pending' do - described_class.new.mark_pending_todos_as_done(issue, john_doe) + method_call - expect(described_class.new.mark_todos_as_done(Todo.all, john_doe)).to eq([second_todo.id]) - end - - it 'returns an empty array if all are done' do - described_class.new.mark_pending_todos_as_done(issue, john_doe) - described_class.new.mark_pending_todos_as_done(another_issue, john_doe) - - expect(described_class.new.mark_todos_as_done(Todo.all, john_doe)).to eq([]) + expect(john_doe.todos.where(state: new_state).count).to eq(2) + expect(john_doe.todos.where(state: state).count).to eq(0) end end end - describe '#mark_todo_as_done' do - it 'marks a todo done' do - todo1 = create(:todo, :pending, user: john_doe) - - described_class.new.mark_todo_as_done(todo1, john_doe) - - expect(todo1.reload.state).to eq('done') - end - - context 'when todo is already in state done' do - let(:todo1) { create(:todo, :done, user: john_doe) } - - it 'does not update the todo' do - expect { described_class.new.mark_todo_as_done(todo1, john_doe) }.not_to change(todo1.reload, :state) + describe '#resolve_todos' do + it_behaves_like 'updating todos state', :pending, :done, 'mark_done' do + subject(:method_call) do + service.resolve_todos(collection, john_doe, resolution: :done, resolved_by_action: :mark_done) end + end + end - it 'does not update cache count' do - expect(john_doe).not_to receive(:update_todos_count_cache) - - described_class.new.mark_todo_as_done(todo1, john_doe) + describe '#restore_todos' do + it_behaves_like 'updating todos state', :done, :pending do + subject(:method_call) do + service.restore_todos(collection, john_doe) end end end - describe '#mark_all_todos_as_done_by_user' do - it 'marks all todos done' do - todo1 = create(:todo, user: john_doe, state: :pending) - todo2 = create(:todo, user: john_doe, state: :done) - todo3 = create(:todo, user: john_doe, state: :pending) + describe '#resolve_todo' do + let!(:todo) { create(:todo, :assigned, user: john_doe) } - ids = described_class.new.mark_all_todos_as_done_by_user(john_doe) + it 'marks pending todo as done' do + expect do + service.resolve_todo(todo, john_doe) + todo.reload + end.to change { todo.done? }.to(true) + end - expect(ids).to contain_exactly(todo1.id, todo3.id) - expect(todo1.reload.state).to eq('done') - expect(todo2.reload.state).to eq('done') - expect(todo3.reload.state).to eq('done') + it 'saves resolution mechanism' do + expect do + service.resolve_todo(todo, john_doe, resolved_by_action: :mark_done) + todo.reload + end.to change { todo.resolved_by_mark_done? }.to(true) end - end - describe '#mark_todos_as_done_by_ids' do - let(:issue) { create(:issue, project: project, author: author, assignees: [john_doe]) } - let(:another_issue) { create(:issue, project: project, author: author, assignees: [john_doe]) } + context 'cached counts' do + it 'updates when todos change' do + expect(john_doe.todos_done_count).to eq(0) + expect(john_doe.todos_pending_count).to eq(1) + expect(john_doe).to receive(:update_todos_count_cache).and_call_original - it 'marks an array of todo ids as done' do - todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) - another_todo = create(:todo, :mentioned, user: john_doe, target: another_issue, project: project) + service.resolve_todo(todo, john_doe) - expect { described_class.new.mark_todos_as_done_by_ids([todo.id, another_todo.id], john_doe) } - .to change { john_doe.todos.done.count }.from(0).to(2) + expect(john_doe.todos_done_count).to eq(1) + expect(john_doe.todos_pending_count).to eq(0) + end end + end - it 'marks a single todo id as done' do - todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) + describe '#restore_todo' do + let!(:todo) { create(:todo, :done, user: john_doe) } - expect { described_class.new.mark_todos_as_done_by_ids(todo.id, john_doe) } - .to change { todo.reload.state }.from('pending').to('done') + it 'marks resolved todo as pending' do + expect do + service.restore_todo(todo, john_doe) + todo.reload + end.to change { todo.pending? }.to(true) end - it 'caches the number of todos of a user', :use_clean_rails_memory_store_caching do - create(:todo, :mentioned, user: john_doe, target: issue, project: project) - todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) + context 'cached counts' do + it 'updates when todos change' do + expect(john_doe.todos_done_count).to eq(1) + expect(john_doe.todos_pending_count).to eq(0) + expect(john_doe).to receive(:update_todos_count_cache).and_call_original - described_class.new.mark_todos_as_done_by_ids(todo, john_doe) + service.restore_todo(todo, john_doe) - # Make sure no TodosFinder is inialized to perform counting - expect(TodosFinder).not_to receive(:new) - - expect(john_doe.todos_done_count).to eq(1) - expect(john_doe.todos_pending_count).to eq(1) + expect(john_doe.todos_done_count).to eq(0) + expect(john_doe.todos_pending_count).to eq(1) + end end end |