diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-30 00:09:22 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-30 00:09:22 +0300 |
commit | 27d314277bfe7fffec215efa9b1833a23bb82940 (patch) | |
tree | 898c606409718e70579beea62174624f84e28629 /app | |
parent | 6b9d3a4e8351e662c4586b24bb152de78ae9e3bf (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/lib/utils/datetime_range.js | 223 | ||||
-rw-r--r-- | app/assets/javascripts/pipelines/components/header_component.vue | 112 | ||||
-rw-r--r-- | app/assets/javascripts/pipelines/pipeline_details_bundle.js | 8 | ||||
-rw-r--r-- | app/assets/javascripts/vue_shared/components/header_ci_component.vue | 24 | ||||
-rw-r--r-- | app/helpers/milestones_helper.rb | 22 | ||||
-rw-r--r-- | app/models/concerns/milestoneish.rb | 8 | ||||
-rw-r--r-- | app/models/deploy_token.rb | 5 | ||||
-rw-r--r-- | app/views/shared/milestones/_issues_tab.html.haml | 5 | ||||
-rw-r--r-- | app/views/shared/milestones/_labels_tab.html.haml | 6 |
9 files changed, 288 insertions, 125 deletions
diff --git a/app/assets/javascripts/lib/utils/datetime_range.js b/app/assets/javascripts/lib/utils/datetime_range.js new file mode 100644 index 00000000000..53b8702afa7 --- /dev/null +++ b/app/assets/javascripts/lib/utils/datetime_range.js @@ -0,0 +1,223 @@ +import dateformat from 'dateformat'; +import { secondsToMilliseconds } from './datetime_utility'; + +const MINIMUM_DATE = new Date(0); + +const DEFAULT_DIRECTION = 'before'; + +const durationToMillis = duration => { + if (Object.entries(duration).length === 1 && Number.isFinite(duration.seconds)) { + return secondsToMilliseconds(duration.seconds); + } + // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings + throw new Error('Invalid duration: only `seconds` is supported'); +}; + +const dateMinusDuration = (date, duration) => new Date(date.getTime() - durationToMillis(duration)); + +const datePlusDuration = (date, duration) => new Date(date.getTime() + durationToMillis(duration)); + +const isValidDuration = duration => Boolean(duration && Number.isFinite(duration.seconds)); + +const isValidDateString = dateString => { + if (typeof dateString !== 'string' || !dateString.trim()) { + return false; + } + + try { + // dateformat throws error that can be caught. + // This is better than using `new Date()` + dateformat(dateString, 'isoUtcDateTime'); + return true; + } catch (e) { + return false; + } +}; + +const handleRangeDirection = ({ direction = DEFAULT_DIRECTION, anchorDate, minDate, maxDate }) => { + let startDate; + let endDate; + + if (direction === DEFAULT_DIRECTION) { + startDate = minDate; + endDate = anchorDate; + } else { + startDate = anchorDate; + endDate = maxDate; + } + + return { + startDate, + endDate, + }; +}; + +/** + * Converts a fixed range to a fixed range + * @param {Object} fixedRange - A range with fixed start and + * end (e.g. "midnight January 1st 2020 to midday January31st 2020") + */ +const convertFixedToFixed = ({ start, end }) => ({ + start, + end, +}); + +/** + * Converts an anchored range to a fixed range + * @param {Object} anchoredRange - A duration of time + * relative to a fixed point in time (e.g., "the 30 minutes + * before midnight January 1st 2020", or "the 2 days + * after midday on the 11th of May 2019") + */ +const convertAnchoredToFixed = ({ anchor, duration, direction }) => { + const anchorDate = new Date(anchor); + + const { startDate, endDate } = handleRangeDirection({ + minDate: dateMinusDuration(anchorDate, duration), + maxDate: datePlusDuration(anchorDate, duration), + direction, + anchorDate, + }); + + return { + start: startDate.toISOString(), + end: endDate.toISOString(), + }; +}; + +/** + * Converts a rolling change to a fixed range + * + * @param {Object} rollingRange - A time range relative to + * now (e.g., "last 2 minutes", or "next 2 days") + */ +const convertRollingToFixed = ({ duration, direction }) => { + // Use Date.now internally for easier mocking in tests + const now = new Date(Date.now()); + + return convertAnchoredToFixed({ + duration, + direction, + anchor: now.toISOString(), + }); +}; + +/** + * Converts an open range to a fixed range + * + * @param {Object} openRange - A time range relative + * to an anchor (e.g., "before midnight on the 1st of + * January 2020", or "after midday on the 11th of May 2019") + */ +const convertOpenToFixed = ({ anchor, direction }) => { + // Use Date.now internally for easier mocking in tests + const now = new Date(Date.now()); + + const { startDate, endDate } = handleRangeDirection({ + minDate: MINIMUM_DATE, + maxDate: now, + direction, + anchorDate: new Date(anchor), + }); + + return { + start: startDate.toISOString(), + end: endDate.toISOString(), + }; +}; + +/** + * Handles invalid date ranges + */ +const handleInvalidRange = () => { + // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings + throw new Error('The input range does not have the right format.'); +}; + +const handlers = { + invalid: handleInvalidRange, + fixed: convertFixedToFixed, + anchored: convertAnchoredToFixed, + rolling: convertRollingToFixed, + open: convertOpenToFixed, +}; + +/** + * Validates and returns the type of range + * + * @param {Object} Date time range + * @returns {String} `key` value for one of the handlers + */ +export function getRangeType(range) { + const { start, end, anchor, duration } = range; + + if ((start || end) && !anchor && !duration) { + return isValidDateString(start) && isValidDateString(end) ? 'fixed' : 'invalid'; + } + if (anchor && duration) { + return isValidDateString(anchor) && isValidDuration(duration) ? 'anchored' : 'invalid'; + } + if (duration && !anchor) { + return isValidDuration(duration) ? 'rolling' : 'invalid'; + } + if (anchor && !duration) { + return isValidDateString(anchor) ? 'open' : 'invalid'; + } + return 'invalid'; +} + +/** + * convertToFixedRange Transforms a `range of time` into a `fixed range of time`. + * + * The following types of a `ranges of time` can be represented: + * + * Fixed Range: A range with fixed start and end (e.g. "midnight January 1st 2020 to midday January 31st 2020") + * Anchored Range: A duration of time relative to a fixed point in time (e.g., "the 30 minutes before midnight January 1st 2020", or "the 2 days after midday on the 11th of May 2019") + * Rolling Range: A time range relative to now (e.g., "last 2 minutes", or "next 2 days") + * Open Range: A time range relative to an anchor (e.g., "before midnight on the 1st of January 2020", or "after midday on the 11th of May 2019") + * + * @param {Object} dateTimeRange - A Time Range representation + * It contains the data needed to create a fixed time range plus + * a label (recommended) to indicate the range that is covered. + * + * A definition via a TypeScript notation is presented below: + * + * + * type Duration = { // A duration of time, always in seconds + * seconds: number; + * } + * + * type Direction = 'before' | 'after'; // Direction of time relative to an anchor + * + * type FixedRange = { + * start: ISO8601; + * end: ISO8601; + * label: string; + * } + * + * type AnchoredRange = { + * anchor: ISO8601; + * duration: Duration; + * direction: Direction; // defaults to 'before' + * label: string; + * } + * + * type RollingRange = { + * duration: Duration; + * direction: Direction; // defaults to 'before' + * label: string; + * } + * + * type OpenRange = { + * anchor: ISO8601; + * direction: Direction; // defaults to 'before' + * label: string; + * } + * + * type DateTimeRange = FixedRange | AnchoredRange | RollingRange | OpenRange; + * + * + * @returns {FixedRange} An object with a start and end in ISO8601 format. + */ +export const convertToFixedRange = dateTimeRange => + handlers[getRangeType(dateTimeRange)](dateTimeRange); diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index 726bba7f9f4..2a3d022c5cd 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -1,6 +1,7 @@ <script> -import { GlLoadingIcon, GlModal } from '@gitlab/ui'; -import ciHeader from '../../vue_shared/components/header_ci_component.vue'; +import { GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui'; +import ciHeader from '~/vue_shared/components/header_ci_component.vue'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; import eventHub from '../event_hub'; import { __ } from '~/locale'; @@ -12,6 +13,10 @@ export default { ciHeader, GlLoadingIcon, GlModal, + LoadingButton, + }, + directives: { + GlModal: GlModalDirective, }, props: { pipeline: { @@ -25,7 +30,9 @@ export default { }, data() { return { - actions: this.getActions(), + isCanceling: false, + isRetrying: false, + isDeleting: false, }; }, @@ -43,67 +50,18 @@ export default { }, }, - watch: { - pipeline() { - this.actions = this.getActions(); - }, - }, - methods: { - onActionClicked(action) { - if (action.modal) { - this.$root.$emit('bv::show::modal', action.modal); - } else { - this.postAction(action); - } + cancelPipeline() { + this.isCanceling = true; + eventHub.$emit('headerPostAction', this.pipeline.cancel_path); }, - postAction(action) { - const index = this.actions.indexOf(action); - - this.$set(this.actions[index], 'isLoading', true); - - eventHub.$emit('headerPostAction', action); + retryPipeline() { + this.isRetrying = true; + eventHub.$emit('headerPostAction', this.pipeline.retry_path); }, deletePipeline() { - const index = this.actions.findIndex(action => action.modal === DELETE_MODAL_ID); - - this.$set(this.actions[index], 'isLoading', true); - - eventHub.$emit('headerDeleteAction', this.actions[index]); - }, - - getActions() { - const actions = []; - - if (this.pipeline.retry_path) { - actions.push({ - label: __('Retry'), - path: this.pipeline.retry_path, - cssClass: 'js-retry-button btn btn-inverted-secondary', - isLoading: false, - }); - } - - if (this.pipeline.cancel_path) { - actions.push({ - label: __('Cancel running'), - path: this.pipeline.cancel_path, - cssClass: 'js-btn-cancel-pipeline btn btn-danger', - isLoading: false, - }); - } - - if (this.pipeline.delete_path) { - actions.push({ - label: __('Delete'), - path: this.pipeline.delete_path, - modal: DELETE_MODAL_ID, - cssClass: 'js-btn-delete-pipeline btn btn-danger btn-inverted', - isLoading: false, - }); - } - - return actions; + this.isDeleting = true; + eventHub.$emit('headerDeleteAction', this.pipeline.delete_path); }, }, DELETE_MODAL_ID, @@ -117,10 +75,38 @@ export default { :item-id="pipeline.id" :time="pipeline.created_at" :user="pipeline.user" - :actions="actions" item-name="Pipeline" - @actionClicked="onActionClicked" - /> + > + <loading-button + v-if="pipeline.retry_path" + :loading="isRetrying" + :disabled="isRetrying" + class="js-retry-button btn btn-inverted-secondary" + container-class="d-inline" + :label="__('Retry')" + @click="retryPipeline()" + /> + + <loading-button + v-if="pipeline.cancel_path" + :loading="isCanceling" + :disabled="isCanceling" + class="js-btn-cancel-pipeline btn btn-danger" + container-class="d-inline" + :label="__('Cancel running')" + @click="cancelPipeline()" + /> + + <loading-button + v-if="pipeline.delete_path" + v-gl-modal="$options.DELETE_MODAL_ID" + :loading="isDeleting" + :disabled="isDeleting" + class="js-btn-delete-pipeline btn btn-danger btn-inverted" + container-class="d-inline" + :label="__('Delete')" + /> + </ci-header> <gl-loading-icon v-if="isLoading" :size="2" class="prepend-top-default append-bottom-default" /> diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index c874c4c6fdd..4ae3e813c36 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -70,16 +70,16 @@ export default () => { eventHub.$off('headerDeleteAction', this.deleteAction); }, methods: { - postAction(action) { + postAction(path) { this.mediator.service - .postAction(action.path) + .postAction(path) .then(() => this.mediator.refreshPipeline()) .catch(() => Flash(__('An error occurred while making the request.'))); }, - deleteAction(action) { + deleteAction(path) { this.mediator.stopPipelinePoll(); this.mediator.service - .deleteAction(action.path) + .deleteAction(path) .then(({ request }) => redirectTo(setUrlFragment(request.responseURL, 'delete_success'))) .catch(() => Flash(__('An error occurred while deleting the pipeline.'))); }, diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index dba4a9231a1..876eb7b899c 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -4,7 +4,6 @@ import { __, sprintf } from '~/locale'; import CiIconBadge from './ci_badge_link.vue'; import TimeagoTooltip from './time_ago_tooltip.vue'; import UserAvatarImage from './user_avatar/user_avatar_image.vue'; -import LoadingButton from '~/vue_shared/components/loading_button.vue'; /** * Renders header component for job and pipeline page based on UI mockups @@ -20,7 +19,6 @@ export default { UserAvatarImage, GlLink, GlButton, - LoadingButton, }, directives: { GlTooltip: GlTooltipDirective, @@ -47,11 +45,6 @@ export default { required: false, default: () => ({}), }, - actions: { - type: Array, - required: false, - default: () => [], - }, hasSidebarButton: { type: Boolean, required: false, @@ -71,9 +64,6 @@ export default { }, methods: { - onClickAction(action) { - this.$emit('actionClicked', action); - }, onClickSidebarButton() { this.$emit('clickedSidebarButton'); }, @@ -115,18 +105,8 @@ export default { </template> </section> - <section v-if="actions.length" class="header-action-buttons"> - <template v-for="(action, i) in actions"> - <loading-button - :key="i" - :loading="action.isLoading" - :disabled="action.isLoading" - :class="action.cssClass" - container-class="d-inline" - :label="action.label" - @click="onClickAction(action)" - /> - </template> + <section v-if="$slots.default" class="header-action-buttons"> + <slot></slot> </section> <gl-button v-if="hasSidebarButton" diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 494c0bee8b8..b12b39073ef 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -26,7 +26,7 @@ module MilestonesHelper end end - def milestones_issues_path(opts = {}) + def milestones_label_path(opts = {}) if @project project_issues_path(@project, opts) elsif @group @@ -281,26 +281,6 @@ module MilestonesHelper can?(current_user, :admin_milestone, @project.group) end end - - def display_issues_count_warning? - milestone_visible_issues_count > Milestone::DISPLAY_ISSUES_LIMIT - end - - def milestone_issues_count_message - total_count = milestone_visible_issues_count - limit = Milestone::DISPLAY_ISSUES_LIMIT - - message = _('Showing %{limit} of %{total_count} issues. ') % { limit: limit, total_count: total_count } - message += link_to(_('View all issues'), milestones_issues_path) - - message.html_safe - end - - private - - def milestone_visible_issues_count - @milestone_visible_issues_count ||= @milestone.issues_visible_to_user(current_user).size - end end MilestonesHelper.prepend_if_ee('EE::MilestonesHelper') diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 9ff60003406..88e752e51e7 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true module Milestoneish - DISPLAY_ISSUES_LIMIT = 20 - def total_issues_count(user) count_issues_by_state(user).values.sum end @@ -55,11 +53,7 @@ module Milestoneish end def sorted_issues(user) - # This method is used on milestone view to filter opened assigned, opened unassigned and closed issues columns. - # We want a limit of DISPLAY_ISSUES_LIMIT for total issues present on all columns. - limited_ids = issues_visible_to_user(user).limit(DISPLAY_ISSUES_LIMIT).select(:id) - - Issue.where(id: limited_ids).preload_associated_models.sort_by_attribute('label_priority') + issues_visible_to_user(user).preload_associated_models.sort_by_attribute('label_priority') end def sorted_merge_requests(user) diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb index 20e1d802178..3d098406ab1 100644 --- a/app/models/deploy_token.rb +++ b/app/models/deploy_token.rb @@ -24,6 +24,11 @@ class DeployToken < ApplicationRecord message: "can contain only letters, digits, '_', '-', '+', and '.'" } + enum deploy_token_type: { + group_type: 1, + project_type: 2 + } + before_save :ensure_token accepts_nested_attributes_for :project_deploy_tokens diff --git a/app/views/shared/milestones/_issues_tab.html.haml b/app/views/shared/milestones/_issues_tab.html.haml index 52ce0482cd0..a8db7f8a556 100644 --- a/app/views/shared/milestones/_issues_tab.html.haml +++ b/app/views/shared/milestones/_issues_tab.html.haml @@ -1,11 +1,6 @@ - args = { show_project_name: local_assigns.fetch(:show_project_name, false), show_full_project_name: local_assigns.fetch(:show_full_project_name, false) } -- if display_issues_count_warning? - .flash-container - .flash-warning#milestone-issue-count-warning - = milestone_issues_count_message - .row.prepend-top-default .col-md-4 = render 'shared/milestones/issuables', args.merge(title: 'Unstarted Issues (open and unassigned)', issuables: issues.opened.unassigned, id: 'unassigned', show_counter: true) diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml index cdea15bf13e..ecab037e378 100644 --- a/app/views/shared/milestones/_labels_tab.html.haml +++ b/app/views/shared/milestones/_labels_tab.html.haml @@ -5,12 +5,12 @@ %li.no-border %span.label-row %span.label-name - = render_label(label, tooltip: false, link: milestones_issues_path(options)) + = render_label(label, tooltip: false, link: milestones_label_path(options)) %span.prepend-description-left = markdown_field(label, :description) .float-right.d-none.d-lg-block.d-xl-block - = link_to milestones_issues_path(options.merge(state: 'opened')), class: 'btn btn-transparent btn-action' do + = link_to milestones_label_path(options.merge(state: 'opened')), class: 'btn btn-transparent btn-action' do - pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue' - = link_to milestones_issues_path(options.merge(state: 'closed')), class: 'btn btn-transparent btn-action' do + = link_to milestones_label_path(options.merge(state: 'closed')), class: 'btn btn-transparent btn-action' do - pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue' |