diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-08 12:07:36 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-08 12:07:36 +0300 |
commit | 7a1235948517e409c00bfe213d43dcd35e614743 (patch) | |
tree | 5576f1cb8336d04870450393b5090abde19b798d | |
parent | dabcc5d12d22ca30d83c986d6ca0b9b81e7ccbfc (diff) |
Add latest changes from gitlab-org/gitlab@master
57 files changed, 697 insertions, 423 deletions
diff --git a/.rubocop_todo/graphql/resource_not_available_error.yml b/.rubocop_todo/graphql/resource_not_available_error.yml index 316cd4a99cb..c52cdfff6b4 100644 --- a/.rubocop_todo/graphql/resource_not_available_error.yml +++ b/.rubocop_todo/graphql/resource_not_available_error.yml @@ -35,7 +35,6 @@ Graphql/ResourceNotAvailableError: - 'ee/app/graphql/mutations/ai/action.rb' - 'ee/app/graphql/mutations/audit_events/instance_external_audit_event_destinations/base.rb' - 'ee/app/graphql/mutations/ci/ai/generate_config.rb' - - 'ee/app/graphql/mutations/geo/registries/update.rb' - 'ee/app/graphql/mutations/issues/set_escalation_policy.rb' - 'ee/app/graphql/mutations/projects/set_locked.rb' - 'ee/app/graphql/resolvers/incident_management/oncall_shifts_resolver.rb' diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index 5a2735ae19c..9a57d1cf822 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -2518,7 +2518,6 @@ RSpec/ContextWording: - 'spec/requests/projects/usage_quotas_spec.rb' - 'spec/requests/projects_controller_spec.rb' - 'spec/requests/rack_attack_global_spec.rb' - - 'spec/requests/sessions_spec.rb' - 'spec/requests/users_controller_spec.rb' - 'spec/routing/git_http_routing_spec.rb' - 'spec/routing/group_routing_spec.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 1f48fdddf7d..2781011fe7b 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1dd095a9550ba00c5f673c207e3e3f31fe917d15 +0279bd27cb92941ba71936f10a63cd52bd081c63 @@ -130,7 +130,7 @@ gem 'grape-swagger-entity', '~> 0.5.1', group: [:development, :test] # GraphQL API gem 'graphql', '~> 1.13.12' -gem 'graphiql-rails', '~> 1.8' +gem 'graphiql-rails', '~> 1.8.0' gem 'apollo_upload_server', '~> 2.1.0' gem 'graphql-docs', '~> 2.1.0', group: [:development, :test] gem 'graphlient', '~> 0.5.0' # Used by BulkImport feature (group::import) diff --git a/Gemfile.lock b/Gemfile.lock index ee8806dbc28..605ee7339e8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1856,7 +1856,7 @@ DEPENDENCIES grape-swagger (~> 1.6.1) grape-swagger-entity (~> 0.5.1) grape_logging (~> 1.8) - graphiql-rails (~> 1.8) + graphiql-rails (~> 1.8.0) graphlient (~> 0.5.0) graphlyte (~> 1.0.0) graphql (~> 1.13.12) diff --git a/app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue b/app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue index 7f25ca8a94d..95616a4c706 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue @@ -22,25 +22,32 @@ export default { </script> <template> <div> - <span class="gl-font-weight-bold">{{ __('Commit') }}</span> + <p class="gl-display-flex gl-flex-wrap gl-align-items-baseline gl-gap-2 gl-mb-0"> + <span class="gl-display-flex gl-font-weight-bold">{{ __('Commit') }}</span> - <gl-link :href="commit.commit_path" class="gl-text-blue-600!" data-testid="commit-sha"> - {{ commit.short_id }} - </gl-link> + <gl-link + :href="commit.commit_path" + class="gl-text-blue-500! gl-font-monospace" + data-testid="commit-sha" + > + {{ commit.short_id }} + </gl-link> - <clipboard-button - :text="commit.id" - :title="__('Copy commit SHA')" - category="tertiary" - size="small" - /> + <clipboard-button + :text="commit.id" + :title="__('Copy commit SHA')" + category="tertiary" + size="small" + class="gl-align-self-center" + /> - <span v-if="mergeRequest"> - {{ __('in') }} - <gl-link :href="mergeRequest.path" class="gl-text-blue-600!" data-testid="link-commit" - >!{{ mergeRequest.iid }}</gl-link - > - </span> + <span v-if="mergeRequest"> + {{ __('in') }} + <gl-link :href="mergeRequest.path" class="gl-text-blue-500!" data-testid="link-commit" + >!{{ mergeRequest.iid }}</gl-link + > + </span> + </p> <p class="gl-mb-0">{{ commit.title }}</p> </div> diff --git a/app/assets/javascripts/jobs/components/job/sidebar/job_container_item.vue b/app/assets/javascripts/jobs/components/job/sidebar/job_container_item.vue index 097ab3b4cf6..b941f7a882d 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/job_container_item.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/job_container_item.vue @@ -40,7 +40,7 @@ export default { }, classes() { return { - retried: this.job.retried, + 'retried gl-text-secondary': this.job.retried, 'gl-font-weight-bold': this.isActive, }; }, @@ -57,7 +57,7 @@ export default { v-gl-tooltip.left.viewport :href="job.status.details_path" :title="tooltipText" - class="gl-display-flex gl-align-items-center" + class="gl-display-flex gl-align-items-center gl-py-3 gl-pl-7" :data-testid="dataTestId" > <gl-icon @@ -67,11 +67,11 @@ export default { :size="14" /> - <ci-icon :status="job.status" class="gl-mr-2" :size="14" /> + <ci-icon :status="job.status" class="gl-mr-3" :size="14" /> <span class="gl-text-truncate gl-w-full">{{ jobName }}</span> - <gl-icon v-if="job.retried" name="retry" /> + <gl-icon v-if="job.retried" name="retry" class="gl-mr-4" /> </gl-link> </div> </template> diff --git a/app/assets/javascripts/jobs/components/job/sidebar/jobs_container.vue b/app/assets/javascripts/jobs/components/job/sidebar/jobs_container.vue index df64b6422c7..18bd2593c2a 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/jobs_container.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/jobs_container.vue @@ -24,7 +24,8 @@ export default { }; </script> <template> - <div class="builds-container"> + <div class="block builds-container"> + <b class="gl-display-flex gl-mb-2 gl-font-weight-semibold">{{ __('Related jobs') }}</b> <job-container-item v-for="job in jobs" :key="job.id" diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue index 530109f9dfd..1c99aa5e19d 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue @@ -17,7 +17,6 @@ export default { i18n: { ...JOB_SIDEBAR_COPY, }, - borderTopClass: ['gl-border-t-solid', 'gl-border-t-1', 'gl-border-t-gray-100'], forwardDeploymentFailureModalId, components: { ArtifactsBlock, @@ -74,49 +73,38 @@ export default { <template> <aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix"> <div class="sidebar-container"> - <div class="blocks-container"> + <div class="blocks-container gl-p-4"> <sidebar-header + class="block gl-pb-4! gl-mb-2" :rest-job="job" :job-id="job.id" @updateVariables="$emit('updateVariables')" /> - <job-sidebar-details-container class="gl-py-4" :class="$options.borderTopClass" /> + <job-sidebar-details-container class="block gl-mb-2" /> <artifacts-block v-if="hasArtifact" - class="gl-py-4" - :class="$options.borderTopClass" + class="block gl-mb-2" :artifact="job.artifact" :help-url="artifactHelpUrl" /> - <trigger-block - v-if="hasTriggers" - class="gl-py-4" - :class="$options.borderTopClass" - :trigger="job.trigger" - /> + <trigger-block v-if="hasTriggers" class="block gl-mb-2" :trigger="job.trigger" /> - <commit-block - :commit="commit" - class="gl-py-4" - :class="$options.borderTopClass" - :merge-request="job.merge_request" - /> + <commit-block class="block gl-mb-2" :commit="commit" :merge-request="job.merge_request" /> <stages-dropdown v-if="job.pipeline" - class="gl-py-4" - :class="$options.borderTopClass" + class="block gl-mb-2" :pipeline="job.pipeline" :selected-stage="selectedStage" :stages="stages" @requestSidebarStageDropdown="fetchJobsForStage" /> - </div> - <jobs-container v-if="jobs.length" :job-id="job.id" :jobs="jobs" /> + <jobs-container v-if="jobs.length" :job-id="job.id" :jobs="jobs" /> + </div> </div> <job-retry-forward-deployment-modal v-if="shouldShowJobRetryForwardDeploymentModal" diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_detail_row.vue index 0ba34eafa58..5b1bf354fd4 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_detail_row.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_detail_row.vue @@ -39,21 +39,26 @@ export default { }; </script> <template> - <p class="gl-display-flex gl-justify-content-space-between gl-mb-2"> - <span v-if="hasTitle"> - <b>{{ title }}:</b> + <p class="build-sidebar-item gl-mb-2"> + <b v-if="hasTitle" class="gl-display-flex">{{ title }}:</b> + <gl-link + v-if="path" + :href="path" + class="gl-text-blue-600!" + data-testid="job-sidebar-value-link" + > + {{ value }} + </gl-link> + <span v-else + >{{ value }} <gl-link - v-if="path" - :href="path" - class="gl-text-blue-600!" - data-testid="job-sidebar-value-link" + v-if="hasHelpURL" + :href="helpUrl" + target="_blank" + data-testid="job-sidebar-help-link" > - {{ value }} + <gl-icon name="question-o" class="gl-ml-2 gl-text-blue-500" /> </gl-link> - <span v-else>{{ value }}</span> </span> - <gl-link v-if="hasHelpURL" :href="helpUrl" target="_blank" data-testid="job-sidebar-help-link"> - <gl-icon name="question-o" /> - </gl-link> </p> </template> diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue index 4ffb8ded8ba..3a6551a0128 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue @@ -90,7 +90,7 @@ export default { </script> <template> - <div class="gl-py-4"> + <div> <tooltip-on-truncate :title="job.name" truncate-target="child" ><h4 class="gl-mt-0 gl-mb-3 gl-text-truncate" data-testid="job-name">{{ job.name }}</h4> </tooltip-on-truncate> @@ -138,6 +138,7 @@ export default { :href="restJob.retry_path" :modal-id="$options.forwardDeploymentFailureModalId" variant="confirm" + data-qa-selector="retry_button" data-testid="retry-button" @updateVariablesClicked="$emit('updateVariables')" /> @@ -155,7 +156,7 @@ export default { /> <gl-button :aria-label="$options.i18n.toggleSidebar" - category="tertiary" + category="secondary" class="gl-md-display-none gl-ml-2" icon="chevron-double-lg-right" @click="toggleSidebar" diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue index 09335476008..ebef3ecaa3f 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue @@ -44,10 +44,14 @@ export default { this.job.finished_at || this.job.erased_at || this.job.queued_duration || + this.job.id || this.job.runner || this.job.coverage, ); }, + jobId() { + return this.job?.id ? `#${this.job.id}` : ''; + }, runnerId() { const { id, short_sha: token, description } = this.job.runner; @@ -81,8 +85,9 @@ export default { ERASED: __('Erased'), QUEUED: __('Queued'), RUNNER: __('Runner'), - TAGS: __('Tags:'), + TAGS: __('Tags'), TIMEOUT: __('Timeout'), + ID: __('Job ID'), }, TIMEOUT_HELP_URL: helpPagePath('/ci/pipelines/settings.md', { anchor: 'set-a-limit-for-how-long-jobs-can-run', @@ -108,6 +113,7 @@ export default { data-testid="job-timeout" :title="$options.i18n.TIMEOUT" /> + <detail-row v-if="job.id" :value="jobId" :title="$options.i18n.ID" /> <detail-row v-if="job.runner" :value="runnerId" @@ -117,8 +123,8 @@ export default { <detail-row v-if="job.coverage" :value="coverage" :title="$options.i18n.COVERAGE" /> <p v-if="hasTags" class="build-detail-row" data-testid="job-tags"> - <span class="font-weight-bold">{{ $options.i18n.TAGS }}</span> - <gl-badge v-for="(tag, i) in job.tags" :key="i" variant="info">{{ tag }}</gl-badge> + <span class="font-weight-bold">{{ $options.i18n.TAGS }}:</span> + <gl-badge v-for="(tag, i) in job.tags" :key="i" variant="info" size="sm">{{ tag }}</gl-badge> </p> </div> </template> diff --git a/app/assets/javascripts/jobs/components/job/sidebar/stages_dropdown.vue b/app/assets/javascripts/jobs/components/job/sidebar/stages_dropdown.vue index 3fee1427256..2a91dea861c 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/stages_dropdown.vue @@ -1,20 +1,20 @@ <script> import { GlLink, GlDisclosureDropdown, GlSprintf } from '@gitlab/ui'; import { isEmpty } from 'lodash'; +import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue'; import { Mousetrap } from '~/lib/mousetrap'; import { s__ } from '~/locale'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import { clickCopyToClipboardButton } from '~/behaviors/copy_to_clipboard'; import { keysFor, MR_COPY_SOURCE_BRANCH_NAME } from '~/behaviors/shortcuts/keybindings'; export default { components: { - CiIcon, ClipboardButton, GlDisclosureDropdown, GlLink, GlSprintf, + CiBadgeLink, }, props: { pipeline: { @@ -51,13 +51,13 @@ export default { }, pipelineInfo() { if (!this.hasRef) { - return s__('Job|%{boldStart}Pipeline%{boldEnd} %{id}'); + return s__('Job|%{boldStart}Pipeline%{boldEnd} %{id} %{status}'); } if (!this.isTriggeredByMergeRequest) { - return s__('Job|%{boldStart}Pipeline%{boldEnd} %{id} for %{ref}'); + return s__('Job|%{boldStart}Pipeline%{boldEnd} %{id} %{status} for %{ref}'); } if (!this.isMergeRequestPipeline) { - return s__('Job|%{boldStart}Pipeline%{boldEnd} %{id} for %{mrId} with %{source}'); + return s__('Job|%{boldStart}Pipeline%{boldEnd} %{id} %{status} for %{mrId} with %{source}'); } return s__( @@ -94,24 +94,26 @@ export default { </script> <template> <div class="dropdown"> - <div class="js-pipeline-info" data-testid="pipeline-info"> - <ci-icon :status="pipeline.details.status" /> + <div class="gl-display-flex gl-flex-wrap gl-gap-2 js-pipeline-info" data-testid="pipeline-info"> <gl-sprintf :message="pipelineInfo"> <template #bold="{ content }"> - <span class="font-weight-bold">{{ content }}</span> + <span class="gl-display-flex gl-font-weight-bold">{{ content }}</span> </template> <template #id> <gl-link :href="pipeline.path" - class="js-pipeline-path link-commit" + class="js-pipeline-path link-commit gl-text-blue-500!" data-testid="pipeline-path" >#{{ pipeline.id }}</gl-link > </template> + <template #status> + <ci-badge-link :status="pipeline.details.status" badge-size="sm" /> + </template> <template #mrId> <gl-link :href="pipeline.merge_request.path" - class="link-commit ref-name" + class="link-commit ref-name gl-text-blue-500!" data-testid="mr-link" >!{{ pipeline.merge_request.iid }}</gl-link > @@ -119,7 +121,7 @@ export default { <template #ref> <gl-link :href="pipeline.ref.path" - class="link-commit ref-name" + class="link-commit ref-name gl-mt-1" data-testid="source-ref-link" >{{ pipeline.ref.name }}</gl-link ><clipboard-button @@ -134,7 +136,7 @@ export default { <template #source> <gl-link :href="pipeline.merge_request.source_branch_path" - class="link-commit ref-name" + class="link-commit ref-name gl-mt-1" data-testid="source-branch-link" >{{ pipeline.merge_request.source_branch }}</gl-link ><clipboard-button @@ -149,7 +151,7 @@ export default { <template #target> <gl-link :href="pipeline.merge_request.target_branch_path" - class="link-commit ref-name" + class="link-commit ref-name gl-mt-1" data-testid="target-branch-link" >{{ pipeline.merge_request.target_branch }}</gl-link ><clipboard-button @@ -167,7 +169,7 @@ export default { :toggle-text="selectedStage" :items="dropdownItems" block - class="gl-mt-3" + class="gl-mt-2" /> </div> </template> diff --git a/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue b/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue index c9172fe0322..315587a3376 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue @@ -68,7 +68,7 @@ export default { <template v-if="hasVariables"> <p class="gl-display-flex gl-justify-content-space-between gl-align-items-center"> - <span class="gl-font-weight-bold">{{ __('Trigger variables:') }}</span> + <span class="gl-display-flex gl-font-weight-bold">{{ __('Trigger variables') }}</span> <gl-button v-if="hasValues" diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue index 9a3f1672d01..d25f40b1af9 100644 --- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue +++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue @@ -120,13 +120,12 @@ export default { <template> <gl-badge v-gl-tooltip - :class="{ 'gl-pl-0!': isSmallBadgeSize }" + :class="{ 'gl-pl-2': isSmallBadgeSize }" :title="title" :href="detailsPath" :size="badgeSize" :variant="badgeStyles.variant" data-testid="ci-badge-link" - data-qa-selector="status_badge_link" @click="$emit('ciStatusBadgeClick')" > <ci-icon :status="status" /> diff --git a/app/assets/stylesheets/page_bundles/build.scss b/app/assets/stylesheets/page_bundles/build.scss index 4f968197d4e..6c80209bc5c 100644 --- a/app/assets/stylesheets/page_bundles/build.scss +++ b/app/assets/stylesheets/page_bundles/build.scss @@ -89,8 +89,6 @@ } .right-sidebar.build-sidebar { - padding: 0; - &.right-sidebar-collapsed { display: none; } @@ -103,29 +101,6 @@ -webkit-overflow-scrolling: touch; } - .blocks-container { - padding: 0 $gl-padding; - width: 289px; - } - - .trigger-variables-btn-container { - justify-content: space-between; - align-items: center; - - .trigger-variables-btn { - margin-top: -5px; - margin-bottom: -5px; - } - } - - .trigger-build-variables { - margin: 0; - overflow-x: auto; - width: 100%; - -ms-overflow-style: scrollbar; - -webkit-overflow-scrolling: touch; - } - .trigger-build-variable { font-weight: $gl-font-weight-normal; color: var(--gray-950, $gray-950); @@ -145,38 +120,20 @@ vertical-align: top; } - .badge.badge-pill { - margin-left: 2px; + .blocks-container { + width: 289px; } - .stage-item { - cursor: pointer; - - &:hover { - color: var(--gl-text-color, $gl-text-color); - } + .block { + width: 262px; } .builds-container { - background-color: var(--white, $white); - border-top: 1px solid var(--border-color, $border-color); - border-bottom: 1px solid var(--border-color, $border-color); - max-height: 300px; - width: 289px; overflow: auto; - a { - padding: $gl-padding 10px $gl-padding 40px; - width: 270px; - - &:hover { - color: var(--gl-text-color, $gl-text-color); - } - } - .icon-arrow-right { - left: 15px; - top: 20px; + left: 8px; + top: 12px; } .build-job { @@ -195,9 +152,15 @@ .container-fluid.container-limited { max-width: 100%; } +} + +.build-sidebar-item { + display: grid; + grid-template-columns: 1fr 2fr; + grid-gap: $gl-padding-8; - .content-wrapper { - padding-bottom: 6px; + &:last-of-type { + @include gl-mb-0; } } diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 8a8ae38c6f3..c058329680a 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -83,8 +83,6 @@ class InvitesController < ApplicationController def authenticate_user! return if current_user - store_location_for(:user, invite_details[:path]) if member - if user_sign_up? set_session_invite_params diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 66ace16400a..afbadc7f4ac 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -16,6 +16,8 @@ class SessionsController < Devise::SessionsController include GoogleSyndicationCSP include PreferredLanguageSwitcher include SkipsAlreadySignedInMessage + include AcceptsPendingInvitations + extend ::Gitlab::Utils::Override skip_before_action :check_two_factor_requirement, only: [:destroy] skip_before_action :check_password_expiration, only: [:destroy] @@ -78,6 +80,8 @@ class SessionsController < Devise::SessionsController flash[:notice] = nil end + accept_pending_invitations + log_audit_event(current_user, resource, with: authentication_method) log_user_activity(current_user) end @@ -94,6 +98,13 @@ class SessionsController < Devise::SessionsController private + override :after_pending_invitations_hook + def after_pending_invitations_hook + member = resource.members.last + + store_location_for(:user, member.source.activity_path) if member + end + def captcha_enabled? request.headers[CAPTCHA_HEADER] && helpers.recaptcha_enabled? end diff --git a/app/graphql/mutations/metrics/dashboard/annotations/base.rb b/app/graphql/mutations/metrics/dashboard/annotations/base.rb deleted file mode 100644 index ad52f84378d..00000000000 --- a/app/graphql/mutations/metrics/dashboard/annotations/base.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module Mutations - module Metrics - module Dashboard - module Annotations - class Base < BaseMutation - private - - # This method is defined here in order to be used by `authorized_find!` in the subclasses. - def find_object(id:) - GitlabSchema.object_from_id(id, expected_type: ::Metrics::Dashboard::Annotation) - end - end - end - end - end -end diff --git a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb index 61fcf8e0b13..d2f2d9a0e32 100644 --- a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb +++ b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb @@ -4,12 +4,12 @@ module Mutations module Metrics module Dashboard module Annotations - class Delete < Base + class Delete < BaseMutation graphql_name 'DeleteAnnotation' authorize :admin_metrics_dashboard_annotation - argument :id, ::Types::GlobalIDType[::Metrics::Dashboard::Annotation], + argument :id, GraphQL::Types::String, required: true, description: 'Global ID of the annotation to delete.' diff --git a/app/models/metrics/dashboard/annotation.rb b/app/models/metrics/dashboard/annotation.rb deleted file mode 100644 index ac0fcb41089..00000000000 --- a/app/models/metrics/dashboard/annotation.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module Metrics - module Dashboard - class Annotation < ApplicationRecord - include DeleteWithLimit - - self.table_name = 'metrics_dashboard_annotations' - - validates :starting_at, presence: true - validates :description, presence: true, length: { maximum: 255 } - validates :dashboard_path, presence: true, length: { maximum: 255 } - validates :panel_xid, length: { maximum: 255 } - validate :ending_at_after_starting_at - - scope :after, ->(after) { where('starting_at >= ?', after) } - scope :before, ->(before) { where('starting_at <= ?', before) } - - scope :for_dashboard, ->(dashboard_path) { where(dashboard_path: dashboard_path) } - scope :ending_before, ->(timestamp) { where('COALESCE(ending_at, starting_at) < ?', timestamp) } - - private - - # If annotation has NULL in ending_at column that indicates, that this annotation IS TIED TO SINGLE POINT - # IN TIME designated by starting_at timestamp. It does NOT mean that annotation is ever going starting from - # stating_at timestamp - def ending_at_after_starting_at - return if ending_at.blank? || starting_at.blank? || starting_at <= ending_at - - errors.add(:ending_at, s_("MetricsDashboardAnnotation|can't be before starting_at time")) - end - end - end -end diff --git a/doc/administration/geo/setup/external_database.md b/doc/administration/geo/setup/external_database.md index 061ae2d4eb8..b8a23f9a251 100644 --- a/doc/administration/geo/setup/external_database.md +++ b/doc/administration/geo/setup/external_database.md @@ -7,7 +7,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Geo with external PostgreSQL instances **(PREMIUM SELF)** This document is relevant if you are using a PostgreSQL instance that is not -managed by the Linux package. This includes cloud-managed instances like Amazon RDS, or +managed by the Linux package. This includes cloud-managed instances like Amazon RDS (Aurora is not supported), or manually installed and configured PostgreSQL instances. Ensure that you are using one of the PostgreSQL versions that diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 47fc3e940aa..c28597d84c3 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -2859,7 +2859,7 @@ Input type: `DeleteAnnotationInput` | Name | Type | Description | | ---- | ---- | ----------- | | <a id="mutationdeleteannotationclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | -| <a id="mutationdeleteannotationid"></a>`id` | [`MetricsDashboardAnnotationID!`](#metricsdashboardannotationid) | Global ID of the annotation to delete. | +| <a id="mutationdeleteannotationid"></a>`id` | [`String!`](#string) | Global ID of the annotation to delete. | #### Fields @@ -3732,6 +3732,32 @@ Input type: `ExternalAuditEventDestinationUpdateInput` | <a id="mutationexternalauditeventdestinationupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationexternalauditeventdestinationupdateexternalauditeventdestination"></a>`externalAuditEventDestination` | [`ExternalAuditEventDestination`](#externalauditeventdestination) | Updated destination. | +### `Mutation.geoRegistriesBulkUpdate` + +Mutates multiple Geo registries for a given registry class. Does not mutate the registries if `geo_registries_update_mutation` feature flag is disabled. + +WARNING: +**Introduced** in 16.4. +This feature is an Experiment. It can be changed or removed at any time. + +Input type: `GeoRegistriesBulkUpdateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationgeoregistriesbulkupdateaction"></a>`action` | [`GeoRegistriesBulkAction!`](#georegistriesbulkaction) | Action to be executed on Geo registries. | +| <a id="mutationgeoregistriesbulkupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationgeoregistriesbulkupdateregistryclass"></a>`registryClass` | [`GeoRegistryClass!`](#georegistryclass) | Class of the Geo registries to be updated. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationgeoregistriesbulkupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationgeoregistriesbulkupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| <a id="mutationgeoregistriesbulkupdateregistryclass"></a>`registryClass` | [`GeoRegistryClass`](#georegistryclass) | Updated Geo registry class. | + ### `Mutation.geoRegistriesUpdate` Mutates a Geo registry. Does not mutate the registry entry if `geo_registries_update_mutation` feature flag is disabled. @@ -3748,7 +3774,7 @@ Input type: `GeoRegistriesUpdateInput` | ---- | ---- | ----------- | | <a id="mutationgeoregistriesupdateaction"></a>`action` | [`GeoRegistryAction!`](#georegistryaction) | Action to be executed on a Geo registry. | | <a id="mutationgeoregistriesupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | -| <a id="mutationgeoregistriesupdateregistryclass"></a>`registryClass` | [`GeoRegistryClass!`](#georegistryclass) | Class of the Geo registry to be updated. | +| <a id="mutationgeoregistriesupdateregistryclass"></a>`registryClass` | [`GeoRegistryClass`](#georegistryclass) | Class of the Geo registry to be updated. | | <a id="mutationgeoregistriesupdateregistryid"></a>`registryId` | [`GeoBaseRegistryID!`](#geobaseregistryid) | ID of the Geo registry entry to be updated. | #### Fields @@ -27012,9 +27038,18 @@ List of statuses for forecasting model. | <a id="forecaststatusready"></a>`READY` | Forecast is ready. | | <a id="forecaststatusunavailable"></a>`UNAVAILABLE` | Forecast is unavailable. | +### `GeoRegistriesBulkAction` + +Action to trigger on multiple Geo registries. + +| Value | Description | +| ----- | ----------- | +| <a id="georegistriesbulkactionresync_all"></a>`RESYNC_ALL` | Resync multiple registries. | +| <a id="georegistriesbulkactionreverify_all"></a>`REVERIFY_ALL` | Reverify multiple registries. | + ### `GeoRegistryAction` -Action to trigger on one or more Geo registries. +Action to trigger on an individual Geo registry. | Value | Description | | ----- | ----------- | @@ -29064,12 +29099,6 @@ A `MergeRequestID` is a global ID. It is encoded as a string. An example `MergeRequestID` is: `"gid://gitlab/MergeRequest/1"`. -### `MetricsDashboardAnnotationID` - -A `MetricsDashboardAnnotationID` is a global ID. It is encoded as a string. - -An example `MetricsDashboardAnnotationID` is: `"gid://gitlab/Metrics::Dashboard::Annotation/1"`. - ### `MilestoneID` A `MilestoneID` is a global ID. It is encoded as a string. diff --git a/doc/integration/advanced_search/elasticsearch.md b/doc/integration/advanced_search/elasticsearch.md index 4af842915cf..3067ff71851 100644 --- a/doc/integration/advanced_search/elasticsearch.md +++ b/doc/integration/advanced_search/elasticsearch.md @@ -71,7 +71,7 @@ The search index updates after you: > - Elasticsearch 6.8 support is removed with GitLab 15.0. > - Upgrading from GitLab 14.10 to 15.0 requires that you are using any version of Elasticsearch 7.x. -You are not required to change the GitLab configuration when you upgrade Elasticsearch. +You don't have to change the GitLab configuration when you upgrade Elasticsearch. You should pause indexing during an Elasticsearch upgrade so changes can still be tracked. When the Elasticsearch cluster is fully upgraded and active, [resume indexing](#unpause-indexing). ## Elasticsearch repository indexer diff --git a/doc/user/profile/service_accounts.md b/doc/user/profile/service_accounts.md index 761d269d504..20fa0325b85 100644 --- a/doc/user/profile/service_accounts.md +++ b/doc/user/profile/service_accounts.md @@ -66,7 +66,7 @@ Prerequisite: This service account is associated with the entire instance, not a specific group or project in the instance. -1. [Create a personal access token](../../api/users.md#create-service-account-user) +1. [Create a personal access token](../../api/groups.md#create-personal-access-token-for-service-account-user) for the service account user. You define the scopes for the service account by [setting the scopes for the personal access token](personal_access_tokens.md#personal-access-token-scopes). diff --git a/doc/user/project/ml/experiment_tracking/mlflow_client.md b/doc/user/project/ml/experiment_tracking/mlflow_client.md index a94739c22c1..9cedb5780ed 100644 --- a/doc/user/project/ml/experiment_tracking/mlflow_client.md +++ b/doc/user/project/ml/experiment_tracking/mlflow_client.md @@ -83,6 +83,7 @@ tested. More information can be found in the [MLflow Documentation](https://www. | `set_experiment` | Yes | 15.11 | | | `get_run` | Yes | 15.11 | | | `start_run` | Yes | 15.11 | (16.3) If a name is not provided, the candidate receives a random nickname. | +| `search_runs` | Yes | 15.11 | (16.4) `experiment_ids` supports only a single experiment ID with order by column or metric. | | `log_artifact` | Yes with caveat | 15.11 | (15.11) `artifact_path` must be empty. Does not support directories. | | `log_artifacts` | Yes with caveat | 15.11 | (15.11) `artifact_path` must be empty. Does not support directories. | | `log_batch` | Yes | 15.11 | | diff --git a/doc/user/workspace/configuration.md b/doc/user/workspace/configuration.md index 63ea9955f0c..467aadeafe6 100644 --- a/doc/user/workspace/configuration.md +++ b/doc/user/workspace/configuration.md @@ -48,7 +48,7 @@ which you can customize to meet the specific needs of each project. You can use any agent defined under the root group of your project, provided that remote development is properly configured for that agent. - You must have at least the Developer role in the root group. -- In each public project you want to use this feature for, create a [devfile](index.md#devfile): +- In each project you want to use this feature for, create a [devfile](index.md#devfile): 1. On the left sidebar, select **Search or go to** and find your project. 1. In the root directory of your project, create a file named `.devfile.yaml`. You can use one of the [example configurations](index.md#example-configurations). @@ -56,6 +56,8 @@ which you can customize to meet the specific needs of each project. ### Create a workspace +> Support for private projects [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124273) in GitLab 16.4. + To create a workspace: 1. On the left sidebar, select **Search or go to**. @@ -63,7 +65,6 @@ To create a workspace: 1. Select **Workspaces**. 1. Select **New workspace**. 1. From the **Select project** dropdown list, [select a project with a `.devfile.yaml` file](#prerequisites). - You can only create workspaces for public projects. 1. From the **Select cluster agent** dropdown list, select a cluster agent owned by the group the project belongs to. 1. In **Time before automatic termination**, enter the number of hours until the workspace automatically terminates. This timeout is a safety measure to prevent a workspace from consuming excessive resources or running indefinitely. diff --git a/doc/user/workspace/index.md b/doc/user/workspace/index.md index 723d68f428f..86a91a7fca3 100644 --- a/doc/user/workspace/index.md +++ b/doc/user/workspace/index.md @@ -128,13 +128,15 @@ The Web IDE is the only code editor available for workspaces. The Web IDE is powered by the [GitLab VS Code fork](https://gitlab.com/gitlab-org/gitlab-web-ide-vscode-fork). For more information, see [Web IDE](../project/web_ide/index.md). -## Private repositories +## Personal access token -You cannot [create a workspace](configuration.md#set-up-a-workspace) for a private repository -because GitLab does not inject any credentials into the workspace. -You can only create a workspace for public repositories that have a devfile. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129715) in GitLab 16.4. -From a workspace, you can clone any repository manually. +When you [create a workspace](configuration.md#set-up-a-workspace), you get a personal access token with `write_repository` permission. +This token is used to initially clone the project while starting the workspace. + +Any Git operation you perform in the workspace uses this token for authentication and authorization. +When you terminate the workspace, the token is revoked. ## Pod interaction in a cluster diff --git a/lib/api/entities/ml/mlflow/get_run.rb b/lib/api/entities/ml/mlflow/get_run.rb new file mode 100644 index 00000000000..4bf10f987cc --- /dev/null +++ b/lib/api/entities/ml/mlflow/get_run.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + module Ml + module Mlflow + class GetRun < Grape::Entity + expose :itself, using: Run, as: :run + end + end + end + end +end diff --git a/lib/api/entities/ml/mlflow/run.rb b/lib/api/entities/ml/mlflow/run.rb index 01d85e8862b..10e2434521d 100644 --- a/lib/api/entities/ml/mlflow/run.rb +++ b/lib/api/entities/ml/mlflow/run.rb @@ -5,13 +5,11 @@ module API module Ml module Mlflow class Run < Grape::Entity - expose :run do - expose :itself, using: RunInfo, as: :info - expose :data do - expose :metrics, using: Metric - expose :params, using: KeyValue - expose :metadata, as: :tags, using: KeyValue - end + expose :itself, using: RunInfo, as: :info + expose :data do + expose :metrics, using: Metric + expose :params, using: KeyValue + expose :metadata, as: :tags, using: KeyValue end end end diff --git a/lib/api/entities/ml/mlflow/search_runs.rb b/lib/api/entities/ml/mlflow/search_runs.rb new file mode 100644 index 00000000000..21c2d58452e --- /dev/null +++ b/lib/api/entities/ml/mlflow/search_runs.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + module Ml + module Mlflow + class SearchRuns < Grape::Entity # rubocop:disable Search/NamespacedClass + expose :candidates, with: Run, as: :runs + expose :next_page_token + end + end + end + end +end diff --git a/lib/api/ml/mlflow/api_helpers.rb b/lib/api/ml/mlflow/api_helpers.rb index 68e69dcd51b..19ac0dbba1b 100644 --- a/lib/api/ml/mlflow/api_helpers.rb +++ b/lib/api/ml/mlflow/api_helpers.rb @@ -40,6 +40,37 @@ module API @candidate ||= find_candidate!(params[:run_id]) end + def candidates_order_params(params) + find_params = { + order_by: nil, + order_by_type: nil, + sort: nil + } + + return find_params if params[:order_by].blank? + + order_by_split = params[:order_by].split(' ') + order_by_column_split = order_by_split[0].split('.') + if order_by_column_split.size == 1 + order_by_column = order_by_column_split[0] + order_by_column_type = 'column' + elsif order_by_column_split[0] == 'metrics' + order_by_column = order_by_column_split[1] + order_by_column_type = 'metric' + else + order_by_column = nil + order_by_column_type = nil + end + + order_by_sort = order_by_split[1] + + { + order_by: order_by_column, + order_by_type: order_by_column_type, + sort: order_by_sort + } + end + def find_experiment!(iid, name) experiment_repository.by_iid_or_name(iid: iid, name: name) || resource_not_found! end diff --git a/lib/api/ml/mlflow/runs.rb b/lib/api/ml/mlflow/runs.rb index f737c6bd497..5b6afffaae1 100644 --- a/lib/api/ml/mlflow/runs.rb +++ b/lib/api/ml/mlflow/runs.rb @@ -26,7 +26,7 @@ module API end post 'create', urgency: :low do present candidate_repository.create!(experiment, params[:start_time], params[:tags], params[:run_name]), - with: Entities::Ml::Mlflow::Run, packages_url: packages_url + with: Entities::Ml::Mlflow::GetRun, packages_url: packages_url end desc 'Gets an MLFlow Run, which maps to GitLab Candidates' do @@ -38,7 +38,47 @@ module API optional :run_uuid, type: String, desc: 'This parameter is ignored' end get 'get', urgency: :low do - present candidate, with: Entities::Ml::Mlflow::Run, packages_url: packages_url + present candidate, with: Entities::Ml::Mlflow::GetRun, packages_url: packages_url + end + + desc 'Searches runs/candidates within a project' do + success Entities::Ml::Mlflow::Run + detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#search-runs' \ + 'experiment_ids supports only a single experiment ID.' \ + 'Introduced in GitLab 16.4' + end + params do + requires :experiment_ids, + type: Array, + desc: 'IDs of the experiments to get searches from, relative to the project' + optional :max_results, + type: Integer, + desc: 'Maximum number of runs/candidates to fetch in a page. Default is 200, maximum in 1000', + default: 200 + optional :order_by, + type: String, + desc: 'Order criteria. Can be by a column of the run/candidate (created_at, name) or by a metric if' \ + 'prefixed by `metrics`. Valid examples: `created_at`, `created_at DESC`, `metrics.my_metric DESC`' \ + 'Sorting by candidate parameter or metadata is not supported.', + default: 'created_at DESC' + optional :page_token, + type: String, + desc: 'Token for pagination' + end + get 'search', urgency: :low do + params[:experiment_id] = params[:experiment_ids][0] + + max_results = [params[:max_results], 1000].min + finder_params = candidates_order_params(params) + finder = ::Projects::Ml::CandidateFinder.new(experiment, finder_params) + paginator = finder.execute.keyset_paginate(cursor: params[:page_token], per_page: max_results) + + result = { + candidates: paginator.records, + next_page_token: paginator.cursor_for_next_page + } + + present result, with: Entities::Ml::Mlflow::SearchRuns, packages_url: packages_url end desc 'Updates a Run.' do diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 92e4bc8fa38..c9c331f0d1c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2656,6 +2656,9 @@ msgstr "" msgid "Action" msgstr "" +msgid "Action '%{action}' in registries is not supported." +msgstr "" + msgid "Action '%{action}' in registry %{registry_id} entry is not supported." msgstr "" @@ -5226,6 +5229,12 @@ msgstr "" msgid "An error occurred while trying to unfollow this user, please try again." msgstr "" +msgid "An error occurred while trying to update the registries: '%{error_message}'." +msgstr "" + +msgid "An error occurred while trying to update the registry: '%{error_message}'." +msgstr "" + msgid "An error occurred while updating approvers" msgstr "" @@ -26610,6 +26619,9 @@ msgstr "" msgid "Job Failed #%{build_id}" msgstr "" +msgid "Job ID" +msgstr "" + msgid "Job artifacts" msgstr "" @@ -26805,16 +26817,16 @@ msgstr "" msgid "Jobs|You're about to retry a job that failed because it attempted to deploy code that is older than the latest deployment. Retrying this job could result in overwriting the environment with the older source code." msgstr "" -msgid "Job|%{boldStart}Pipeline%{boldEnd} %{id}" +msgid "Job|%{boldStart}Pipeline%{boldEnd} %{id} %{status}" msgstr "" -msgid "Job|%{boldStart}Pipeline%{boldEnd} %{id} for %{mrId} with %{source}" +msgid "Job|%{boldStart}Pipeline%{boldEnd} %{id} %{status} for %{mrId} with %{source}" msgstr "" -msgid "Job|%{boldStart}Pipeline%{boldEnd} %{id} for %{mrId} with %{source} into %{target}" +msgid "Job|%{boldStart}Pipeline%{boldEnd} %{id} %{status} for %{ref}" msgstr "" -msgid "Job|%{boldStart}Pipeline%{boldEnd} %{id} for %{ref}" +msgid "Job|%{boldStart}Pipeline%{boldEnd} %{id} for %{mrId} with %{source} into %{target}" msgstr "" msgid "Job|%{searchLength} results found for %{searchTerm}" @@ -29517,9 +29529,6 @@ msgstr "" msgid "Metrics:" msgstr "" -msgid "MetricsDashboardAnnotation|can't be before starting_at time" -msgstr "" - msgid "Metrics|Create metric" msgstr "" @@ -38611,6 +38620,12 @@ msgstr "" msgid "RegistrationFeatures|use this feature" msgstr "" +msgid "Registries enqueued to be resynced" +msgstr "" + +msgid "Registries enqueued to be reverified" +msgstr "" + msgid "Registry entry enqueued to be resynced" msgstr "" @@ -38644,6 +38659,9 @@ msgstr "" msgid "Related issues" msgstr "" +msgid "Related jobs" +msgstr "" + msgid "Related merge request %{link_to_merge_request} to merge %{link_to_merge_request_source_branch}" msgstr "" @@ -46249,9 +46267,6 @@ msgstr "" msgid "Tags this commit to %{tag_name}." msgstr "" -msgid "Tags:" -msgstr "" - msgid "TagsPage|Are you sure you want to delete this tag?" msgstr "" @@ -49692,7 +49707,7 @@ msgstr "" msgid "Trigger token:" msgstr "" -msgid "Trigger variables:" +msgid "Trigger variables" msgstr "" msgid "Trigger was created successfully." diff --git a/qa/qa/page/component/ci_badge_link.rb b/qa/qa/page/component/ci_badge_link.rb index 485e363d960..0fddd1cbf12 100644 --- a/qa/qa/page/component/ci_badge_link.rb +++ b/qa/qa/page/component/ci_badge_link.rb @@ -32,12 +32,12 @@ module QA super base.view 'app/assets/javascripts/vue_shared/components/ci_badge_link.vue' do - element :status_badge_link + element 'ci-badge-link' end end def status_badge - find_element(:status_badge_link).text + find_element('ci-badge-link').text end def completed?(timeout: 60) diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb index f3b21e191c4..b3b7753df61 100644 --- a/spec/controllers/invites_controller_spec.rb +++ b/spec/controllers/invites_controller_spec.rb @@ -192,26 +192,6 @@ RSpec.describe InvitesController do expect(session[:invite_email]).to eq(member.invite_email) end - context 'with stored location for user' do - it 'stores the correct path for user' do - request - - expect(controller.stored_location_for(:user)).to eq(activity_project_path(member.source)) - end - - context 'with relative root' do - before do - stub_default_url_options(script_name: '/gitlab') - end - - it 'stores the correct path for user' do - request - - expect(controller.stored_location_for(:user)).to eq(activity_project_path(member.source)) - end - end - end - context 'when it is part of our invite email experiment' do let(:extra_params) { { invite_type: 'initial_email' } } diff --git a/spec/factories/metrics/dashboard/annotations.rb b/spec/factories/metrics/dashboard/annotations.rb deleted file mode 100644 index 50c9ed01fd8..00000000000 --- a/spec/factories/metrics/dashboard/annotations.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -FactoryBot.define do - factory :metrics_dashboard_annotation, class: '::Metrics::Dashboard::Annotation' do - description { "Dashbaord annoation description" } - dashboard_path { "custom_dashbaord.yml" } - starting_at { Time.current } - end -end diff --git a/spec/factories/ml/candidate_params.rb b/spec/factories/ml/candidate_params.rb index 73cb0c54089..e3af8ab834b 100644 --- a/spec/factories/ml/candidate_params.rb +++ b/spec/factories/ml/candidate_params.rb @@ -4,7 +4,7 @@ FactoryBot.define do factory :ml_candidate_params, class: '::Ml::CandidateParam' do association :candidate, factory: :ml_candidates - sequence(:name) { |n| "metric#{n}" } + sequence(:name) { |n| "params#{n}" } sequence(:value) { |n| "value#{n}" } end end diff --git a/spec/factories/ml/candidates.rb b/spec/factories/ml/candidates.rb index b9a2320138a..9bfb78066bd 100644 --- a/spec/factories/ml/candidates.rb +++ b/spec/factories/ml/candidates.rb @@ -7,16 +7,12 @@ FactoryBot.define do experiment { association :ml_experiments, project_id: project.id } trait :with_metrics_and_params do - after(:create) do |candidate| - candidate.metrics = FactoryBot.create_list(:ml_candidate_metrics, 2, candidate: candidate ) - candidate.params = FactoryBot.create_list(:ml_candidate_params, 2, candidate: candidate ) - end + metrics { Array.new(2) { association(:ml_candidate_metrics, candidate: instance) } } + params { Array.new(2) { association(:ml_candidate_params, candidate: instance) } } end trait :with_metadata do - after(:create) do |candidate| - candidate.metadata = FactoryBot.create_list(:ml_candidate_metadata, 2, candidate: candidate ) - end + metadata { Array.new(2) { association(:ml_candidate_metadata, candidate: instance) } } end trait :with_artifact do diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb index 785a34e0b9b..847b2fd2d81 100644 --- a/spec/features/invites_spec.rb +++ b/spec/features/invites_spec.rb @@ -4,7 +4,8 @@ require 'spec_helper' RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_category: :experimentation_expansion do let_it_be(:owner) { create(:user, name: 'John Doe') } - let_it_be(:group) { create(:group, name: 'Owned') } + # private will ensure we really have access to the group when we land on the activity page + let_it_be(:group) { create(:group, :private, name: 'Owned') } let_it_be(:project) { create(:project, :repository, namespace: group) } let(:group_invite) { group.group_members.invite.last } @@ -80,7 +81,7 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate context 'when invite clicked and not signed in' do before do - visit invite_path(group_invite.raw_invite_token) + visit invite_path(group_invite.raw_invite_token, invite_type: Emails::Members::INITIAL_INVITE) end it 'sign in, grants access and redirects to group activity page' do @@ -88,7 +89,7 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate gitlab_sign_in(user, remember: true, visit: false) - expect(page).to have_current_path(activity_group_path(group), ignore_query: true) + expect_to_be_on_group_activity_page(group) end end @@ -149,6 +150,10 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate end end end + + def expect_to_be_on_group_activity_page(group) + expect(page).to have_current_path(activity_group_path(group)) + end end end end @@ -201,11 +206,11 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate context 'when the user sign-up using a different email address' do let(:invite_email) { build_stubbed(:user).email } - it 'signs up and redirects to the activity page' do + it 'signs up and redirects to the projects dashboard' do fill_in_sign_up_form(new_user) fill_in_welcome_form - expect(page).to have_current_path(activity_group_path(group), ignore_query: true) + expect_to_be_on_projects_dashboard_with_zero_authorized_projects end end end @@ -255,13 +260,13 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate stub_feature_flags(identity_verification: false) end - it 'signs up and redirects to the group activity page' do + it 'signs up and redirects to the projects dashboard' do fill_in_sign_up_form(new_user) confirm_email(new_user) gitlab_sign_in(new_user, remember: true, visit: false) fill_in_welcome_form - expect(page).to have_current_path(activity_group_path(group), ignore_query: true) + expect_to_be_on_projects_dashboard_with_zero_authorized_projects end end @@ -271,15 +276,22 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days end - it 'signs up and redirects to the group activity page' do + it 'signs up and redirects to the projects dashboard' do fill_in_sign_up_form(new_user) fill_in_welcome_form - expect(page).to have_current_path(activity_group_path(group), ignore_query: true) + expect_to_be_on_projects_dashboard_with_zero_authorized_projects end end end end + + def expect_to_be_on_projects_dashboard_with_zero_authorized_projects + expect(page).to have_current_path(dashboard_projects_path) + + expect(page).to have_content _('Welcome to GitLab') + expect(page).to have_content _('Faster releases. Better code. Less pain.') + end end context 'when accepting an invite without an account' do diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 67486b545c9..5a8cee5ef6e 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -93,7 +93,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou visit project_job_path(project, job) within '.js-pipeline-info' do - expect(page).to have_content("Pipeline ##{pipeline.id} for #{pipeline.ref}") + expect(page).to have_content("Pipeline ##{pipeline.id} #{pipeline.status} for #{pipeline.ref}") end end @@ -239,7 +239,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state, feature_category: :grou href = new_project_issue_path(project, options) - page.within('.build-sidebar') do + page.within('aside.right-sidebar') do expect(find('[data-testid="job-new-issue"]')['href']).to include(href) end end diff --git a/spec/fixtures/api/schemas/ml/search_runs.json b/spec/fixtures/api/schemas/ml/search_runs.json new file mode 100644 index 00000000000..c1db2c9f15c --- /dev/null +++ b/spec/fixtures/api/schemas/ml/search_runs.json @@ -0,0 +1,82 @@ +{ + "type": "object", + "required": [ + "runs", + "next_page_token" + ], + "properties": { + "runs": { + "type": "array", + "items": { + "type": "object", + "required": [ + "info", + "data" + ], + "properties": { + "info": { + "type": "object", + "required": [ + "run_id", + "run_uuid", + "user_id", + "experiment_id", + "status", + "start_time", + "artifact_uri", + "lifecycle_stage" + ], + "optional": [ + "end_time" + ], + "properties": { + "run_id": { + "type": "string" + }, + "run_uuid": { + "type": "string" + }, + "experiment_id": { + "type": "string" + }, + "artifact_uri": { + "type": "string" + }, + "start_time": { + "type": "integer" + }, + "end_time": { + "type": "integer" + }, + "user_id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "RUNNING", + "SCHEDULED", + "FINISHED", + "FAILED", + "KILLED" + ] + }, + "lifecycle_stage": { + "type": "string", + "enum": [ + "active" + ] + } + } + }, + "data": { + "type": "object" + } + } + } + }, + "next_page_token": { + "type": "string" + } + } +} diff --git a/spec/frontend/jobs/components/job/job_sidebar_details_container_spec.js b/spec/frontend/jobs/components/job/job_sidebar_details_container_spec.js index c1028f3929d..0b704890d57 100644 --- a/spec/frontend/jobs/components/job/job_sidebar_details_container_spec.js +++ b/spec/frontend/jobs/components/job/job_sidebar_details_container_spec.js @@ -53,6 +53,7 @@ describe('Job Sidebar Details Container', () => { ['erased_at', 'Erased: 3 weeks ago'], ['finished_at', 'Finished: 3 weeks ago'], ['queued_duration', 'Queued: 9 seconds'], + ['id', 'Job ID: #4757'], ['runner', 'Runner: #1 (ABCDEFGH) local ci runner'], ['coverage', 'Coverage: 20%'], ])('uses %s to render job-%s', async (detail, value) => { @@ -77,7 +78,7 @@ describe('Job Sidebar Details Container', () => { createWrapper(); await store.dispatch('receiveJobSuccess', job); - expect(findAllDetailsRow()).toHaveLength(7); + expect(findAllDetailsRow()).toHaveLength(8); }); describe('duration row', () => { diff --git a/spec/frontend/jobs/components/job/stages_dropdown_spec.js b/spec/frontend/jobs/components/job/stages_dropdown_spec.js index c42edc62183..780193b33d0 100644 --- a/spec/frontend/jobs/components/job/stages_dropdown_spec.js +++ b/spec/frontend/jobs/components/job/stages_dropdown_spec.js @@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils'; import { Mousetrap } from '~/lib/mousetrap'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import StagesDropdown from '~/jobs/components/job/sidebar/stages_dropdown.vue'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue'; import * as copyToClipboard from '~/behaviors/copy_to_clipboard'; import { mockPipelineWithoutRef, @@ -15,7 +15,7 @@ import { describe('Stages Dropdown', () => { let wrapper; - const findStatus = () => wrapper.findComponent(CiIcon); + const findStatus = () => wrapper.findComponent(CiBadgeLink); const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown); const findSelectedStageText = () => findDropdown().props('toggleText'); diff --git a/spec/lib/api/entities/ml/mlflow/get_run_spec.rb b/spec/lib/api/entities/ml/mlflow/get_run_spec.rb new file mode 100644 index 00000000000..513ecdeee3c --- /dev/null +++ b/spec/lib/api/entities/ml/mlflow/get_run_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Entities::Ml::Mlflow::GetRun, feature_category: :mlops do + let_it_be(:candidate) { build(:ml_candidates, :with_metrics_and_params) } + + subject { described_class.new(candidate).as_json } + + it 'has run key' do + expect(subject).to have_key(:run) + end + + it 'has the id' do + expect(subject.dig(:run, :info, :run_id)).to eq(candidate.eid.to_s) + end + + it 'presents the metrics' do + expect(subject.dig(:run, :data, :metrics).size).to eq(candidate.metrics.size) + end + + it 'presents metrics correctly' do + presented_metric = subject.dig(:run, :data, :metrics)[0] + metric = candidate.metrics[0] + + expect(presented_metric[:key]).to eq(metric.name) + expect(presented_metric[:value]).to eq(metric.value) + expect(presented_metric[:timestamp]).to eq(metric.tracked_at) + expect(presented_metric[:step]).to eq(metric.step) + end + + it 'presents the params' do + expect(subject.dig(:run, :data, :params).size).to eq(candidate.params.size) + end + + it 'presents params correctly' do + presented_param = subject.dig(:run, :data, :params)[0] + param = candidate.params[0] + + expect(presented_param[:key]).to eq(param.name) + expect(presented_param[:value]).to eq(param.value) + end + + context 'when candidate has no metrics' do + before do + allow(candidate).to receive(:metrics).and_return([]) + end + + it 'returns empty data' do + expect(subject.dig(:run, :data, :metrics)).to be_empty + end + end + + context 'when candidate has no params' do + before do + allow(candidate).to receive(:params).and_return([]) + end + + it 'data is empty' do + expect(subject.dig(:run, :data, :params)).to be_empty + end + end +end diff --git a/spec/lib/api/entities/ml/mlflow/run_info_spec.rb b/spec/lib/api/entities/ml/mlflow/run_info_spec.rb index 28fef16a532..1664d9f18d2 100644 --- a/spec/lib/api/entities/ml/mlflow/run_info_spec.rb +++ b/spec/lib/api/entities/ml/mlflow/run_info_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe API::Entities::Ml::Mlflow::RunInfo, feature_category: :mlops do - let_it_be(:candidate) { create(:ml_candidates) } + let_it_be(:candidate) { build(:ml_candidates) } subject { described_class.new(candidate, packages_url: 'http://example.com').as_json } diff --git a/spec/lib/api/entities/ml/mlflow/run_spec.rb b/spec/lib/api/entities/ml/mlflow/run_spec.rb index a57f70f788b..58148212a7b 100644 --- a/spec/lib/api/entities/ml/mlflow/run_spec.rb +++ b/spec/lib/api/entities/ml/mlflow/run_spec.rb @@ -3,24 +3,20 @@ require 'spec_helper' RSpec.describe API::Entities::Ml::Mlflow::Run do - let_it_be(:candidate) { create(:ml_candidates, :with_metrics_and_params) } + let_it_be(:candidate) { build(:ml_candidates, :with_metrics_and_params) } subject { described_class.new(candidate).as_json } - it 'has run key' do - expect(subject).to have_key(:run) - end - it 'has the id' do - expect(subject.dig(:run, :info, :run_id)).to eq(candidate.eid.to_s) + expect(subject.dig(:info, :run_id)).to eq(candidate.eid.to_s) end it 'presents the metrics' do - expect(subject.dig(:run, :data, :metrics).size).to eq(candidate.metrics.size) + expect(subject.dig(:data, :metrics).size).to eq(candidate.metrics.size) end it 'presents metrics correctly' do - presented_metric = subject.dig(:run, :data, :metrics)[0] + presented_metric = subject.dig(:data, :metrics)[0] metric = candidate.metrics[0] expect(presented_metric[:key]).to eq(metric.name) @@ -30,11 +26,11 @@ RSpec.describe API::Entities::Ml::Mlflow::Run do end it 'presents the params' do - expect(subject.dig(:run, :data, :params).size).to eq(candidate.params.size) + expect(subject.dig(:data, :params).size).to eq(candidate.params.size) end it 'presents params correctly' do - presented_param = subject.dig(:run, :data, :params)[0] + presented_param = subject.dig(:data, :params)[0] param = candidate.params[0] expect(presented_param[:key]).to eq(param.name) @@ -47,7 +43,7 @@ RSpec.describe API::Entities::Ml::Mlflow::Run do end it 'returns empty data' do - expect(subject.dig(:run, :data, :metrics)).to be_empty + expect(subject.dig(:data, :metrics)).to be_empty end end @@ -57,7 +53,7 @@ RSpec.describe API::Entities::Ml::Mlflow::Run do end it 'data is empty' do - expect(subject.dig(:run, :data, :params)).to be_empty + expect(subject.dig(:data, :params)).to be_empty end end end diff --git a/spec/lib/api/entities/ml/mlflow/search_runs_spec.rb b/spec/lib/api/entities/ml/mlflow/search_runs_spec.rb new file mode 100644 index 00000000000..6ed59d454fa --- /dev/null +++ b/spec/lib/api/entities/ml/mlflow/search_runs_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Entities::Ml::Mlflow::SearchRuns, feature_category: :mlops do + let_it_be(:candidates) { [build_stubbed(:ml_candidates, :with_metrics_and_params), build_stubbed(:ml_candidates)] } + + let(:next_page_token) { 'abcdef' } + + subject { described_class.new({ candidates: candidates, next_page_token: next_page_token }).as_json } + + it 'presents the candidates', :aggregate_failures do + expect(subject[:runs].size).to eq(2) + expect(subject.dig(:runs, 0, :info, :run_id)).to eq(candidates[0].eid.to_s) + expect(subject.dig(:runs, 1, :info, :run_id)).to eq(candidates[1].eid.to_s) + end + + it 'presents metrics', :aggregate_failures do + expect(subject.dig(:runs, 0, :data, :metrics).size).to eq(candidates[0].metrics.size) + expect(subject.dig(:runs, 1, :data, :metrics).size).to eq(0) + + presented_metric = subject.dig(:runs, 0, :data, :metrics, 0, :key) + metric = candidates[0].metrics[0].name + + expect(presented_metric).to eq(metric) + end + + it 'presents params', :aggregate_failures do + expect(subject.dig(:runs, 0, :data, :params).size).to eq(candidates[0].params.size) + expect(subject.dig(:runs, 1, :data, :params).size).to eq(0) + + presented_param = subject.dig(:runs, 0, :data, :params, 0, :key) + param = candidates[0].params[0].name + + expect(presented_param).to eq(param) + end +end diff --git a/spec/lib/api/ml/mlflow/api_helpers_spec.rb b/spec/lib/api/ml/mlflow/api_helpers_spec.rb index 4f6a37c66c4..757a73ed612 100644 --- a/spec/lib/api/ml/mlflow/api_helpers_spec.rb +++ b/spec/lib/api/ml/mlflow/api_helpers_spec.rb @@ -37,4 +37,28 @@ RSpec.describe API::Ml::Mlflow::ApiHelpers, feature_category: :mlops do it { is_expected.to eql("http://localhost/gitlab/root/api/v4/projects/#{user_project.id}/packages/generic") } end end + + describe '#candidates_order_params' do + using RSpec::Parameterized::TableSyntax + + subject { candidates_order_params(params) } + + where(:input, :order_by, :order_by_type, :sort) do + '' | nil | nil | nil + 'created_at' | 'created_at' | 'column' | nil + 'created_at ASC' | 'created_at' | 'column' | 'ASC' + 'metrics.something' | 'something' | 'metric' | nil + 'metrics.something asc' | 'something' | 'metric' | 'asc' + 'metrics.something.blah asc' | 'something' | 'metric' | 'asc' + 'params.something ASC' | nil | nil | 'ASC' + 'metadata.something ASC' | nil | nil | 'ASC' + end + with_them do + let(:params) { { order_by: input } } + + it 'is correct' do + is_expected.to include({ order_by: order_by, order_by_type: order_by_type, sort: sort }) + end + end + end end diff --git a/spec/models/metrics/dashboard/annotation_spec.rb b/spec/models/metrics/dashboard/annotation_spec.rb deleted file mode 100644 index 7c4f392fcdc..00000000000 --- a/spec/models/metrics/dashboard/annotation_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Metrics::Dashboard::Annotation do - using RSpec::Parameterized::TableSyntax - - describe 'validation' do - it { is_expected.to validate_presence_of(:description) } - it { is_expected.to validate_presence_of(:dashboard_path) } - it { is_expected.to validate_presence_of(:starting_at) } - it { is_expected.to validate_length_of(:dashboard_path).is_at_most(255) } - it { is_expected.to validate_length_of(:panel_xid).is_at_most(255) } - it { is_expected.to validate_length_of(:description).is_at_most(255) } - - context 'ending_at_after_starting_at' do - where(:starting_at, :ending_at, :valid?, :message) do - 2.days.ago.beginning_of_day | 1.day.ago.beginning_of_day | true | nil - 1.day.ago.beginning_of_day | nil | true | nil - 1.day.ago.beginning_of_day | 1.day.ago.beginning_of_day | true | nil - 1.day.ago.beginning_of_day | 2.days.ago.beginning_of_day | false | /Ending at can't be before starting_at time/ - nil | 2.days.ago.beginning_of_day | false | /Starting at can't be blank/ # validation is covered by other method, be we need to assure, that ending_at_after_starting_at will not break with nil as starting_at - nil | nil | false | /Starting at can't be blank/ # validation is covered by other method, be we need to assure, that ending_at_after_starting_at will not break with nil as starting_at - end - - with_them do - subject(:annotation) { build(:metrics_dashboard_annotation, starting_at: starting_at, ending_at: ending_at) } - - it do - expect(annotation.valid?).to be(valid?) - expect(annotation.errors.full_messages).to include(message) if message - end - end - end - end - - describe 'scopes' do - let_it_be(:nine_minutes_old_annotation) { create(:metrics_dashboard_annotation, starting_at: 9.minutes.ago) } - let_it_be(:fifteen_minutes_old_annotation) { create(:metrics_dashboard_annotation, starting_at: 15.minutes.ago) } - let_it_be(:just_created_annotation) { create(:metrics_dashboard_annotation) } - - describe '#after' do - it 'returns only younger annotations' do - expect(described_class.after(12.minutes.ago)).to match_array [nine_minutes_old_annotation, just_created_annotation] - end - end - - describe '#before' do - it 'returns only older annotations' do - expect(described_class.before(5.minutes.ago)).to match_array [fifteen_minutes_old_annotation, nine_minutes_old_annotation] - end - end - - describe '#for_dashboard' do - let!(:other_dashboard_annotation) { create(:metrics_dashboard_annotation, dashboard_path: 'other_dashboard.yml') } - - it 'returns annotations only for appointed dashboard' do - expect(described_class.for_dashboard('other_dashboard.yml')).to match_array [other_dashboard_annotation] - end - end - - describe '#ending_before' do - it 'returns annotations only for appointed dashboard' do - freeze_time do - twelve_minutes_old_annotation = create(:metrics_dashboard_annotation, starting_at: 15.minutes.ago, ending_at: 12.minutes.ago) - create(:metrics_dashboard_annotation, starting_at: 15.minutes.ago, ending_at: 11.minutes.ago) - - expect(described_class.ending_before(11.minutes.ago)).to match_array [fifteen_minutes_old_annotation, twelve_minutes_old_annotation] - end - end - end - end -end diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb index 0a14cb1f3d0..d05cc19de96 100644 --- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb +++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb @@ -34,18 +34,6 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create, feature_categ graphql_mutation(:create_annotation, variables) end - context 'when the user does not have permission' do - before do - project.add_reporter(current_user) - end - - it 'does not create the annotation' do - expect do - post_graphql_mutation(mutation, current_user: current_user) - end.not_to change { Metrics::Dashboard::Annotation.count } - end - end - context 'when the user has permission' do before do project.add_developer(current_user) @@ -125,18 +113,6 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create, feature_categ end end - context 'without permission' do - before do - project.add_guest(current_user) - end - - it 'does not create the annotation' do - expect do - post_graphql_mutation(mutation, current_user: current_user) - end.not_to change { Metrics::Dashboard::Annotation.count } - end - end - context 'when cluster_id is invalid' do let(:mutation) do variables = { diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb index c81f6381398..6768998b31c 100644 --- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb +++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb @@ -7,19 +7,14 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete, feature_categ let_it_be(:current_user) { create(:user) } let_it_be(:project) { create(:project, :private, :repository) } - let_it_be(:annotation) { create(:metrics_dashboard_annotation) } - let(:variables) { { id: GitlabSchema.id_from_object(annotation).to_s } } + let(:variables) { { id: 'ids-dont-matter' } } let(:mutation) { graphql_mutation(:delete_annotation, variables) } def mutation_response graphql_mutation_response(:delete_annotation) end - before do - stub_feature_flags(remove_monitor_metrics: false) - end - specify { expect(described_class).to require_graphql_authorizations(:admin_metrics_dashboard_annotation) } context 'when the user has permission to delete the annotation' do @@ -30,16 +25,11 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete, feature_categ context 'with invalid params' do let(:variables) { { id: GitlabSchema.id_from_object(project).to_s } } - it_behaves_like 'a mutation that returns top-level errors' do - let(:match_errors) { contain_exactly(include('invalid value for id')) } - end + it_behaves_like 'a mutation that returns top-level errors', + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] end context 'when metrics dashboard feature is unavailable' do - before do - stub_feature_flags(remove_monitor_metrics: true) - end - it_behaves_like 'a mutation that returns top-level errors', errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] end @@ -51,11 +41,5 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete, feature_categ end it_behaves_like 'a mutation that returns top-level errors', errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] - - it 'does not delete the annotation' do - expect do - post_graphql_mutation(mutation, current_user: current_user) - end.not_to change { Metrics::Dashboard::Annotation.count } - end end end diff --git a/spec/requests/api/metrics/dashboard/annotations_spec.rb b/spec/requests/api/metrics/dashboard/annotations_spec.rb index 6000fc2a6b7..0d4a112c527 100644 --- a/spec/requests/api/metrics/dashboard/annotations_spec.rb +++ b/spec/requests/api/metrics/dashboard/annotations_spec.rb @@ -10,13 +10,12 @@ RSpec.describe API::Metrics::Dashboard::Annotations, feature_category: :metrics let(:dashboard) { 'config/prometheus/common_metrics.yml' } let(:starting_at) { Time.now.iso8601 } let(:ending_at) { 1.hour.from_now.iso8601 } - let(:params) { attributes_for(:metrics_dashboard_annotation, environment: environment, starting_at: starting_at, ending_at: ending_at, dashboard_path: dashboard) } + let(:params) { { environment: environment, starting_at: starting_at, ending_at: ending_at, dashboard_path: dashboard, description: 'desc' } } shared_examples 'POST /:source_type/:id/metrics_dashboard/annotations' do |source_type| let(:url) { "/#{source_type.pluralize}/#{source.id}/metrics_dashboard/annotations" } before do - stub_feature_flags(remove_monitor_metrics: false) project.add_developer(user) end diff --git a/spec/requests/api/ml/mlflow/runs_spec.rb b/spec/requests/api/ml/mlflow/runs_spec.rb index 53e32322d32..af04c387830 100644 --- a/spec/requests/api/ml/mlflow/runs_spec.rb +++ b/spec/requests/api/ml/mlflow/runs_spec.rb @@ -185,6 +185,132 @@ RSpec.describe API::Ml::Mlflow::Runs, feature_category: :mlops do end end + describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/runs/search' do + let_it_be(:search_experiment) { create(:ml_experiments, user: nil, project: project) } + let_it_be(:first_candidate) do + create(:ml_candidates, experiment: search_experiment, name: 'c', user: nil).tap do |c| + c.metrics.create!(name: 'metric1', value: 0.3) + end + end + + let_it_be(:second_candidate) do + create(:ml_candidates, experiment: search_experiment, name: 'a', user: nil).tap do |c| + c.metrics.create!(name: 'metric1', value: 0.2) + end + end + + let_it_be(:third_candidate) do + create(:ml_candidates, experiment: search_experiment, name: 'b', user: nil).tap do |c| + c.metrics.create!(name: 'metric1', value: 0.6) + end + end + + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/search" } + let(:order_by) { nil } + let(:default_params) do + { + 'max_results' => 2, + 'experiment_ids' => [search_experiment.iid], + 'order_by' => order_by + } + end + + it 'searches runs for a project', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + is_expected.to match_response_schema('ml/search_runs') + end + + describe 'pagination and ordering' do + RSpec.shared_examples 'a paginated search runs request with order' do + it 'paginates respecting the provided order by' do + first_page_runs = json_response['runs'] + expect(first_page_runs.size).to eq(2) + + expect(first_page_runs[0]['info']['run_id']).to eq(expected_order[0].eid) + expect(first_page_runs[1]['info']['run_id']).to eq(expected_order[1].eid) + + params = default_params.merge(page_token: json_response['next_page_token']) + + get api(route), params: params, headers: headers + + second_page_response = Gitlab::Json.parse(response.body) + second_page_runs = second_page_response['runs'] + + expect(second_page_response['next_page_token']).to be_nil + expect(second_page_runs.size).to eq(1) + expect(second_page_runs[0]['info']['run_id']).to eq(expected_order[2].eid) + end + end + + let(:default_order) { [third_candidate, second_candidate, first_candidate] } + + context 'when ordering is not provided' do + let(:expected_order) { default_order } + + it_behaves_like 'a paginated search runs request with order' + end + + context 'when order by column is provided', 'and column exists' do + let(:order_by) { 'name ASC' } + let(:expected_order) { [second_candidate, third_candidate, first_candidate] } + + it_behaves_like 'a paginated search runs request with order' + end + + context 'when order by column is provided', 'and column does not exist' do + let(:order_by) { 'something DESC' } + let(:expected_order) { default_order } + + it_behaves_like 'a paginated search runs request with order' + end + + context 'when order by metric is provided', 'and metric exists' do + let(:order_by) { 'metrics.metric1' } + let(:expected_order) { [third_candidate, first_candidate, second_candidate] } + + it_behaves_like 'a paginated search runs request with order' + end + + context 'when order by metric is provided', 'and metric does not exist' do + let(:order_by) { 'metrics.something' } + + it 'returns no results' do + expect(json_response['runs']).to be_empty + end + end + + context 'when order by params is provided' do + let(:order_by) { 'params.something' } + let(:expected_order) { default_order } + + it_behaves_like 'a paginated search runs request with order' + end + end + + describe 'Error States' do + context 'when experiment_ids is not passed' do + let(:default_params) { {} } + + it_behaves_like 'MLflow|Bad Request' + end + + context 'when experiment_ids is empty' do + let(:default_params) { { 'experiment_ids' => [] } } + + it_behaves_like 'MLflow|Not Found - Resource Does Not Exist' + end + + context 'when experiment_ids is invalid' do + let(:default_params) { { 'experiment_ids' => [non_existing_record_id] } } + + it_behaves_like 'MLflow|Not Found - Resource Does Not Exist' + end + + it_behaves_like 'MLflow|shared error cases' + it_behaves_like 'MLflow|Requires read_api scope' + end + end + describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/update' do let(:default_params) { { run_id: candidate.eid.to_s, status: 'FAILED', end_time: Time.now.to_i } } let(:request) { post api(route), params: params, headers: headers } diff --git a/spec/requests/sessions_spec.rb b/spec/requests/sessions_spec.rb index 8e069427678..9454d75d990 100644 --- a/spec/requests/sessions_spec.rb +++ b/spec/requests/sessions_spec.rb @@ -5,9 +5,9 @@ require 'spec_helper' RSpec.describe 'Sessions', feature_category: :system_access do include SessionHelpers - context 'authentication', :allow_forgery_protection do - let(:user) { create(:user) } + let_it_be(:user) { create(:user) } + context 'for authentication', :allow_forgery_protection do it 'logout does not require a csrf token' do login_as(user) @@ -17,16 +17,11 @@ RSpec.describe 'Sessions', feature_category: :system_access do end end - describe 'about_gitlab_active_user' do - before do - allow(::Gitlab).to receive(:com?).and_return(true) - end - - let(:user) { create(:user) } - + describe 'about_gitlab_active_user', :saas do context 'when user signs in' do it 'sets marketing cookie' do post user_session_path(user: { login: user.username, password: user.password }) + expect(response.cookies['about_gitlab_active_user']).to be_present end end @@ -34,12 +29,24 @@ RSpec.describe 'Sessions', feature_category: :system_access do context 'when user uses remember_me' do it 'sets marketing cookie' do post user_session_path(user: { login: user.username, password: user.password, remember_me: true }) + expect(response.cookies['about_gitlab_active_user']).to be_present end end + context 'when user has pending invitations' do + it 'accepts the invitations and stores a user location' do + create(:group_member, :invited, invite_email: user.email) + member = create(:group_member, :invited, invite_email: user.email) + + post user_session_path(user: { login: user.username, password: user.password }) + + expect(response).to redirect_to(activity_group_path(member.source)) + end + end + context 'when using two-factor authentication via OTP' do - let(:user) { create(:user, :two_factor, :invalid) } + let_it_be(:user) { create(:user, :two_factor, :invalid) } let(:user_params) { { login: user.username, password: user.password } } def authenticate_2fa(otp_attempt:) @@ -74,6 +81,7 @@ RSpec.describe 'Sessions', feature_category: :system_access do it 'deletes marketing cookie' do post(destroy_user_session_path) + expect(response.cookies['about_gitlab_active_user']).to be_nil end end @@ -85,6 +93,7 @@ RSpec.describe 'Sessions', feature_category: :system_access do it 'does not set marketing cookie' do post user_session_path(user: { login: user.username, password: user.password }) + expect(response.cookies['about_gitlab_active_user']).to be_nil end end diff --git a/spec/tasks/gitlab/audit_event_types/check_docs_task_spec.rb b/spec/tasks/gitlab/audit_event_types/check_docs_task_spec.rb index 5dd7599696b..16cd8c92273 100644 --- a/spec/tasks/gitlab/audit_event_types/check_docs_task_spec.rb +++ b/spec/tasks/gitlab/audit_event_types/check_docs_task_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'rake_helper' require_relative '../../../../lib/tasks/gitlab/audit_event_types/check_docs_task' require_relative '../../../../lib/tasks/gitlab/audit_event_types/compile_docs_task' diff --git a/spec/tasks/gitlab/audit_event_types/compile_docs_task_spec.rb b/spec/tasks/gitlab/audit_event_types/compile_docs_task_spec.rb index a881d17d3b8..da8dd170bec 100644 --- a/spec/tasks/gitlab/audit_event_types/compile_docs_task_spec.rb +++ b/spec/tasks/gitlab/audit_event_types/compile_docs_task_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'rake_helper' require_relative '../../../../lib/tasks/gitlab/audit_event_types/compile_docs_task' RSpec.describe Tasks::Gitlab::AuditEventTypes::CompileDocsTask, feature_category: :audit_events do |