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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-09-08 12:07:36 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-09-08 12:07:36 +0300
commit7a1235948517e409c00bfe213d43dcd35e614743 (patch)
tree5576f1cb8336d04870450393b5090abde19b798d
parentdabcc5d12d22ca30d83c986d6ca0b9b81e7ccbfc (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/graphql/resource_not_available_error.yml1
-rw-r--r--.rubocop_todo/rspec/context_wording.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue39
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/job_container_item.vue8
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/jobs_container.vue3
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue30
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/sidebar_detail_row.vue29
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue5
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue12
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/stages_dropdown.vue30
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue3
-rw-r--r--app/assets/stylesheets/page_bundles/build.scss65
-rw-r--r--app/controllers/invites_controller.rb2
-rw-r--r--app/controllers/sessions_controller.rb11
-rw-r--r--app/graphql/mutations/metrics/dashboard/annotations/base.rb18
-rw-r--r--app/graphql/mutations/metrics/dashboard/annotations/delete.rb4
-rw-r--r--app/models/metrics/dashboard/annotation.rb34
-rw-r--r--doc/administration/geo/setup/external_database.md2
-rw-r--r--doc/api/graphql/reference/index.md47
-rw-r--r--doc/integration/advanced_search/elasticsearch.md2
-rw-r--r--doc/user/profile/service_accounts.md2
-rw-r--r--doc/user/project/ml/experiment_tracking/mlflow_client.md1
-rw-r--r--doc/user/workspace/configuration.md5
-rw-r--r--doc/user/workspace/index.md12
-rw-r--r--lib/api/entities/ml/mlflow/get_run.rb13
-rw-r--r--lib/api/entities/ml/mlflow/run.rb12
-rw-r--r--lib/api/entities/ml/mlflow/search_runs.rb14
-rw-r--r--lib/api/ml/mlflow/api_helpers.rb31
-rw-r--r--lib/api/ml/mlflow/runs.rb44
-rw-r--r--locale/gitlab.pot37
-rw-r--r--qa/qa/page/component/ci_badge_link.rb4
-rw-r--r--spec/controllers/invites_controller_spec.rb20
-rw-r--r--spec/factories/metrics/dashboard/annotations.rb9
-rw-r--r--spec/factories/ml/candidate_params.rb2
-rw-r--r--spec/factories/ml/candidates.rb10
-rw-r--r--spec/features/invites_spec.rb30
-rw-r--r--spec/features/projects/jobs_spec.rb4
-rw-r--r--spec/fixtures/api/schemas/ml/search_runs.json82
-rw-r--r--spec/frontend/jobs/components/job/job_sidebar_details_container_spec.js3
-rw-r--r--spec/frontend/jobs/components/job/stages_dropdown_spec.js4
-rw-r--r--spec/lib/api/entities/ml/mlflow/get_run_spec.rb63
-rw-r--r--spec/lib/api/entities/ml/mlflow/run_info_spec.rb2
-rw-r--r--spec/lib/api/entities/ml/mlflow/run_spec.rb20
-rw-r--r--spec/lib/api/entities/ml/mlflow/search_runs_spec.rb37
-rw-r--r--spec/lib/api/ml/mlflow/api_helpers_spec.rb24
-rw-r--r--spec/models/metrics/dashboard/annotation_spec.rb73
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb24
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb22
-rw-r--r--spec/requests/api/metrics/dashboard/annotations_spec.rb3
-rw-r--r--spec/requests/api/ml/mlflow/runs_spec.rb126
-rw-r--r--spec/requests/sessions_spec.rb29
-rw-r--r--spec/tasks/gitlab/audit_event_types/check_docs_task_spec.rb2
-rw-r--r--spec/tasks/gitlab/audit_event_types/compile_docs_task_spec.rb2
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
diff --git a/Gemfile b/Gemfile
index b5de1790873..d8f0e09913f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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