Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-01-30 00:09:22 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-01-30 00:09:22 +0300
commit27d314277bfe7fffec215efa9b1833a23bb82940 (patch)
tree898c606409718e70579beea62174624f84e28629 /app
parent6b9d3a4e8351e662c4586b24bb152de78ae9e3bf (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/lib/utils/datetime_range.js223
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue112
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue24
-rw-r--r--app/helpers/milestones_helper.rb22
-rw-r--r--app/models/concerns/milestoneish.rb8
-rw-r--r--app/models/deploy_token.rb5
-rw-r--r--app/views/shared/milestones/_issues_tab.html.haml5
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml6
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'