diff options
39 files changed, 1112 insertions, 205 deletions
@@ -259,7 +259,7 @@ gem 'asana', '0.10.2' gem 'ruby-fogbugz', '~> 0.2.1' # Kubernetes integration -gem 'kubeclient', '~> 4.6.0' +gem 'kubeclient', '~> 4.9.1' # Sanitize user input gem 'sanitize', '~> 5.2.1' diff --git a/Gemfile.lock b/Gemfile.lock index 3c4c55c6142..437a39d5ac4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -253,7 +253,7 @@ GEM discordrb-webhooks-blackst0ne (3.3.0) rest-client (~> 2.0) docile (1.3.2) - domain_name (0.5.20180417) + domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) doorkeeper (5.3.3) railties (>= 5) @@ -563,14 +563,15 @@ GEM html2text (0.2.0) nokogiri (~> 1.6) htmlentities (4.3.4) - http (4.2.0) + http (4.4.1) addressable (~> 2.3) http-cookie (~> 1.0) - http-form_data (~> 2.0) + http-form_data (~> 2.2) http-parser (~> 1.2.0) + http-accept (1.7.0) http-cookie (1.0.3) domain_name (~> 0.5) - http-form_data (2.1.1) + http-form_data (2.3.0) http-parser (1.2.1) ffi-compiler (>= 1.0, < 2.0) httparty (0.16.4) @@ -611,6 +612,9 @@ GEM hana (~> 1.3) regexp_parser (~> 1.5) uri_template (~> 0.7) + jsonpath (1.0.5) + multi_json + to_regexp (~> 0.2.1) jwt (2.1.0) kaminari (1.2.1) activesupport (>= 4.1.0) @@ -631,9 +635,10 @@ GEM rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) - kubeclient (4.6.0) + kubeclient (4.9.1) http (>= 3.0, < 5.0) - recursive-open-struct (~> 1.0, >= 1.0.4) + jsonpath (~> 1.0) + recursive-open-struct (~> 1.1, >= 1.1.1) rest-client (~> 2.0) launchy (2.4.3) addressable (~> 2.3) @@ -847,7 +852,7 @@ GEM pry (~> 0.13.0) pry-rails (0.3.9) pry (>= 0.10.4) - public_suffix (4.0.3) + public_suffix (4.0.6) pyu-ruby-sasl (0.0.3.3) raabro (1.1.6) rack (2.1.4) @@ -920,7 +925,7 @@ GEM re2 (1.2.0) recaptcha (4.13.1) json - recursive-open-struct (1.1.1) + recursive-open-struct (1.1.2) redis (4.1.3) redis-actionpack (5.2.0) actionpack (>= 5, < 7) @@ -951,7 +956,8 @@ GEM responders (3.0.0) actionpack (>= 5.0) railties (>= 5.0) - rest-client (2.0.2) + rest-client (2.1.0) + http-accept (>= 1.7.0, < 2.0) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) @@ -1145,6 +1151,7 @@ GEM timecop (0.9.1) timeliness (0.3.10) timfel-krb5-auth (0.8.3) + to_regexp (0.2.1) toml (0.2.0) parslet (~> 1.8.0) toml-rb (1.0.0) @@ -1161,7 +1168,7 @@ GEM uber (0.1.0) unf (0.1.4) unf_ext - unf_ext (0.0.7.5) + unf_ext (0.0.7.7) unicode-display_width (1.7.0) unicode_plot (0.0.4) enumerable-statistics (>= 2.0.1) @@ -1371,7 +1378,7 @@ DEPENDENCIES kaminari (~> 1.0) knapsack (~> 1.17) kramdown (~> 2.3.0) - kubeclient (~> 4.6.0) + kubeclient (~> 4.9.1) letter_opener_web (~> 1.3.4) license_finder (~> 6.0) licensee (~> 8.9) diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue index 4cb8d9ebd62..d4cc98e3743 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue @@ -1,7 +1,6 @@ <script> -import { GlProgressBar } from '@gitlab/ui'; +import { GlProgressBar, GlTooltipDirective } from '@gitlab/ui'; import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; -import tooltip from '../../../vue_shared/directives/tooltip'; import { s__, sprintf } from '~/locale'; export default { @@ -10,7 +9,7 @@ export default { GlProgressBar, }, directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, props: { timeSpent: { @@ -73,7 +72,7 @@ export default { <template> <div class="time-tracking-comparison-pane"> <div - v-tooltip + v-gl-tooltip :title="timeRemainingTooltip" :class="timeRemainingStatusClass" class="compare-meter" diff --git a/app/assets/stylesheets/page_bundles/environments.scss b/app/assets/stylesheets/page_bundles/environments.scss index 8f65a626e5f..871f118ea9d 100644 --- a/app/assets/stylesheets/page_bundles/environments.scss +++ b/app/assets/stylesheets/page_bundles/environments.scss @@ -8,24 +8,24 @@ .external-url, .dropdown-new { - color: $gl-text-color-secondary; + color: var(--gray-500, $gray-500); } .build-link, .ref-name { - color: $gl-text-color; + color: var(--gray-900, $gray-900); } .folder-icon { margin-right: 3px; - color: $gl-text-color-secondary; + color: var(--gray-500, $gray-500); display: inline-block; vertical-align: text-top; } .folder-name { cursor: pointer; - color: $gl-text-color-secondary; + color: var(--gray-500, $gray-500); display: inline-block; } @@ -74,17 +74,17 @@ .x-axis path, .y-axis path { - stroke: $gray-300; + stroke: var(--gray-300, $gray-300); } .label-x-axis-line, .label-y-axis-line { - stroke: $border-color; + stroke: var(--gray-100, $gray-100); } .y-axis { line { - stroke: $gray-300; + stroke: var(--gray-300, $gray-300); stroke-width: 1; } } @@ -94,13 +94,13 @@ } .rect-text-metric { - fill: $white; + fill: var(--white, $white); stroke-width: 1; - stroke: $gray-darkest; + stroke: var(--gray-600, $gray-600); } .rect-axis-text { - fill: $white; + fill: var(--white, $white); } .text-metric { @@ -108,18 +108,18 @@ } .selected-metric-line { - stroke: $gray-900; + stroke: var(--gray-900, $gray-900); stroke-width: 1; } .deployment-line { - stroke: $black; + stroke: var(--white, $white); stroke-width: 1; } .divider-line { stroke-width: 1; - stroke: $gray-darkest; + stroke: var(--gray-600, $gray-600); } .environments-actions { diff --git a/app/finders/concerns/time_frame_filter.rb b/app/finders/concerns/time_frame_filter.rb index e0baba25b64..d1ebed730f6 100644 --- a/app/finders/concerns/time_frame_filter.rb +++ b/app/finders/concerns/time_frame_filter.rb @@ -11,4 +11,11 @@ module TimeFrameFilter rescue ArgumentError items end + + def containing_date(items) + return items unless params[:containing_date] + + date = params[:containing_date].to_date + items.within_timeframe(date, date) + end end diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb index 16e59b31b36..5d2a54ac979 100644 --- a/app/finders/milestones_finder.rb +++ b/app/finders/milestones_finder.rb @@ -9,6 +9,8 @@ # order - Orders by field default due date asc. # title - filter by title. # state - filters by state. +# start_date & end_date - filters by timeframe (see TimeFrameFilter) +# containing_date - filters by point in time (see TimeFrameFilter) class MilestonesFinder include FinderMethods @@ -28,6 +30,7 @@ class MilestonesFinder items = by_search_title(items) items = by_state(items) items = by_timeframe(items) + items = containing_date(items) order(items) end diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb index 5d29d0bd437..2b8854fb4d0 100644 --- a/app/graphql/resolvers/base_resolver.rb +++ b/app/graphql/resolvers/base_resolver.rb @@ -6,6 +6,8 @@ module Resolvers include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Graphql::GlobalIDCompatibility + argument_class ::Types::BaseArgument + def self.single @single ||= Class.new(self) do def ready?(**args) diff --git a/app/graphql/resolvers/concerns/time_frame_arguments.rb b/app/graphql/resolvers/concerns/time_frame_arguments.rb index ef333dd05a5..56fa0943cb2 100644 --- a/app/graphql/resolvers/concerns/time_frame_arguments.rb +++ b/app/graphql/resolvers/concerns/time_frame_arguments.rb @@ -3,21 +3,33 @@ module TimeFrameArguments extend ActiveSupport::Concern + OVERLAPPING_TIMEFRAME_DESC = 'List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present)' + included do argument :start_date, Types::TimeType, required: false, - description: 'List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)' + description: OVERLAPPING_TIMEFRAME_DESC, + deprecated: { reason: 'Use timeframe.start', milestone: '14.0' } argument :end_date, Types::TimeType, required: false, - description: 'List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)' + description: OVERLAPPING_TIMEFRAME_DESC, + deprecated: { reason: 'Use timeframe.end', milestone: '14.0' } + + argument :timeframe, Types::TimeframeInputType, + required: false, + description: 'List items overlapping the given timeframe' end + # TODO: remove when the start_date and end_date arguments are removed def validate_timeframe_params!(args) - return unless args[:start_date].present? || args[:end_date].present? + return unless %i[start_date end_date timeframe].any? { |k| args[k].present? } + return if args[:timeframe] && %i[start_date end_date].all? { |k| args[k].nil? } error_message = - if args[:start_date].nil? || args[:end_date].nil? + if args[:timeframe].present? + "startDate and endDate are deprecated in favor of timeframe. Please use only timeframe." + elsif args[:start_date].nil? || args[:end_date].nil? "Both startDate and endDate must be present." elsif args[:start_date] > args[:end_date] "startDate is after endDate" diff --git a/app/graphql/resolvers/milestones_resolver.rb b/app/graphql/resolvers/milestones_resolver.rb index 5f80506c01b..84712b674db 100644 --- a/app/graphql/resolvers/milestones_resolver.rb +++ b/app/graphql/resolvers/milestones_resolver.rb @@ -13,6 +13,18 @@ module Resolvers required: false, description: 'Filter milestones by state' + argument :title, GraphQL::STRING_TYPE, + required: false, + description: 'The title of the milestone' + + argument :search_title, GraphQL::STRING_TYPE, + required: false, + description: 'A search string for the title' + + argument :containing_date, Types::TimeType, + required: false, + description: 'A date that the milestone contains' + type Types::MilestoneType, null: true def resolve(**args) @@ -29,9 +41,18 @@ module Resolvers { ids: parse_gids(args[:ids]), state: args[:state] || 'all', - start_date: args[:start_date], - end_date: args[:end_date] - }.merge(parent_id_parameters(args)) + title: args[:title], + search_title: args[:search_title], + containing_date: args[:containing_date] + }.merge!(timeframe_parameters(args)).merge!(parent_id_parameters(args)) + end + + def timeframe_parameters(args) + if args[:timeframe] + args[:timeframe].transform_keys { |k| :"#{k}_date" } + else + args.slice(:start_date, :end_date) + end end def parent diff --git a/app/graphql/types/base_argument.rb b/app/graphql/types/base_argument.rb new file mode 100644 index 00000000000..11774d0b59d --- /dev/null +++ b/app/graphql/types/base_argument.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + class BaseArgument < GraphQL::Schema::Argument + include GitlabStyleDeprecations + + def initialize(*args, **kwargs, &block) + kwargs = gitlab_deprecation(kwargs) + kwargs.delete(:deprecation_reason) + + super(*args, **kwargs, &block) + end + end +end diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index 1e72a4cddf5..5c8aabfe163 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -5,6 +5,8 @@ module Types prepend Gitlab::Graphql::Authorize include GitlabStyleDeprecations + argument_class ::Types::BaseArgument + DEFAULT_COMPLEXITY = 1 def initialize(*args, **kwargs, &block) diff --git a/app/graphql/types/date_type.rb b/app/graphql/types/date_type.rb new file mode 100644 index 00000000000..7129b75b8bb --- /dev/null +++ b/app/graphql/types/date_type.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Types + class DateType < BaseScalar + graphql_name 'Date' + description 'Date represented in ISO 8601' + + def self.coerce_input(value, ctx) + return if value.nil? + + Date.iso8601(value) + rescue ArgumentError, TypeError => e + raise GraphQL::CoercionError, e.message + end + + def self.coerce_result(value, ctx) + return if value.nil? + + value.to_date.iso8601 + end + end +end diff --git a/app/graphql/types/range_input_type.rb b/app/graphql/types/range_input_type.rb new file mode 100644 index 00000000000..766e523a99e --- /dev/null +++ b/app/graphql/types/range_input_type.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class RangeInputType < BaseInputObject + def self.[](type, closed = true) + @subtypes ||= {} + + @subtypes[[type, closed]] ||= Class.new(self) do + argument :start, type, + required: closed, + description: 'The start of the range' + + argument :end, type, + required: closed, + description: 'The end of the range' + end + end + + def prepare + if self[:end] && self[:start] && self[:end] < self[:start] + raise ::Gitlab::Graphql::Errors::ArgumentError, 'start must be before end' + end + + to_h + end + end + # rubocop: enable Graphql/AuthorizeTypes +end diff --git a/app/graphql/types/timeframe_input_type.rb b/app/graphql/types/timeframe_input_type.rb new file mode 100644 index 00000000000..79c1bc5cf01 --- /dev/null +++ b/app/graphql/types/timeframe_input_type.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class TimeframeInputType < RangeInputType[::Types::DateType] + graphql_name 'Timeframe' + description 'A time-frame defined as a closed inclusive range of two dates' + end + # rubocop: enable Graphql/AuthorizeTypes +end diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb index 3e2cf9031d0..23fd73f2904 100644 --- a/app/models/concerns/timebox.rb +++ b/app/models/concerns/timebox.rb @@ -73,6 +73,32 @@ module Timebox end end + # A timebox is within the timeframe (start_date, end_date) if it overlaps + # with that timeframe: + # + # [ timeframe ] + # ----| ................ # Not overlapping + # |--| ................ # Not overlapping + # ------|............... # Overlapping + # -----------------------| # Overlapping + # ---------|............ # Overlapping + # |-----|............ # Overlapping + # |--------------| # Overlapping + # |--------------------| # Overlapping + # ...|-----|...... # Overlapping + # .........|-----| # Overlapping + # .........|--------- # Overlapping + # |-------------------- # Overlapping + # .........|--------| # Overlapping + # ...............|--| # Overlapping + # ............... |-| # Not Overlapping + # ............... |-- # Not Overlapping + # + # where: . = in timeframe + # ---| no start + # |--- no end + # |--| defined start and end + # scope :within_timeframe, -> (start_date, end_date) do where('start_date is not NULL or due_date is not NULL') .where('start_date is NULL or start_date <= ?', end_date) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 55326b9a282..0a315ba8db2 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -46,6 +46,10 @@ class Milestone < ApplicationRecord state :active end + def self.min_chars_for_partial_matching + 2 + end + def self.reference_prefix '%' end diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 6ba363e6555..43aaa7cb405 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -7,7 +7,7 @@ .content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed .files-changed-inner - .inline-parallel-buttons.d-none.d-sm-none.d-md-block + .inline-parallel-buttons.d-none.d-md-block - if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? } = link_to _('Expand all'), url_for(safe_params.merge(expanded: 1, format: nil)), class: 'btn btn-default' - if show_whitespace_toggle diff --git a/changelogs/unreleased/264790-bs4-optimization-diff.yml b/changelogs/unreleased/264790-bs4-optimization-diff.yml new file mode 100644 index 00000000000..13a795d2568 --- /dev/null +++ b/changelogs/unreleased/264790-bs4-optimization-diff.yml @@ -0,0 +1,5 @@ +--- +title: Remove duplicated BS display properties from Diff's HAML +merge_request: 44848 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/ajk-graphql-milestone-filters.yml b/changelogs/unreleased/ajk-graphql-milestone-filters.yml new file mode 100644 index 00000000000..872cc180fd5 --- /dev/null +++ b/changelogs/unreleased/ajk-graphql-milestone-filters.yml @@ -0,0 +1,5 @@ +--- +title: Add filters on Milestone title in the GraphQL API +merge_request: 44208 +author: +type: changed diff --git a/changelogs/unreleased/kubeclient_491.yml b/changelogs/unreleased/kubeclient_491.yml new file mode 100644 index 00000000000..56f3c1403ed --- /dev/null +++ b/changelogs/unreleased/kubeclient_491.yml @@ -0,0 +1,6 @@ +--- +title: Bump kubeclient to 4.9.1 which includes ability to integrate Kubernetes clusters + where their API url is on a sub-path +merge_request: 44856 +author: +type: other diff --git a/db/structure.sql b/db/structure.sql index 38a89fe7dc4..602622e6418 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -20563,6 +20563,8 @@ CREATE INDEX index_issues_on_updated_at ON issues USING btree (updated_at); CREATE INDEX index_issues_on_updated_by_id ON issues USING btree (updated_by_id) WHERE (updated_by_id IS NOT NULL); +CREATE INDEX index_issues_project_id_issue_type_incident ON issues USING btree (project_id) WHERE (issue_type = 1); + CREATE UNIQUE INDEX index_jira_connect_installations_on_client_key ON jira_connect_installations USING btree (client_key); CREATE INDEX index_jira_connect_subscriptions_on_namespace_id ON jira_connect_subscriptions USING btree (namespace_id); diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index bd231c76b14..fd26d567574 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -1222,8 +1222,8 @@ type BoardEpic implements CurrentUserTodos & Noteable { before: String """ - List items within a time frame where items.end_date is between startDate and - endDate parameters (startDate parameter must be present) + List items overlapping a time frame defined by startDate..endDate (if one + date is provided, both must be present). Deprecated in 14.0: Use timeframe.end """ endDate: Time @@ -1273,8 +1273,9 @@ type BoardEpic implements CurrentUserTodos & Noteable { sort: EpicSort """ - List items within a time frame where items.start_date is between startDate - and endDate parameters (endDate parameter must be present) + List items overlapping a time frame defined by startDate..endDate (if one + date is provided, both must be present). Deprecated in 14.0: Use + timeframe.start """ startDate: Time @@ -1282,6 +1283,11 @@ type BoardEpic implements CurrentUserTodos & Noteable { Filter epics by state """ state: EpicState + + """ + List items overlapping the given timeframe + """ + timeframe: Timeframe ): EpicConnection """ @@ -4279,6 +4285,11 @@ enum DastSiteProfileValidationStatusEnum { } """ +Date represented in ISO 8601 +""" +scalar Date + +""" Autogenerated input type of DeleteAnnotation """ input DeleteAnnotationInput { @@ -5893,8 +5904,8 @@ type Epic implements CurrentUserTodos & Noteable { before: String """ - List items within a time frame where items.end_date is between startDate and - endDate parameters (startDate parameter must be present) + List items overlapping a time frame defined by startDate..endDate (if one + date is provided, both must be present). Deprecated in 14.0: Use timeframe.end """ endDate: Time @@ -5944,8 +5955,9 @@ type Epic implements CurrentUserTodos & Noteable { sort: EpicSort """ - List items within a time frame where items.start_date is between startDate - and endDate parameters (endDate parameter must be present) + List items overlapping a time frame defined by startDate..endDate (if one + date is provided, both must be present). Deprecated in 14.0: Use + timeframe.start """ startDate: Time @@ -5953,6 +5965,11 @@ type Epic implements CurrentUserTodos & Noteable { Filter epics by state """ state: EpicState + + """ + List items overlapping the given timeframe + """ + timeframe: Timeframe ): EpicConnection """ @@ -7381,8 +7398,8 @@ type Group { authorUsername: String """ - List items within a time frame where items.end_date is between startDate and - endDate parameters (startDate parameter must be present) + List items overlapping a time frame defined by startDate..endDate (if one + date is provided, both must be present). Deprecated in 14.0: Use timeframe.end """ endDate: Time @@ -7422,8 +7439,9 @@ type Group { sort: EpicSort """ - List items within a time frame where items.start_date is between startDate - and endDate parameters (endDate parameter must be present) + List items overlapping a time frame defined by startDate..endDate (if one + date is provided, both must be present). Deprecated in 14.0: Use + timeframe.start """ startDate: Time @@ -7431,6 +7449,11 @@ type Group { Filter epics by state """ state: EpicState + + """ + List items overlapping the given timeframe + """ + timeframe: Timeframe ): Epic """ @@ -7453,8 +7476,8 @@ type Group { before: String """ - List items within a time frame where items.end_date is between startDate and - endDate parameters (startDate parameter must be present) + List items overlapping a time frame defined by startDate..endDate (if one + date is provided, both must be present). Deprecated in 14.0: Use timeframe.end """ endDate: Time @@ -7504,8 +7527,9 @@ type Group { sort: EpicSort """ - List items within a time frame where items.start_date is between startDate - and endDate parameters (endDate parameter must be present) + List items overlapping a time frame defined by startDate..endDate (if one + date is provided, both must be present). Deprecated in 14.0: Use + timeframe.start """ startDate: Time @@ -7513,6 +7537,11 @@ type Group { Filter epics by state """ state: EpicState + + """ + List items overlapping the given timeframe + """ + timeframe: Timeframe ): EpicConnection """ @@ -7715,8 +7744,8 @@ type Group { before: String """ - List items within a time frame where items.end_date is between startDate and - endDate parameters (startDate parameter must be present) + List items overlapping a time frame defined by startDate..endDate (if one + date is provided, both must be present). Deprecated in 14.0: Use timeframe.end """ endDate: Time @@ -7746,8 +7775,9 @@ type Group { last: Int """ - List items within a time frame where items.start_date is between startDate - and endDate parameters (endDate parameter must be present) + List items overlapping a time frame defined by startDate..endDate (if one + date is provided, both must be present). Deprecated in 14.0: Use + timeframe.start """ startDate: Time @@ -7757,6 +7787,11 @@ type Group { state: IterationState """ + List items overlapping the given timeframe + """ + timeframe: Timeframe + + """ Fuzzy search by title """ title: String @@ -7912,8 +7947,13 @@ type Group { before: String """ - List items within a time frame where items.end_date is between startDate and - endDate parameters (startDate parameter must be present) + A date that the milestone contains + """ + containingDate: Time + + """ + List items overlapping a time frame defined by startDate..endDate (if one + date is provided, both must be present). Deprecated in 14.0: Use timeframe.end """ endDate: Time @@ -7938,8 +7978,14 @@ type Group { last: Int """ - List items within a time frame where items.start_date is between startDate - and endDate parameters (endDate parameter must be present) + A search string for the title + """ + searchTitle: String + + """ + List items overlapping a time frame defined by startDate..endDate (if one + date is provided, both must be present). Deprecated in 14.0: Use + timeframe.start """ startDate: Time @@ -7947,6 +7993,16 @@ type Group { Filter milestones by state """ state: MilestoneStateEnum + + """ + List items overlapping the given timeframe + """ + timeframe: Timeframe + + """ + The title of the milestone + """ + title: String ): MilestoneConnection """ @@ -13755,8 +13811,8 @@ type Project { before: String """ - List items within a time frame where items.end_date is between startDate and - endDate parameters (startDate parameter must be present) + List items overlapping a time frame defined by startDate..endDate (if one + date is provided, both must be present). Deprecated in 14.0: Use timeframe.end """ endDate: Time @@ -13786,8 +13842,9 @@ type Project { last: Int """ - List items within a time frame where items.start_date is between startDate - and endDate parameters (endDate parameter must be present) + List items overlapping a time frame defined by startDate..endDate (if one + date is provided, both must be present). Deprecated in 14.0: Use + timeframe.start """ startDate: Time @@ -13797,6 +13854,11 @@ type Project { state: IterationState """ + List items overlapping the given timeframe + """ + timeframe: Timeframe + + """ Fuzzy search by title """ title: String @@ -14004,8 +14066,13 @@ type Project { before: String """ - List items within a time frame where items.end_date is between startDate and - endDate parameters (startDate parameter must be present) + A date that the milestone contains + """ + containingDate: Time + + """ + List items overlapping a time frame defined by startDate..endDate (if one + date is provided, both must be present). Deprecated in 14.0: Use timeframe.end """ endDate: Time @@ -14030,8 +14097,14 @@ type Project { last: Int """ - List items within a time frame where items.start_date is between startDate - and endDate parameters (endDate parameter must be present) + A search string for the title + """ + searchTitle: String + + """ + List items overlapping a time frame defined by startDate..endDate (if one + date is provided, both must be present). Deprecated in 14.0: Use + timeframe.start """ startDate: Time @@ -14039,6 +14112,16 @@ type Project { Filter milestones by state """ state: MilestoneStateEnum + + """ + List items overlapping the given timeframe + """ + timeframe: Timeframe + + """ + The title of the milestone + """ + title: String ): MilestoneConnection """ @@ -18336,6 +18419,21 @@ interface TimeboxBurnupTimeSeriesInterface { burnupTimeSeries: [BurnupChartDailyTotals!] } +""" +A time-frame defined as a closed inclusive range of two dates +""" +input Timeframe { + """ + The end of the range + """ + end: Date! + + """ + The start of the range + """ + start: Date! +} + type Timelog { """ Timestamp of when the time tracked was spent at. Deprecated in 12.10: Use `spentAt` diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index f70814267ef..7e6266ca26c 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -3181,7 +3181,7 @@ "args": [ { "name": "startDate", - "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)", + "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.start", "type": { "kind": "SCALAR", "name": "Time", @@ -3191,7 +3191,7 @@ }, { "name": "endDate", - "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)", + "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.end", "type": { "kind": "SCALAR", "name": "Time", @@ -3200,6 +3200,16 @@ "defaultValue": null }, { + "name": "timeframe", + "description": "List items overlapping the given timeframe", + "type": { + "kind": "INPUT_OBJECT", + "name": "Timeframe", + "ofType": null + }, + "defaultValue": null + }, + { "name": "iid", "description": "IID of the epic, e.g., \"1\"", "type": { @@ -11567,6 +11577,16 @@ "possibleTypes": null }, { + "kind": "SCALAR", + "name": "Date", + "description": "Date represented in ISO 8601", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { "kind": "INPUT_OBJECT", "name": "DeleteAnnotationInput", "description": "Autogenerated input type of DeleteAnnotation", @@ -16290,7 +16310,7 @@ "args": [ { "name": "startDate", - "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)", + "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.start", "type": { "kind": "SCALAR", "name": "Time", @@ -16300,7 +16320,7 @@ }, { "name": "endDate", - "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)", + "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.end", "type": { "kind": "SCALAR", "name": "Time", @@ -16309,6 +16329,16 @@ "defaultValue": null }, { + "name": "timeframe", + "description": "List items overlapping the given timeframe", + "type": { + "kind": "INPUT_OBJECT", + "name": "Timeframe", + "ofType": null + }, + "defaultValue": null + }, + { "name": "iid", "description": "IID of the epic, e.g., \"1\"", "type": { @@ -20364,7 +20394,7 @@ "args": [ { "name": "startDate", - "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)", + "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.start", "type": { "kind": "SCALAR", "name": "Time", @@ -20374,7 +20404,7 @@ }, { "name": "endDate", - "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)", + "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.end", "type": { "kind": "SCALAR", "name": "Time", @@ -20383,6 +20413,16 @@ "defaultValue": null }, { + "name": "timeframe", + "description": "List items overlapping the given timeframe", + "type": { + "kind": "INPUT_OBJECT", + "name": "Timeframe", + "ofType": null + }, + "defaultValue": null + }, + { "name": "iid", "description": "IID of the epic, e.g., \"1\"", "type": { @@ -20503,7 +20543,7 @@ "args": [ { "name": "startDate", - "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)", + "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.start", "type": { "kind": "SCALAR", "name": "Time", @@ -20513,7 +20553,7 @@ }, { "name": "endDate", - "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)", + "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.end", "type": { "kind": "SCALAR", "name": "Time", @@ -20522,6 +20562,16 @@ "defaultValue": null }, { + "name": "timeframe", + "description": "List items overlapping the given timeframe", + "type": { + "kind": "INPUT_OBJECT", + "name": "Timeframe", + "ofType": null + }, + "defaultValue": null + }, + { "name": "iid", "description": "IID of the epic, e.g., \"1\"", "type": { @@ -21134,7 +21184,7 @@ "args": [ { "name": "startDate", - "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)", + "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.start", "type": { "kind": "SCALAR", "name": "Time", @@ -21144,7 +21194,7 @@ }, { "name": "endDate", - "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)", + "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.end", "type": { "kind": "SCALAR", "name": "Time", @@ -21153,6 +21203,16 @@ "defaultValue": null }, { + "name": "timeframe", + "description": "List items overlapping the given timeframe", + "type": { + "kind": "INPUT_OBJECT", + "name": "Timeframe", + "ofType": null + }, + "defaultValue": null + }, + { "name": "state", "description": "Filter iterations by state", "type": { @@ -21580,7 +21640,7 @@ "args": [ { "name": "startDate", - "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)", + "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.start", "type": { "kind": "SCALAR", "name": "Time", @@ -21590,7 +21650,7 @@ }, { "name": "endDate", - "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)", + "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.end", "type": { "kind": "SCALAR", "name": "Time", @@ -21599,6 +21659,16 @@ "defaultValue": null }, { + "name": "timeframe", + "description": "List items overlapping the given timeframe", + "type": { + "kind": "INPUT_OBJECT", + "name": "Timeframe", + "ofType": null + }, + "defaultValue": null + }, + { "name": "ids", "description": "Array of global milestone IDs, e.g., \"gid://gitlab/Milestone/1\"", "type": { @@ -21627,6 +21697,36 @@ "defaultValue": null }, { + "name": "title", + "description": "The title of the milestone", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "searchTitle", + "description": "A search string for the title", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "containingDate", + "description": "A date that the milestone contains", + "type": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + }, + "defaultValue": null + }, + { "name": "includeDescendants", "description": "Also return milestones in all subgroups and subprojects", "type": { @@ -40165,7 +40265,7 @@ "args": [ { "name": "startDate", - "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)", + "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.start", "type": { "kind": "SCALAR", "name": "Time", @@ -40175,7 +40275,7 @@ }, { "name": "endDate", - "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)", + "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.end", "type": { "kind": "SCALAR", "name": "Time", @@ -40184,6 +40284,16 @@ "defaultValue": null }, { + "name": "timeframe", + "description": "List items overlapping the given timeframe", + "type": { + "kind": "INPUT_OBJECT", + "name": "Timeframe", + "ofType": null + }, + "defaultValue": null + }, + { "name": "state", "description": "Filter iterations by state", "type": { @@ -40737,7 +40847,7 @@ "args": [ { "name": "startDate", - "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)", + "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.start", "type": { "kind": "SCALAR", "name": "Time", @@ -40747,7 +40857,7 @@ }, { "name": "endDate", - "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)", + "description": "List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present). Deprecated in 14.0: Use timeframe.end", "type": { "kind": "SCALAR", "name": "Time", @@ -40756,6 +40866,16 @@ "defaultValue": null }, { + "name": "timeframe", + "description": "List items overlapping the given timeframe", + "type": { + "kind": "INPUT_OBJECT", + "name": "Timeframe", + "ofType": null + }, + "defaultValue": null + }, + { "name": "ids", "description": "Array of global milestone IDs, e.g., \"gid://gitlab/Milestone/1\"", "type": { @@ -40784,6 +40904,36 @@ "defaultValue": null }, { + "name": "title", + "description": "The title of the milestone", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "searchTitle", + "description": "A search string for the title", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "containingDate", + "description": "A date that the milestone contains", + "type": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + }, + "defaultValue": null + }, + { "name": "includeAncestors", "description": "Also return milestones in the project's parent group and its ancestors", "type": { @@ -53363,6 +53513,45 @@ ] }, { + "kind": "INPUT_OBJECT", + "name": "Timeframe", + "description": "A time-frame defined as a closed inclusive range of two dates", + "fields": null, + "inputFields": [ + { + "name": "start", + "description": "The start of the range", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "end", + "description": "The end of the range", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { "kind": "OBJECT", "name": "Timelog", "description": null, diff --git a/doc/api/lint.md b/doc/api/lint.md index 652a5289f13..addc8d0f9a3 100644 --- a/doc/api/lint.md +++ b/doc/api/lint.md @@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # CI Lint API -## Validate the CI YAML config +## Validate the CI YAML configuration > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5953) in GitLab 8.12. @@ -26,7 +26,7 @@ POST /ci/lint curl --header "Content-Type: application/json" "https://gitlab.example.com/api/v4/ci/lint" --data '{"content": "{ \"image\": \"ruby:2.6\", \"services\": [\"postgres\"], \"before_script\": [\"bundle install\", \"bundle exec rake db:create\"], \"variables\": {\"DB_NAME\": \"postgres\"}, \"types\": [\"test\", \"deploy\", \"notify\"], \"rspec\": { \"script\": \"rake spec\", \"tags\": [\"ruby\", \"postgres\"], \"only\": [\"branches\"]}}"}' ``` -Be sure to paste the exact contents of your GitLab CI/CD YAML config because YAML +Be sure to paste the exact contents of your GitLab CI/CD YAML configuration because YAML is very sensitive about indentation and spacing. Example responses: @@ -61,7 +61,10 @@ Example responses: ### YAML expansion -The expansion only works for CI configurations that don't have local [includes](../ci/yaml/README.md#include). +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29568) in GitLab 13.5. + +The CI lint returns an expanded version of the configuration. The expansion does not +work for CI configuration added with [`include: local`](../ci/yaml/README.md#includelocal). Example contents of a `.gitlab-ci.yml` passed to the CI Lint API with `include_merged_yaml` set as true: @@ -119,7 +122,7 @@ curl "https://gitlab.example.com/api/v4/projects/:id/ci/lint" Example responses: -- Valid config: +- Valid configuration: ```json { @@ -130,7 +133,7 @@ Example responses: } ``` -- Invalid config: +- Invalid configuration: ```json { diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 0656c76236d..920cf92fe88 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -2622,10 +2622,12 @@ The `stop_review_app` job is **required** to have the following keywords defined - `environment:action` Additionally, both jobs should have matching [`rules`](../yaml/README.md#onlyexcept-basic) -or [`only/except`](../yaml/README.md#onlyexcept-basic) configuration. In the example -above, if the configuration is not identical, the `stop_review_app` job might not be -included in all pipelines that include the `review_app` job, and it is not -possible to trigger the `action: stop` to stop the environment automatically. +or [`only/except`](../yaml/README.md#onlyexcept-basic) configuration. + +In the example above, if the configuration is not identical: + +- The `stop_review_app` job might not be included in all pipelines that include the `review_app` job. +- It is not possible to trigger the `action: stop` to stop the environment automatically. #### `environment:auto_stop_in` @@ -2774,17 +2776,17 @@ rspec: - binaries/ ``` -Note that since cache is shared between jobs, if you're using different -paths for different jobs, you should also set a different **cache:key** -otherwise cache content can be overwritten. +The cache is shared between jobs, so if you're using different +paths for different jobs, you should also set a different `cache:key`. +Otherwise cache content can be overwritten. #### `cache:key` > Introduced in GitLab Runner v1.0.0. -Since the cache is shared between jobs, if you're using different -paths for different jobs, you should also set a different `cache:key` -otherwise cache content can be overwritten. +The cache is shared between jobs, so if you're using different +paths for different jobs, you should also set a different `cache:key`. +Otherwise cache content can be overwritten. The `key` parameter defines the affinity of caching between jobs, to have a single cache for all jobs, cache per-job, cache per-branch @@ -2973,13 +2975,13 @@ rspec: - bundle exec rspec ... ``` -This helps to speed up job execution and reduce load on the cache server, -especially when you have a large number of cache-using jobs executing in +This helps to speed up job execution and reduce load on the cache server. +It is especially helpful when you have a large number of cache-using jobs executing in parallel. -Additionally, if you have a job that unconditionally recreates the cache without -reference to its previous contents, you can use `policy: push` in that job to -skip the download step. +If you have a job that unconditionally recreates the cache without +referring to its previous contents, you can skip the download step. +To do so, add `policy: push` to the job. ### `artifacts` @@ -2992,7 +2994,7 @@ skip the download step. `artifacts` is used to specify a list of files and directories that are attached to the job when it [succeeds, fails, or always](#artifactswhen). -The artifacts are sent to GitLab after the job finishes and are +The artifacts are sent to GitLab after the job finishes. They are available for download in the GitLab UI if the size is not larger than the [maximum artifact size](../../user/gitlab_com/index.md#gitlab-cicd). @@ -3341,19 +3343,22 @@ These are the available report types: > Introduced in GitLab 8.6 and GitLab Runner v1.1.1. -By default, all [`artifacts`](#artifacts) from all previous [stages](#stages) -are passed, but you can use the `dependencies` parameter to define a limited -list of jobs (or no jobs) to fetch artifacts from. +By default, all [`artifacts`](#artifacts) from previous [stages](#stages) +are passed to each job. However, you can use the `dependencies` parameter to +define a limited list of jobs to fetch artifacts from. You can also set a job to download no artifacts at all. To use this feature, define `dependencies` in context of the job and pass a list of all previous jobs the artifacts should be downloaded from. -You can only define jobs from stages that are executed before the current one. -An error is shown if you define jobs from the current stage or next ones. -Defining an empty array skips downloading any artifacts for that job. -The status of the previous job is not considered when using `dependencies`, so -if it failed or it's a manual job that was not run, no error occurs. -In the following example, we define two jobs with artifacts, `build:osx` and +You can define jobs from stages that were executed before the current one. +An error occurs if you define jobs from the current or an upcoming stage. + +To prevent a job from downloading artifacts, define an empty array. + +When you use `dependencies`, the status of the previous job is not considered. +If a job fails or it's a manual job that was not run, no error occurs. + +The following example defines two jobs with artifacts: `build:osx` and `build:linux`. When the `test:osx` is executed, the artifacts from `build:osx` are downloaded and extracted in the context of the build. The same happens for `test:linux` and artifacts from `build:linux`. @@ -3435,14 +3440,14 @@ job1: Use `retry` to configure how many times a job is retried in case of a failure. -When a job fails and has `retry` configured, the job is processed again, -up to the amount of times specified by the `retry` keyword. +When a job fails, the job is processed again, +until the limit specified by the `retry` keyword is reached. -If `retry` is set to 2, and a job succeeds in a second run (first retry), it is not tried -again. `retry` value has to be a positive integer, equal to or larger than 0, but -less than or equal to 2 (two retries maximum, three runs in total). +If `retry` is set to `2`, and a job succeeds in a second run (first retry), it is not retried. +The `retry` value must be a positive integer, from `0` to `2` +(two retries maximum, three runs in total). -A simple example to retry in all failure cases: +This example retries all failure cases: ```yaml test: @@ -3640,9 +3645,9 @@ You can use this keyword to create two different types of downstream pipelines: - [Multi-project pipelines](../multi_project_pipelines.md#creating-multi-project-pipelines-from-gitlab-ciyml) - [Child pipelines](../parent_child_pipelines.md) -[Since GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/issues/197140/), you can -see which job triggered a downstream pipeline by hovering your mouse cursor over -the downstream pipeline job in the [pipeline graph](../pipelines/index.md#visualize-pipelines). +[In GitLab 13.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/197140/), you can +view which job triggered a downstream pipeline. In the [pipeline graph](../pipelines/index.md#visualize-pipelines), +hover over the downstream pipeline job. In [GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/201938) and later, you can use [`when:manual`](#whenmanual) in the same job as `trigger`. In GitLab 13.4 and @@ -3782,7 +3787,7 @@ trigger_job: This setting can help keep your pipeline execution linear. In the example above, jobs from subsequent stages wait for the triggered pipeline to successfully complete before -starting, at the cost of reduced parallelization. +starting, which reduces parallelization. #### Trigger a pipeline by API call @@ -3859,7 +3864,7 @@ to semaphores in other programming languages. When the `resource_group` key is defined for a job in `.gitlab-ci.yml`, job executions are mutually exclusive across different pipelines for the same project. If multiple jobs belonging to the same resource group are enqueued simultaneously, -only one of the jobs is picked by the runner, and the other jobs wait until the +only one of the jobs is picked by the runner. The other jobs wait until the `resource_group` is free. Here is a simple example: @@ -3870,9 +3875,7 @@ deploy-to-production: resource_group: production ``` -In this case, if a `deploy-to-production` job is running in a pipeline, and a new -`deploy-to-production` job is created in a different pipeline, it doesn't run until -the currently running/pending `deploy-to-production` job finishes. As a result, +In this case, two `deploy-to-production` jobs in two separate pipelines can never run at the same time. As a result, you can ensure that concurrent deployments never happen to the production environment. There can be multiple `resource_group`s defined per environment. A good use case for this diff --git a/doc/user/admin_area/img/admin_wrench.png b/doc/user/admin_area/img/admin_wrench.png Binary files differdeleted file mode 100644 index 17eee143e87..00000000000 --- a/doc/user/admin_area/img/admin_wrench.png +++ /dev/null diff --git a/doc/user/admin_area/license.md b/doc/user/admin_area/license.md index ecbc615f56a..c37a61d6748 100644 --- a/doc/user/admin_area/license.md +++ b/doc/user/admin_area/license.md @@ -8,130 +8,126 @@ type: howto # Activate GitLab EE with a license **(STARTER ONLY)** To activate all GitLab Enterprise Edition (EE) functionality, you need to upload -a license. Once you've received your license from GitLab Inc., you can upload it -by **signing into your GitLab instance as an admin** or add it at +a license. After you've received your license from GitLab Inc., you can upload it +by **signing into your GitLab instance as an admin** or adding it at installation time. -The license has the form of a base64 encoded ASCII text with a `.gitlab-license` -extension and can be obtained when you [purchase one](https://about.gitlab.com/pricing/) or when you sign -up for a [free trial](https://about.gitlab.com/free-trial/). +The license is a base64-encoded ASCII text file with a `.gitlab-license` +extension. You can obtain the file by [purchasing a license](https://about.gitlab.com/pricing/) +or by signing up for a [free trial](https://about.gitlab.com/free-trial/). -NOTE: **Note:** As of GitLab Enterprise Edition 9.4.0, a newly-installed instance without an -uploaded license will only have the Core features active. A trial license will -activate all Ultimate features, but after +uploaded license only has the Core features active. A trial license +activates all Ultimate features, but after [the trial expires](#what-happens-when-your-license-expires), some functionality -will be locked. +is locked. ## Uploading your license The very first time you visit your GitLab EE installation signed in as an admin, you should see a note urging you to upload a license with a link that takes you -straight to **Admin Area > License**. +to **Admin Area > License**. Otherwise, you can: -1. Navigate manually to the **Admin Area** by clicking the wrench icon in the menu bar. +1. Navigate manually to the **Admin Area** by clicking the wrench (**{admin}**) icon in the menu bar. - ![Admin Area icon](img/admin_wrench.png) - -1. And then going to the **License** tab and click on **Upload New License**. +1. Navigate to the **License** tab, and click **Upload New License**. ![License Admin Area](img/license_admin_area.png) -1. If you've received a `.gitlab-license` file, you should have already downloaded - it in your local machine. You can then upload it directly by choosing the - license file and clicking the **Upload license** button. In the image below, - you can see that the selected license file is named `GitLab.gitlab-license`. + - *If you've received a `.gitlab-license` file,* you should have already downloaded + it in your local machine. You can then upload it directly by choosing the + license file and clicking the **Upload license** button. In the image below, + the selected license file is named `GitLab.gitlab-license`. - ![Upload license](img/license_upload.png) + ![Upload license](img/license_upload.png) - If you've received your license as plain text, you need to select the - "Enter license key" option, copy the license, paste it into the "License key" - field and click **Upload license**. + - *If you've received your license as plain text,* select the + **Enter license key** option, copy the license, paste it into the **License key** + field, and click **Upload license**. ## Add your license at install time -A license can be automatically imported at install time, by placing a file named -`Gitlab.gitlab-license` in `/etc/gitlab/` for Omnibus, or `config/` for source installations. +A license can be automatically imported at install time by placing a file named +`Gitlab.gitlab-license` in `/etc/gitlab/` for Omnibus GitLab, or `config/` for source installations. -It is also possible to specify a custom location and filename for the license. +You can also specify a custom location and filename for the license: -Source installations should set the `GITLAB_LICENSE_FILE` environment -variable with the path to a valid GitLab Enterprise Edition license. +- Source installations should set the `GITLAB_LICENSE_FILE` environment + variable with the path to a valid GitLab Enterprise Edition license. -```shell -export GITLAB_LICENSE_FILE="/path/to/license/file" -``` + ```shell + export GITLAB_LICENSE_FILE="/path/to/license/file" + ``` -Omnibus installations should add this entry to `gitlab.rb`: +- Omnibus GitLab installations should add this entry to `gitlab.rb`: -```ruby -gitlab_rails['initial_license_file'] = "/path/to/license/file" -``` + ```ruby + gitlab_rails['initial_license_file'] = "/path/to/license/file" + ``` CAUTION: **Caution:** -These methods will only add a license at the time of installation. Use the -Admin Area in the web user interface to renew or upgrade licenses. +These methods only add a license at the time of installation. Use the +**{admin}** **Admin Area** in the web user interface to renew or upgrade licenses. --- -Once the license is uploaded, all GitLab Enterprise Edition functionality -will be active until the end of the license period. When that period ends, the +After the license is uploaded, all GitLab Enterprise Edition functionality +is active until the end of the license period. When that period ends, the instance will [fall back](#what-happens-when-your-license-expires) to Core-only functionality. -You can review the license details at any time in the License section of the -Admin Area. +You can review the license details at any time in the **License** section of the +**Admin Area**. ![License details](img/license_details.png) ## Notification before the license expires -One month before the license expires, a message informing when the expiration -is due to, will start appearing to GitLab admins. Make sure that you update your -license, otherwise you will miss all the paid features if it expires. +One month before the license expires, a message informing about the expiration +date is displayed to GitLab admins. Make sure that you update your +license, otherwise you miss all the paid features if your license expires. ## What happens when your license expires -In case your license expires, GitLab will lock down some features like Git pushes, -issue creation, etc., and a message to inform of the expired license will be -presented to all admins. +In case your license expires, GitLab locks down some features like Git pushes, +and issue creation, and displays a message to all admins to inform of the expired license. -In order to get back all the previous functionality, a new license must be uploaded. -To fall back to having only the Core features active, you'll need to delete the +To get back all the previous functionality, you must upload a new license. +To fall back to having only the Core features active, you must delete the expired license(s). ### Remove a license To remove a license from a self-managed instance: -1. Go to the [Admin Area](index.md) (click the wrench in the top navigation bar). +1. In the top navigation bar, click the **{admin}** wrench icon to navigate to the [Admin Area](index.md). 1. Click **License** in the left sidebar. 1. Click **Remove License**. ## License history -It's possible to upload and view more than one license, -but only the latest license will be used as the active license. +You can upload and view more than one license, +but only the latest license is used as the active license. ## Troubleshooting ### There is no License tab in the Admin Area -If you originally installed Community Edition rather than Enterprise Edition you will need to +If you originally installed Community Edition rather than Enterprise Edition you must [upgrade to Enterprise Edition](../../update/README.md#community-to-enterprise-edition) before uploading your license. -GitLab.com users cannot upload and use a self-managed license. If you -wish to use paid features on GitLab.com, a separate subscription may be -[purchased](../../subscriptions/gitlab_com/index.md). +GitLab.com users can't upload and use a self-managed license. If you +want to use paid features on GitLab.com, you can +[purchase a separate subscription](../../subscriptions/gitlab_com/index.md). ### Users exceed license limit upon renewal -If you've added new users to your GitLab instance prior to renewal you may need to -purchase additional seats to cover those users. If this is the case and a license -without enough users is uploaded a message is displayed prompting you to purchase +If you've added new users to your GitLab instance prior to renewal, you may need to +purchase additional seats to cover those users. If this is the case, and a license +without enough users is uploaded, GitLab displays a message prompting you to purchase additional users. More information on how to determine the required number of users and how to add additional seats can be found in the [licensing FAQ](https://about.gitlab.com/pricing/licensing-faq/). diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb index 8ac7285d70c..5781bf8a7f0 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb @@ -37,8 +37,10 @@ module QA project.wait_for_push_new_branch # Check that the push worked - expect(page).to have_content(file_name) - expect(page).to have_content(file_content) + Page::Project::Show.perform do |project_page| + expect(project_page).to have_file(file_name) + expect(project_page).to have_readme_content(file_content) + end # And check that the correct Git protocol was used expect(git_protocol_reported).to eq(git_protocol) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb index 83945a09587..2d86cfdbaf8 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb @@ -37,8 +37,10 @@ module QA # Check that the target project has the commit from the source target_project.visit! - expect(page).to have_content('README.md') - expect(page).to have_content('The rendered file could not be displayed because it is stored in LFS') + Page::Project::Show.perform do |project_page| + expect(project_page).to have_file('README.md') + expect(project_page).to have_readme_content('The rendered file could not be displayed because it is stored in LFS') + end end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb index 8b6973e6cea..cf14017b7f1 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb @@ -36,8 +36,10 @@ module QA project.visit! - expect(page).to have_content('README.md') - expect(page).to have_content("This is a test project named #{project.name}") + Page::Project::Show.perform do |project_page| + expect(project_page).to have_file('README.md') + expect(project_page).to have_readme_content("This is a test project named #{project.name}") + end end end end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 4b4cb444903..332c90df6d7 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -229,7 +229,7 @@ RSpec.describe 'Issue Boards', :js do end context 'time tracking' do - let(:compare_meter_tooltip) { find('.time-tracking .time-tracking-content .compare-meter')['data-original-title'] } + let(:compare_meter_tooltip) { find('.time-tracking .time-tracking-content .compare-meter')['title'] } before do issue2.timelogs.create(time_spent: 14400, user: user) diff --git a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js index 1f028f74423..5307be0bf58 100644 --- a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js +++ b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js @@ -155,8 +155,7 @@ describe('Issuable Time Tracker', () => { it('should show the correct tooltip text', done => { Vue.nextTick(() => { expect(vm.showComparisonState).toBe(true); - const $title = vm.$el.querySelector('.time-tracking-content .compare-meter').dataset - .originalTitle; + const $title = vm.$el.querySelector('.time-tracking-content .compare-meter').title; expect($title).toBe('Time remaining: 26h 23m'); done(); diff --git a/spec/graphql/resolvers/group_milestones_resolver_spec.rb b/spec/graphql/resolvers/group_milestones_resolver_spec.rb index 05d0ec38192..d8ff8e9c1f2 100644 --- a/spec/graphql/resolvers/group_milestones_resolver_spec.rb +++ b/spec/graphql/resolvers/group_milestones_resolver_spec.rb @@ -15,6 +15,12 @@ RSpec.describe Resolvers::GroupMilestonesResolver do let_it_be(:now) { Time.now } let_it_be(:group) { create(:group, :private) } + def args(**arguments) + satisfy("contain only #{arguments.inspect}") do |passed| + expect(passed.compact).to match(arguments) + end + end + before_all do group.add_developer(current_user) end @@ -30,7 +36,7 @@ RSpec.describe Resolvers::GroupMilestonesResolver do context 'without parameters' do it 'calls MilestonesFinder to retrieve all milestones' do expect(MilestonesFinder).to receive(:new) - .with(ids: nil, group_ids: group.id, state: 'all', start_date: nil, end_date: nil) + .with(args(group_ids: group.id, state: 'all')) .and_call_original resolve_group_milestones @@ -43,11 +49,22 @@ RSpec.describe Resolvers::GroupMilestonesResolver do end_date = start_date + 1.hour expect(MilestonesFinder).to receive(:new) - .with(ids: nil, group_ids: group.id, state: 'closed', start_date: start_date, end_date: end_date) + .with(args(group_ids: group.id, state: 'closed', start_date: start_date, end_date: end_date)) .and_call_original resolve_group_milestones(start_date: start_date, end_date: end_date, state: 'closed') end + + it 'understands the timeframe argument' do + start_date = now + end_date = start_date + 1.hour + + expect(MilestonesFinder).to receive(:new) + .with(args(group_ids: group.id, state: 'closed', start_date: start_date, end_date: end_date)) + .and_call_original + + resolve_group_milestones(timeframe: { start: start_date, end: end_date }, state: 'closed') + end end context 'by ids' do @@ -55,7 +72,7 @@ RSpec.describe Resolvers::GroupMilestonesResolver do milestone = create(:milestone, group: group) expect(MilestonesFinder).to receive(:new) - .with(ids: [milestone.id.to_s], group_ids: group.id, state: 'all', start_date: nil, end_date: nil) + .with(args(ids: [milestone.id.to_s], group_ids: group.id, state: 'all')) .and_call_original resolve_group_milestones(ids: [milestone.to_global_id]) diff --git a/spec/graphql/resolvers/project_milestones_resolver_spec.rb b/spec/graphql/resolvers/project_milestones_resolver_spec.rb index e0b250cfe7c..b641a54393e 100644 --- a/spec/graphql/resolvers/project_milestones_resolver_spec.rb +++ b/spec/graphql/resolvers/project_milestones_resolver_spec.rb @@ -13,13 +13,19 @@ RSpec.describe Resolvers::ProjectMilestonesResolver do project.add_developer(current_user) end + def args(**arguments) + satisfy("contain only #{arguments.inspect}") do |passed| + expect(passed.compact).to match(arguments) + end + end + def resolve_project_milestones(args = {}, context = { current_user: current_user }) resolve(described_class, obj: project, args: args, ctx: context) end it 'calls MilestonesFinder to retrieve all milestones' do expect(MilestonesFinder).to receive(:new) - .with(ids: nil, project_ids: project.id, state: 'all', start_date: nil, end_date: nil) + .with(args(project_ids: project.id, state: 'all')) .and_call_original resolve_project_milestones @@ -36,7 +42,7 @@ RSpec.describe Resolvers::ProjectMilestonesResolver do it 'calls MilestonesFinder with correct parameters' do expect(MilestonesFinder).to receive(:new) - .with(ids: nil, project_ids: project.id, group_ids: contain_exactly(group, parent_group), state: 'all', start_date: nil, end_date: nil) + .with(args(project_ids: project.id, group_ids: contain_exactly(group, parent_group), state: 'all')) .and_call_original resolve_project_milestones(include_ancestors: true) @@ -48,7 +54,7 @@ RSpec.describe Resolvers::ProjectMilestonesResolver do milestone = create(:milestone, project: project) expect(MilestonesFinder).to receive(:new) - .with(ids: [milestone.id.to_s], project_ids: project.id, state: 'all', start_date: nil, end_date: nil) + .with(args(ids: [milestone.id.to_s], project_ids: project.id, state: 'all')) .and_call_original resolve_project_milestones(ids: [milestone.to_global_id]) @@ -58,7 +64,7 @@ RSpec.describe Resolvers::ProjectMilestonesResolver do context 'by state' do it 'calls MilestonesFinder with correct parameters' do expect(MilestonesFinder).to receive(:new) - .with(ids: nil, project_ids: project.id, state: 'closed', start_date: nil, end_date: nil) + .with(args(project_ids: project.id, state: 'closed')) .and_call_original resolve_project_milestones(state: 'closed') @@ -72,7 +78,7 @@ RSpec.describe Resolvers::ProjectMilestonesResolver do end_date = Time.now + 5.days expect(MilestonesFinder).to receive(:new) - .with(ids: nil, project_ids: project.id, state: 'all', start_date: start_date, end_date: end_date) + .with(args(project_ids: project.id, state: 'all', start_date: start_date, end_date: end_date)) .and_call_original resolve_project_milestones(start_date: start_date, end_date: end_date) @@ -102,6 +108,51 @@ RSpec.describe Resolvers::ProjectMilestonesResolver do end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /Both startDate and endDate/) end end + + context 'when passing a timeframe' do + it 'calls MilestonesFinder with correct parameters' do + start_date = Time.now + end_date = Time.now + 5.days + + expect(MilestonesFinder).to receive(:new) + .with(args(project_ids: project.id, state: 'all', start_date: start_date, end_date: end_date)) + .and_call_original + + resolve_project_milestones(timeframe: { start: start_date, end: end_date }) + end + end + end + + context 'when title is present' do + it 'calls MilestonesFinder with correct parameters' do + expect(MilestonesFinder).to receive(:new) + .with(args(title: '13.5', state: 'all', project_ids: project.id)) + .and_call_original + + resolve_project_milestones(title: '13.5') + end + end + + context 'when search_title is present' do + it 'calls MilestonesFinder with correct parameters' do + expect(MilestonesFinder).to receive(:new) + .with(args(search_title: '13', state: 'all', project_ids: project.id)) + .and_call_original + + resolve_project_milestones(search_title: '13') + end + end + + context 'when containing date is present' do + it 'calls MilestonesFinder with correct parameters' do + t = Time.now + + expect(MilestonesFinder).to receive(:new) + .with(args(containing_date: t, state: 'all', project_ids: project.id)) + .and_call_original + + resolve_project_milestones(containing_date: t) + end end context 'when user cannot read milestones' do diff --git a/spec/graphql/types/range_input_type_spec.rb b/spec/graphql/types/range_input_type_spec.rb new file mode 100644 index 00000000000..aa6fd72cf13 --- /dev/null +++ b/spec/graphql/types/range_input_type_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Types::RangeInputType do + let(:of_integer) { ::GraphQL::INT_TYPE } + + context 'parameterized on Integer' do + let(:type) { described_class[of_integer] } + + it 'accepts start and end' do + input = { start: 1, end: 10 } + output = { start: 1, end: 10 } + + expect(type.coerce_isolated_input(input)).to eq(output) + end + + it 'rejects inverted ranges' do + input = { start: 10, end: 1 } + + expect { type.coerce_isolated_input(input) }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) + end + end + + it 'follows expected subtyping relationships for instances' do + context = GraphQL::Query::Context.new( + query: OpenStruct.new(schema: nil), + values: {}, + object: nil + ) + instance = described_class[of_integer].new(context: context, defaults_used: [], ruby_kwargs: {}) + + expect(instance).to be_a_kind_of(described_class) + expect(instance).to be_a_kind_of(described_class[of_integer]) + expect(instance).not_to be_a_kind_of(described_class[GraphQL::ID_TYPE]) + end + + it 'follows expected subtyping relationships for classes' do + expect(described_class[of_integer]).to be < described_class + expect(described_class[of_integer]).not_to be < described_class[GraphQL::ID_TYPE] + expect(described_class[of_integer]).not_to be < described_class[of_integer, false] + end +end diff --git a/spec/graphql/types/timeframe_type_spec.rb b/spec/graphql/types/timeframe_type_spec.rb new file mode 100644 index 00000000000..dfde3242897 --- /dev/null +++ b/spec/graphql/types/timeframe_type_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['Timeframe'] do + let(:input) { { start: "2018-06-04", end: "2020-10-06" } } + let(:output) { { start: Date.parse(input[:start]), end: Date.parse(input[:end]) } } + + it 'coerces ISO-dates into Time objects' do + expect(described_class.coerce_isolated_input(input)).to eq(output) + end + + it 'rejects invalid input' do + input[:start] = 'foo' + + expect { described_class.coerce_isolated_input(input) } + .to raise_error(GraphQL::CoercionError) + end + + it 'accepts times as input' do + with_time = input.merge(start: '2018-06-04T13:48:14Z') + + expect(described_class.coerce_isolated_input(with_time)).to eq(output) + end + + it 'requires both ends of the range' do + types = described_class.arguments.slice('start', 'end').values.map(&:type) + + expect(types).to all(be_non_null) + end + + it 'rejects invalid range' do + input.merge!(start: input[:end], end: input[:start]) + + expect { described_class.coerce_isolated_input(input) } + .to raise_error(::Gitlab::Graphql::Errors::ArgumentError, 'start must be before end') + end +end diff --git a/spec/requests/api/graphql/project/milestones_spec.rb b/spec/requests/api/graphql/project/milestones_spec.rb new file mode 100644 index 00000000000..2fede4c7285 --- /dev/null +++ b/spec/requests/api/graphql/project/milestones_spec.rb @@ -0,0 +1,202 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting milestone listings nested in a project' do + include GraphqlHelpers + + let_it_be(:today) { Time.now.utc.to_date } + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:current_user) { create(:user) } + + let_it_be(:no_dates) { create(:milestone, project: project, title: 'no dates') } + let_it_be(:no_end) { create(:milestone, project: project, title: 'no end', start_date: today - 10.days) } + let_it_be(:no_start) { create(:milestone, project: project, title: 'no start', due_date: today - 5.days) } + let_it_be(:fully_past) { create(:milestone, project: project, title: 'past', start_date: today - 10.days, due_date: today - 5.days) } + let_it_be(:covers_today) { create(:milestone, project: project, title: 'present', start_date: today - 5.days, due_date: today + 5.days) } + let_it_be(:fully_future) { create(:milestone, project: project, title: 'future', start_date: today + 5.days, due_date: today + 10.days) } + let_it_be(:closed) { create(:milestone, :closed, project: project) } + + let(:results) { graphql_data_at(:project, :milestones, :nodes) } + + let(:search_params) { nil } + + def query_milestones(fields) + graphql_query_for( + :project, + { full_path: project.full_path }, + query_graphql_field(:milestones, search_params, [ + query_graphql_field(:nodes, nil, %i[id title]) + ]) + ) + end + + def result_list(expected) + expected.map do |milestone| + a_hash_including('id' => global_id_of(milestone)) + end + end + + let(:query) do + query_milestones(all_graphql_fields_for('Milestone', max_depth: 1)) + end + + let(:all_milestones) do + [no_dates, no_end, no_start, fully_past, fully_future, covers_today, closed] + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end + + shared_examples 'searching with parameters' do + it 'finds the right milestones' do + post_graphql(query, current_user: current_user) + + expect(results).to match_array(result_list(expected)) + end + end + + context 'there are no search params' do + let(:search_params) { nil } + let(:expected) { all_milestones } + + it_behaves_like 'searching with parameters' + end + + context 'the search params do not match anything' do + let(:search_params) { { title: 'wibble' } } + let(:expected) { [] } + + it_behaves_like 'searching with parameters' + end + + context 'searching by state:closed' do + let(:search_params) { { state: :closed } } + let(:expected) { [closed] } + + it_behaves_like 'searching with parameters' + end + + context 'searching by state:active' do + let(:search_params) { { state: :active } } + let(:expected) { all_milestones - [closed] } + + it_behaves_like 'searching with parameters' + end + + context 'searching by title' do + let(:search_params) { { title: 'no start' } } + let(:expected) { [no_start] } + + it_behaves_like 'searching with parameters' + end + + context 'searching by search_title' do + let(:search_params) { { search_title: 'no' } } + let(:expected) { [no_dates, no_start, no_end] } + + it_behaves_like 'searching with parameters' + end + + context 'searching by containing_date' do + let(:search_params) { { containing_date: (today - 7.days).iso8601 } } + let(:expected) { [no_start, no_end, fully_past] } + + it_behaves_like 'searching with parameters' + end + + context 'searching by containing_date = today' do + let(:search_params) { { containing_date: today.iso8601 } } + let(:expected) { [no_end, covers_today] } + + it_behaves_like 'searching with parameters' + end + + context 'searching by custom range' do + let(:expected) { [no_end, fully_future] } + let(:search_params) do + { + start_date: (today + 6.days).iso8601, + end_date: (today + 7.days).iso8601 + } + end + + it_behaves_like 'searching with parameters' + end + + context 'using timeframe argument' do + let(:expected) { [no_end, fully_future] } + let(:search_params) do + { + timeframe: { + start: (today + 6.days).iso8601, + end: (today + 7.days).iso8601 + } + } + end + + it_behaves_like 'searching with parameters' + end + + describe 'timeframe validations' do + let(:vars) do + { + path: project.full_path, + start: (today + 6.days).iso8601, + end: (today + 7.days).iso8601 + } + end + + it_behaves_like 'a working graphql query' do + before do + query = <<~GQL + query($path: ID!, $start: Date!, $end: Date!) { + project(fullPath: $path) { + milestones(timeframe: { start: $start, end: $end }) { + nodes { id } + } + } + } + GQL + + post_graphql(query, current_user: current_user, variables: vars) + end + end + + it 'is invalid to provide timeframe and start_date/end_date' do + query = <<~GQL + query($path: ID!, $tstart: Date!, $tend: Date!, $start: Time!, $end: Time!) { + project(fullPath: $path) { + milestones(timeframe: { start: $tstart, end: $tend }, startDate: $start, endDate: $end) { + nodes { id } + } + } + } + GQL + + post_graphql(query, current_user: current_user, + variables: vars.merge(vars.transform_keys { |k| :"t#{k}" })) + + expect(graphql_errors).to contain_exactly(a_hash_including('message' => include('deprecated in favor of timeframe'))) + end + + it 'is invalid to invert the timeframe arguments' do + query = <<~GQL + query($path: ID!, $start: Date!, $end: Date!) { + project(fullPath: $path) { + milestones(timeframe: { start: $end, end: $start }) { + nodes { id } + } + } + } + GQL + + post_graphql(query, current_user: current_user, variables: vars) + + expect(graphql_errors).to contain_exactly(a_hash_including('message' => include('start must be before end'))) + end + end +end diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index 35eba81cfd6..db769041f1e 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -219,7 +219,7 @@ module GraphqlHelpers def as_graphql_literal(value) case value when Array then "[#{value.map { |v| as_graphql_literal(v) }.join(',')}]" - when Hash then "{#{value.map { |k, v| "#{k}:#{as_graphql_literal(v)}" }.join(',')}}" + when Hash then "{#{attributes_to_graphql(value)}}" when Integer, Float then value.to_s when String then "\"#{value.gsub(/"/, '\\"')}\"" when Symbol then value diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb index d199bae4170..f91e4bd8cf7 100644 --- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb @@ -9,6 +9,11 @@ RSpec.shared_examples 'a timebox' do |timebox_type| let(:user) { create(:user) } let(:timebox_table_name) { timebox_type.to_s.pluralize.to_sym } + # Values implementions can override + let(:mid_point) { Time.now.utc.to_date } + let(:open_on_left) { nil } + let(:open_on_right) { nil } + describe 'modules' do context 'with a project' do it_behaves_like 'AtomicInternalId' do @@ -240,4 +245,85 @@ RSpec.shared_examples 'a timebox' do |timebox_type| expect(timebox.to_ability_name).to eq(timebox_type.to_s) end end + + describe '.within_timeframe' do + let(:factory) { timebox_type } + let(:min_date) { mid_point - 10.days } + let(:max_date) { mid_point + 10.days } + + def box(from, to) + create(factory, *timebox_args, + start_date: from || open_on_left, + due_date: to || open_on_right) + end + + it 'can find overlapping timeboxes' do + fully_open = box(nil, nil) + # ----| ................ # Not overlapping + non_overlapping_open_on_left = box(nil, min_date - 1.day) + # |--| ................ # Not overlapping + non_overlapping_closed_on_left = box(min_date - 2.days, min_date - 1.day) + # ------|............... # Overlapping + overlapping_open_on_left_just = box(nil, min_date) + # -----------------------| # Overlapping + overlapping_open_on_left_fully = box(nil, max_date + 1.day) + # ---------|............ # Overlapping + overlapping_open_on_left_partial = box(nil, min_date + 1.day) + # |-----|............ # Overlapping + overlapping_closed_partial = box(min_date - 1.day, min_date + 1.day) + # |--------------| # Overlapping + exact_match = box(min_date, max_date) + # |--------------------| # Overlapping + larger = box(min_date - 1.day, max_date + 1.day) + # ...|-----|...... # Overlapping + smaller = box(min_date + 1.day, max_date - 1.day) + # .........|-----| # Overlapping + at_end = box(max_date - 1.day, max_date) + # .........|--------- # Overlapping + at_end_open = box(max_date - 1.day, nil) + # |-------------------- # Overlapping + cover_from_left = box(min_date - 1.day, nil) + # .........|--------| # Overlapping + cover_from_middle_closed = box(max_date - 1.day, max_date + 1.day) + # ...............|--| # Overlapping + overlapping_at_end_just = box(max_date, max_date + 1.day) + # ............... |-| # Not Overlapping + not_overlapping_at_right_closed = box(max_date + 1.day, max_date + 2.days) + # ............... |-- # Not Overlapping + not_overlapping_at_right_open = box(max_date + 1.day, nil) + + matches = described_class.within_timeframe(min_date, max_date) + + expect(matches).to include( + overlapping_open_on_left_just, + overlapping_open_on_left_fully, + overlapping_open_on_left_partial, + overlapping_closed_partial, + exact_match, + larger, + smaller, + at_end, + at_end_open, + cover_from_left, + cover_from_middle_closed, + overlapping_at_end_just + ) + + expect(matches).not_to include( + non_overlapping_open_on_left, + non_overlapping_closed_on_left, + not_overlapping_at_right_closed, + not_overlapping_at_right_open + ) + + # Whether we match the 'fully-open' range depends on whether + # it is in fact open (i.e. whether the class allows infinite + # ranges) + if open_on_left.nil? && open_on_right.nil? + expect(matches).not_to include(fully_open) + else + expect(matches).to include(fully_open) + end + end + end end |