diff options
30 files changed, 534 insertions, 81 deletions
@@ -149,7 +149,7 @@ gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 2.0.10' gem 'asciidoctor-include-ext', '~> 0.3.1', require: false gem 'asciidoctor-plantuml', '0.0.10' -gem 'rouge', '~> 3.16.0' +gem 'rouge', '~> 3.17.0' gem 'truncato', '~> 0.7.11' gem 'bootstrap_form', '~> 4.2.0' gem 'nokogiri', '~> 1.10.5' diff --git a/Gemfile.lock b/Gemfile.lock index 169b30c2c6e..89cee333738 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -889,7 +889,7 @@ GEM retriable (3.1.2) rinku (2.0.0) rotp (2.1.2) - rouge (3.16.0) + rouge (3.17.0) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) @@ -1346,7 +1346,7 @@ DEPENDENCIES request_store (~> 1.3) responders (~> 3.0) retriable (~> 3.1.2) - rouge (~> 3.16.0) + rouge (~> 3.17.0) rqrcode-rails3 (~> 0.1.7) rspec-parameterized rspec-rails (~> 4.0.0.beta4) diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue index a1b581dc627..f9f23fd556f 100644 --- a/app/assets/javascripts/clusters_list/components/clusters.vue +++ b/app/assets/javascripts/clusters_list/components/clusters.vue @@ -28,6 +28,10 @@ export default { label: __('Size'), }, { + key: 'memory', + label: __('Total memory (GB)'), + }, + { key: 'clusterType', label: __('Cluster level'), formatter: value => CLUSTER_TYPES[value], diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue index 80a88489545..0e160e8d568 100644 --- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue +++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue @@ -20,7 +20,7 @@ import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import { __ } from '~/locale'; import { isEmpty } from 'lodash'; -export const tableDataClass = 'table-col d-flex d-sm-table-cell align-items-center'; +export const tableDataClass = 'table-col d-flex d-md-table-cell align-items-center'; export default { FIRST_PAGE: 1, @@ -35,7 +35,7 @@ export default { key: 'error', label: __('Error'), thClass: 'w-60p', - tdClass: `${tableDataClass} px-3`, + tdClass: `${tableDataClass} px-3 rounded-top`, }, { key: 'events', @@ -58,11 +58,11 @@ export default { { key: 'status', label: '', - tdClass: `${tableDataClass} text-center`, + tdClass: `table-col d-none d-md-table-cell align-items-center pl-md-0`, }, { key: 'details', - tdClass: 'table-col d-sm-none d-flex align-items-center', + tdClass: 'table-col d-md-none d-flex align-items-center rounded-bottom bg-secondary', thClass: 'invisible w-0', }, ], @@ -221,7 +221,7 @@ export default { <div class="error-list"> <div v-if="errorTrackingEnabled"> <div class="row flex-column flex-sm-row align-items-sm-center row-top m-0 mt-sm-2 p-0 p-sm-3"> - <div class="search-box flex-fill mr-sm-2 my-3 m-sm-0 p-3 p-sm-0"> + <div class="search-box flex-fill mr-sm-2 my-3 m-sm-0 p-3 p-sm-0 bg-secondary"> <div class="filtered-search-box mb-0"> <gl-dropdown :text="__('Recent searches')" @@ -321,25 +321,25 @@ export default { </div> <template v-else> - <h4 class="d-block d-sm-none my-3">{{ __('Open errors') }}</h4> + <h4 class="d-block d-md-none my-3">{{ __('Open errors') }}</h4> <gl-table - class="mt-3" + class="error-list-table mt-3" :items="errors" :fields="$options.fields" :show-empty="true" fixed - stacked="sm" + stacked="md" tbody-tr-class="table-row mb-4" > <template #head(error)> - <div class="d-none d-sm-block">{{ __('Open errors') }}</div> + <div class="d-none d-md-block">{{ __('Open errors') }}</div> </template> <template #head(events)="data"> - <div class="text-sm-right">{{ data.label }}</div> + <div class="text-md-right">{{ data.label }}</div> </template> <template #head(users)="data"> - <div class="text-sm-right">{{ data.label }}</div> + <div class="text-md-right">{{ data.label }}</div> </template> <template #cell(error)="errors"> @@ -361,7 +361,7 @@ export default { </template> <template #cell(lastSeen)="errors"> - <div class="text-md-left text-right"> + <div class="text-lg-left text-right"> <time-ago :time="errors.item.lastSeen" class="text-secondary" /> </div> </template> @@ -381,9 +381,28 @@ export default { </template> <template #cell(details)="errors"> <gl-button + category="primary" + variant="info" + block + class="mb-1 mt-2" + @click="updateIssueStatus(errors.item.id, 'resolved')" + > + {{ __('Resolve') }} + </gl-button> + <gl-button + category="secondary" + variant="default" + block + class="mb-2" + @click="updateIssueStatus(errors.item.id, 'ignored')" + > + {{ __('Ignore') }} + </gl-button> + <gl-button :href="getDetailsLink(errors.item.id)" - variant="outline-info" - class="d-block" + category="secondary" + variant="info" + class="d-block mb-2" > {{ __('More details') }} </gl-button> diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index a70bab013c6..f6077673ad5 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -566,6 +566,14 @@ export const getDateInPast = (date, daysInPast) => export const getDateInFuture = (date, daysInFuture) => new Date(newDate(date).setDate(date.getDate() + daysInFuture)); +/** + * Checks if a given date-instance was created with a valid date + * + * @param {Date} date + * @returns boolean + */ +export const isValidDate = date => date instanceof Date && !Number.isNaN(date.getTime()); + /* * Appending T00:00:00 makes JS assume local time and prevents it from shifting the date * to match the user's time zone. We want to display the date in server time for now, to diff --git a/app/assets/javascripts/pages/projects/settings/operations/show/index.js b/app/assets/javascripts/pages/projects/settings/operations/show/index.js index a32c188909c..721d4a31fe4 100644 --- a/app/assets/javascripts/pages/projects/settings/operations/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/operations/show/index.js @@ -7,5 +7,7 @@ document.addEventListener('DOMContentLoaded', () => { mountErrorTrackingForm(); mountOperationSettings(); mountGrafanaIntegration(); - initSettingsPanels(); + if (!IS_EE) { + initSettingsPanels(); + } }); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue index 7c71463c949..b85be8b9652 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { escape as esc } from 'lodash'; import { n__, s__, sprintf } from '~/locale'; import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility'; import Icon from '~/vue_shared/components/icon.vue'; @@ -35,7 +35,7 @@ export default { 'mrWidget|The source branch is %{commitsBehindLinkStart}%{commitsBehind}%{commitsBehindLinkEnd} the target branch', ), { - commitsBehindLinkStart: `<a href="${_.escape(this.mr.targetBranchPath)}">`, + commitsBehindLinkStart: `<a href="${esc(this.mr.targetBranchPath)}">`, commitsBehind: n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount), commitsBehindLinkEnd: '</a>', }, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue index 90fb254ecca..d81e99d3c09 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { isNumber } from 'lodash'; import ArtifactsApp from './artifacts_list_app.vue'; import Deployment from './deployment/deployment.vue'; import MrWidgetContainer from './mr_widget_container.vue'; @@ -67,7 +67,7 @@ export default { return this.mr.visualReviewAppAvailable && this.glFeatures.anonymousVisualReviewFeedback; }, showMergeTrainPositionIndicator() { - return _.isNumber(this.mr.mergeTrainIndex); + return isNumber(this.mr.mergeTrainIndex); }, }, }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue index 01524f4b650..266c07ead25 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue @@ -1,6 +1,6 @@ <script> import { GlButton } from '@gitlab/ui'; -import _ from 'underscore'; +import { escape as esc } from 'lodash'; import { __, n__, sprintf, s__ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; @@ -60,7 +60,7 @@ export default { { commitCount: `<strong class="commits-count-message">${this.commitsCountMessage}</strong>`, mergeCommitCount: `<strong>${s__('mrWidgetCommitsAdded|1 merge commit')}</strong>`, - targetBranch: `<span class="label-branch">${_.escape(this.targetBranch)}</span>`, + targetBranch: `<span class="label-branch">${esc(this.targetBranch)}</span>`, }, false, ); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue index ad80a51c5f9..a368e29d086 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue @@ -1,5 +1,4 @@ <script> -import _ from 'underscore'; import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge'; import Flash from '../../../flash'; import statusIcon from '../mr_widget_status_icon.vue'; @@ -72,7 +71,7 @@ export default { .merge(options) .then(res => res.data) .then(data => { - if (_.includes(AUTO_MERGE_STRATEGIES, data.status)) { + if (AUTO_MERGE_STRATEGIES.includes(data.status)) { eventHub.$emit('MRWidgetUpdateRequested'); } }) diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue index 3df4a777aca..139cbe17e35 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue @@ -1,6 +1,6 @@ <script> import $ from 'jquery'; -import _ from 'underscore'; +import { escape as esc } from 'lodash'; import { s__, sprintf } from '~/locale'; import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover'; import StatusIcon from '../mr_widget_status_icon.vue'; @@ -50,7 +50,7 @@ export default { content: sprintf( s__('mrWidget|%{link_start}Learn more about resolving conflicts%{link_end}'), { - link_start: `<a href="${_.escape( + link_start: `<a href="${esc( this.mr.conflictsDocsPath, )}" target="_blank" rel="noopener noreferrer">`, link_end: '</a>', diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 5eccc0c543d..e34060c3393 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { isEmpty } from 'lodash'; import { GlIcon, GlButton } from '@gitlab/ui'; import successSvg from 'icons/_icon_status_success.svg'; import warningSvg from 'icons/_icon_status_warning.svg'; @@ -51,7 +51,7 @@ export default { }, computed: { isAutoMergeAvailable() { - return !_.isEmpty(this.mr.availableAutoMergeStrategies); + return !isEmpty(this.mr.availableAutoMergeStrategies); }, status() { const { pipeline, isPipelineFailed, hasCI, ciStatus } = this.mr; @@ -158,7 +158,7 @@ export default { .then(data => { const hasError = data.status === 'failed' || data.status === 'hook_validation_error'; - if (_.includes(AUTO_MERGE_STRATEGIES, data.status)) { + if (AUTO_MERGE_STRATEGIES.includes(data.status)) { eventHub.$emit('MRWidgetUpdateRequested'); } else if (data.status === 'success') { this.initiateMergePolling(); diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index df86725c025..8b12e8ffb73 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { isEmpty } from 'lodash'; import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store'; import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service'; import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps'; @@ -118,7 +118,7 @@ export default { return this.mr.allowCollaboration && this.mr.isOpen; }, shouldRenderMergedPipeline() { - return this.mr.state === 'merged' && !_.isEmpty(this.mr.mergePipeline); + return this.mr.state === 'merged' && !isEmpty(this.mr.mergePipeline); }, showMergePipelineForkWarning() { return Boolean( diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 2aecd0938e4..321b9270dde 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -1,5 +1,4 @@ import { format } from 'timeago.js'; -import _ from 'underscore'; import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key'; import { stateKey } from './state_maps'; import { formatDate } from '../../lib/utils/datetime_utility'; @@ -228,11 +227,13 @@ export default class MergeRequestStore { } static getPreferredAutoMergeStrategy(availableAutoMergeStrategies) { - if (_.includes(availableAutoMergeStrategies, MTWPS_MERGE_STRATEGY)) { + if (availableAutoMergeStrategies === undefined) return undefined; + + if (availableAutoMergeStrategies.includes(MTWPS_MERGE_STRATEGY)) { return MTWPS_MERGE_STRATEGY; - } else if (_.includes(availableAutoMergeStrategies, MT_MERGE_STRATEGY)) { + } else if (availableAutoMergeStrategies.includes(MT_MERGE_STRATEGY)) { return MT_MERGE_STRATEGY; - } else if (_.includes(availableAutoMergeStrategies, MWPS_MERGE_STRATEGY)) { + } else if (availableAutoMergeStrategies.includes(MWPS_MERGE_STRATEGY)) { return MWPS_MERGE_STRATEGY; } diff --git a/app/assets/stylesheets/pages/error_list.scss b/app/assets/stylesheets/pages/error_list.scss index f97953ce824..88fdcc47492 100644 --- a/app/assets/stylesheets/pages/error_list.scss +++ b/app/assets/stylesheets/pages/error_list.scss @@ -20,47 +20,19 @@ $gray-border: 1px solid $border-color; } } - @include media-breakpoint-down(xs) { - .table-row { - border: $gray-border; - border-radius: 4px; - } - - .search-box { - border-top: $gray-border; - border-bottom: $gray-border; - background-color: $gray-50; - } - - .table-col { - min-height: 68px; - - &::before { - text-align: left !important; - } - - &:first-child { - div { - padding: 0 !important; - align-items: flex-end; - } - } - - &:last-child { - height: 64px; - background-color: $gray-normal; - - &::before { - content: none !important; - } - - div { - width: 100% !important; - padding: 0 !important; + @include media-breakpoint-down(md) { + .error-list-table { + .table-col { + min-height: 68px; + + &:last-child { + &::before { + content: none !important; + } - a { - color: $blue-500; - border-color: $blue-500; + div { + width: 100% !important; + padding: 0 !important; } } } diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 3085f5e89b5..305956e1baf 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -420,7 +420,7 @@ table.pipeline-project-metrics tr td { p { @include str-truncated; - max-width: none; + max-width: 100%; } } diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 4916c4651dd..a9c19502a7c 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -11,6 +11,8 @@ = render_if_exists 'groups/self_or_ancestor_marked_for_deletion_notice', group: @group + = render_if_exists 'groups/group_activity_analytics', group: @group + .groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } } .top-area.group-nav-container.justify-content-between .scrolling-tabs-container.inner-page-scroll-tabs diff --git a/app/views/projects/settings/operations/show.html.haml b/app/views/projects/settings/operations/show.html.haml index 22477f315d6..ee47d70171b 100644 --- a/app/views/projects/settings/operations/show.html.haml +++ b/app/views/projects/settings/operations/show.html.haml @@ -8,3 +8,4 @@ = render 'projects/settings/operations/external_dashboard' = render 'projects/settings/operations/grafana_integration' = render_if_exists 'projects/settings/operations/tracing' += render_if_exists 'projects/settings/operations/status_page' diff --git a/changelogs/unreleased/208885-optimize-ci_pipeline-counters-related-to-the-ci-pipeline.yml b/changelogs/unreleased/208885-optimize-ci_pipeline-counters-related-to-the-ci-pipeline.yml new file mode 100644 index 00000000000..b08b4dbbd27 --- /dev/null +++ b/changelogs/unreleased/208885-optimize-ci_pipeline-counters-related-to-the-ci-pipeline.yml @@ -0,0 +1,5 @@ +--- +title: Optimize ci_pipelines counters in usage data +merge_request: 26774 +author: +type: performance diff --git a/changelogs/unreleased/208936.yml b/changelogs/unreleased/208936.yml new file mode 100644 index 00000000000..ce561fd6027 --- /dev/null +++ b/changelogs/unreleased/208936.yml @@ -0,0 +1,5 @@ +--- +title: update table layout for error tracking list on medium view ports +merge_request: 26479 +author: +type: other diff --git a/db/migrate/20200306170321_add_index_on_user_id_and_created_at_to_ci_pipelines.rb b/db/migrate/20200306170321_add_index_on_user_id_and_created_at_to_ci_pipelines.rb new file mode 100644 index 00000000000..b88f938d1c2 --- /dev/null +++ b/db/migrate/20200306170321_add_index_on_user_id_and_created_at_to_ci_pipelines.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddIndexOnUserIdAndCreatedAtToCiPipelines < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :ci_pipelines, [:user_id, :created_at] + remove_concurrent_index :ci_pipelines, [:user_id] + end + + def down + add_concurrent_index :ci_pipelines, [:user_id] + remove_concurrent_index :ci_pipelines, [:user_id, :created_at] + end +end diff --git a/db/schema.rb b/db/schema.rb index c45e7f053f3..62cce205424 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -873,7 +873,7 @@ ActiveRecord::Schema.define(version: 2020_03_09_195710) do t.index ["project_id", "status", "config_source"], name: "index_ci_pipelines_on_project_id_and_status_and_config_source" t.index ["project_id", "status", "updated_at"], name: "index_ci_pipelines_on_project_id_and_status_and_updated_at" t.index ["status"], name: "index_ci_pipelines_on_status" - t.index ["user_id"], name: "index_ci_pipelines_on_user_id" + t.index ["user_id", "created_at"], name: "index_ci_pipelines_on_user_id_and_created_at" end create_table "ci_pipelines_config", primary_key: "pipeline_id", force: :cascade do |t| diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index fc26af40c69..f47f07953d7 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2220,6 +2220,61 @@ type Epic implements Noteable { } """ +Autogenerated input type of EpicAddIssue +""" +input EpicAddIssueInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The group the epic to mutate belongs to + """ + groupPath: ID! + + """ + The iid of the epic to mutate + """ + iid: ID! + + """ + The iid of the issue to be added + """ + issueIid: String! + + """ + The project the issue belongs to + """ + projectPath: ID! +} + +""" +Autogenerated return type of EpicAddIssue +""" +type EpicAddIssuePayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The epic after mutation + """ + epic: Epic + + """ + The epic-issue relation + """ + epicIssue: EpicIssue + + """ + Reasons why the mutation failed. + """ + errors: [String!]! +} + +""" The connection type for Epic. """ type EpicConnection { @@ -2689,7 +2744,7 @@ input EpicSetSubscriptionInput { clientMutationId: String """ - The group the epic to mutate is in + The group the epic to mutate belongs to """ groupPath: ID! @@ -4872,6 +4927,7 @@ type Mutation { designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload destroyNote(input: DestroyNoteInput!): DestroyNotePayload destroySnippet(input: DestroySnippetInput!): DestroySnippetPayload + epicAddIssue(input: EpicAddIssueInput!): EpicAddIssuePayload epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload issueSetConfidential(input: IssueSetConfidentialInput!): IssueSetConfidentialPayload diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 4d52f45d3e4..3472a8bf742 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -19403,6 +19403,33 @@ "deprecationReason": null }, { + "name": "epicAddIssue", + "description": null, + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "EpicAddIssueInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "EpicAddIssuePayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "epicSetSubscription", "description": null, "args": [ @@ -25120,7 +25147,7 @@ }, { "name": "groupPath", - "description": "The group the epic to mutate is in", + "description": "The group the epic to mutate belongs to", "type": { "kind": "NON_NULL", "name": null, @@ -25163,6 +25190,164 @@ }, { "kind": "OBJECT", + "name": "EpicAddIssuePayload", + "description": "Autogenerated return type of EpicAddIssue", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "epic", + "description": "The epic after mutation", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Epic", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "epicIssue", + "description": "The epic-issue relation", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "EpicIssue", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": "Reasons why the mutation failed.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "EpicAddIssueInput", + "description": "Autogenerated input type of EpicAddIssue", + "fields": null, + "inputFields": [ + { + "name": "iid", + "description": "The iid of the epic to mutate", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "groupPath", + "description": "The group the epic to mutate belongs to", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "projectPath", + "description": "The project the issue belongs to", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "issueIid", + "description": "The iid of the issue to be added", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "__Schema", "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", "fields": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 7230be484e6..df85e13d194 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -346,6 +346,17 @@ Represents an epic. | `webPath` | String! | Web path of the epic | | `webUrl` | String! | Web URL of the epic | +## EpicAddIssuePayload + +Autogenerated return type of EpicAddIssue + +| Name | Type | Description | +| --- | ---- | ---------- | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `epic` | Epic | The epic after mutation | +| `epicIssue` | EpicIssue | The epic-issue relation | +| `errors` | String! => Array | Reasons why the mutation failed. | + ## EpicDescendantCount Counts of descendent epics. diff --git a/doc/ci/jenkins/index.md b/doc/ci/jenkins/index.md index 4b5f93fa1a2..3caea124351 100644 --- a/doc/ci/jenkins/index.md +++ b/doc/ci/jenkins/index.md @@ -24,12 +24,40 @@ can be a great resource. There are some high level differences between the products worth mentioning: - With GitLab you don't need a root `pipeline` keyword to wrap everything. +- The way pipelines are triggered and [trigger other pipelines](../yaml/README.md#trigger) + is different than Jenkins. GitLab pipelines can be triggered: + + - on push + - on [schedule](../pipelines/schedules.md) + - from the [GitLab UI](../pipelines.md#manually-executing-pipelines) + - by [API call](../triggers/README.md) + - by [webhook](../triggers/README.md#triggering-a-pipeline-from-a-webhook) + - by [ChatOps](../chatops/README.md) + + You can control which jobs run in which cases, depending on how they are triggered, + with the [`rules` syntax](../yaml/README.md#rules). +- GitLab [pipeline scheduling concepts](../pipelines/schedules.md) are also different than with Jenkins. - All jobs within a single stage always run in parallel, and all stages run in sequence. We are planning to allow certain jobs to break this sequencing as needed with our [directed acyclic graph](https://gitlab.com/gitlab-org/gitlab-foss/issues/47063) feature. +- The [`parallel`](../yaml/README.md#parallel) keyword can automatically parallelize tasks, + like tests that support parallelization. +- Normally all jobs within a single stage run in parallel, and all stages run in sequence. + There are different [pipeline architectures](../pipelines/pipeline_architectures.md) + that allow you to change this behavior. +- The new [`rules` syntax](../yaml/README.md#rules) is the recommended method of + controlling when different jobs run. It is more powerful than the `only/except` syntax. +- One important difference is that jobs run independently of each other and have a + fresh environment in each job. Passing artifacts between jobs is controlled using the + [`artifacts`](../yaml/README.md#artifacts) and [`dependencies`](../yaml/README.md#dependencies) + keywords. When finished, the planned [Workspaces](https://gitlab.com/gitlab-org/gitlab/issues/29265) + feature will allow you to more easily persist a common workspace between serial jobs. - The `.gitlab-ci.yml` file is checked in to the root of your repository, much like a Jenkinsfile, but is in the YAML format (see [complete reference](../yaml/README.md)) instead of a Groovy DSL. It's most analogous to the declarative Jenkinsfile format. +- Manual approvals or gates can be set up as [`when:manual` jobs](../yaml/README.md#whenmanual). These can + also leverage [`protected environments`](../yaml/README.md#protecting-manual-jobs-premium) + to control who is able to approve them. - GitLab comes with a [container registry](../../user/packages/container_registry/index.md), and we recommend using container images to set up your build environment. - Totally stuck and not sure where to turn for advice? The [GitLab community forum](https://forum.gitlab.com/) can be a great resource. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2eaf892d8c1..210e4ca7c12 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -18771,6 +18771,45 @@ msgstr "" msgid "Status: %{title}" msgstr "" +msgid "StatusPage|AWS Secret access key" +msgstr "" + +msgid "StatusPage|AWS access key ID" +msgstr "" + +msgid "StatusPage|AWS documentation" +msgstr "" + +msgid "StatusPage|AWS region" +msgstr "" + +msgid "StatusPage|Active" +msgstr "" + +msgid "StatusPage|Bucket %{docsLink}" +msgstr "" + +msgid "StatusPage|Configure file storage settings to link issues in this project to an external status page." +msgstr "" + +msgid "StatusPage|For help with configuration, visit %{docsLink}" +msgstr "" + +msgid "StatusPage|S3 Bucket name" +msgstr "" + +msgid "StatusPage|Status page" +msgstr "" + +msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}" +msgstr "" + +msgid "StatusPage|configuration documentation" +msgstr "" + +msgid "StatusPage|your status page frontend." +msgstr "" + msgid "Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments." msgstr "" @@ -20819,6 +20858,9 @@ msgstr "" msgid "Total issues" msgstr "" +msgid "Total memory (GB)" +msgstr "" + msgid "Total test time for all commits/merges" msgstr "" diff --git a/spec/frontend/clusters_list/mock_data.js b/spec/frontend/clusters_list/mock_data.js index 0a49c2e9f43..1812bf9b03f 100644 --- a/spec/frontend/clusters_list/mock_data.js +++ b/spec/frontend/clusters_list/mock_data.js @@ -5,6 +5,7 @@ export default [ size: '3', clusterType: 'group_type', status: 'disabled', + memory: '22.50 (30% free)', }, { name: 'My Cluster 2', @@ -12,6 +13,7 @@ export default [ size: '12', clusterType: 'project_type', status: 'unreachable', + memory: '11 (60% free)', }, { name: 'My Cluster 3', @@ -19,6 +21,7 @@ export default [ size: '12', clusterType: 'project_type', status: 'authentication_failure', + memory: '22 (33% free)', }, { name: 'My Cluster 4', @@ -26,6 +29,7 @@ export default [ size: '12', clusterType: 'project_type', status: 'deleting', + memory: '45 (15% free)', }, { name: 'My Cluster 5', @@ -33,5 +37,6 @@ export default [ size: '12', clusterType: 'project_type', status: 'connected', + memory: '20.12 (35% free)', }, ]; diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js index 27b88d78ff0..f6878c7c920 100644 --- a/spec/frontend/lib/utils/datetime_utility_spec.js +++ b/spec/frontend/lib/utils/datetime_utility_spec.js @@ -474,6 +474,23 @@ describe('getDateInFuture', () => { }); }); +describe('isValidDate', () => { + it.each` + valueToCheck | isValid + ${new Date()} | ${true} + ${new Date('December 17, 1995 03:24:00')} | ${true} + ${new Date('1995-12-17T03:24:00')} | ${true} + ${new Date('foo')} | ${false} + ${5} | ${false} + ${''} | ${false} + ${false} | ${false} + ${undefined} | ${false} + ${null} | ${false} + `('returns $expectedReturnValue when called with $dateToCheck', ({ valueToCheck, isValid }) => { + expect(datetimeUtility.isValidDate(valueToCheck)).toBe(isValid); + }); +}); + describe('getDatesInRange', () => { it('returns an empty array if 1st or 2nd argument is not a Date object', () => { const d1 = new Date('2019-01-01'); diff --git a/spec/lib/gitlab/project_authorizations_spec.rb b/spec/lib/gitlab/project_authorizations_spec.rb index 7b282433061..d1c441c8605 100644 --- a/spec/lib/gitlab/project_authorizations_spec.rb +++ b/spec/lib/gitlab/project_authorizations_spec.rb @@ -43,6 +43,78 @@ describe Gitlab::ProjectAuthorizations do end end + context 'unapproved access request' do + let_it_be(:group) { create(:group) } + let_it_be(:user) { create(:user) } + + subject(:mapping) { map_access_levels(authorizations) } + + context 'group membership' do + let!(:group_project) { create(:project, namespace: group) } + + before do + create(:group_member, :developer, :access_request, user: user, group: group) + end + + it 'does not create authorization' do + expect(mapping[group_project.id]).to be_nil + end + end + + context 'inherited group membership' do + let!(:sub_group) { create(:group, parent: group) } + let!(:sub_group_project) { create(:project, namespace: sub_group) } + + before do + create(:group_member, :developer, :access_request, user: user, group: group) + end + + it 'does not create authorization' do + expect(mapping[sub_group_project.id]).to be_nil + end + end + + context 'project membership' do + let!(:group_project) { create(:project, namespace: group) } + + before do + create(:project_member, :developer, :access_request, user: user, project: group_project) + end + + it 'does not create authorization' do + expect(mapping[group_project.id]).to be_nil + end + end + + context 'shared group' do + let!(:shared_group) { create(:group) } + let!(:shared_group_project) { create(:project, namespace: shared_group) } + + before do + create(:group_group_link, shared_group: shared_group, shared_with_group: group) + create(:group_member, :developer, :access_request, user: user, group: group) + end + + it 'does not create authorization' do + expect(mapping[shared_group_project.id]).to be_nil + end + end + + context 'shared project' do + let!(:another_group) { create(:group) } + let!(:shared_project) { create(:project, namespace: another_group) } + + before do + create(:project_group_link, group: group, project: shared_project) + create(:group_member, :developer, :access_request, user: user, group: group) + end + + it 'does not create authorization' do + expect(mapping[shared_project.id]).to be_nil + end + end + end + context 'with nested groups' do let(:group) { create(:group) } let!(:nested_group) { create(:group, parent: group) } |