Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-01-10 15:08:33 +00:00
parent fbf183eebe
commit 87f8fdb93c
54 changed files with 833 additions and 353 deletions

View File

@ -18,13 +18,13 @@ export const HTTP_STATUS_BAD_REQUEST = 400;
export const HTTP_STATUS_UNAUTHORIZED = 401;
export const HTTP_STATUS_FORBIDDEN = 403;
export const HTTP_STATUS_NOT_FOUND = 404;
export const HTTP_STATUS_INTERNAL_SERVER_ERROR = 500;
export const HTTP_STATUS_SERVICE_UNAVAILABLE = 503;
// TODO move the rest of the status codes to primitive constants
// https://docs.gitlab.com/ee/development/fe_guide/style/javascript.html#export-constants-as-primitives
const httpStatusCodes = {
OK: 200,
INTERNAL_SERVER_ERROR: 500,
SERVICE_UNAVAILABLE: 503,
};
export const successCodes = [

View File

@ -1,7 +1,8 @@
<script>
import { GlTable, GlLink, GlPagination } from '@gitlab/ui';
import { GlTable, GlLink, GlPagination, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import { getParameterValues, setUrlParams } from '~/lib/utils/url_utility';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import IncubationAlert from './incubation_alert.vue';
export default {
@ -9,9 +10,13 @@ export default {
components: {
GlTable,
GlLink,
TimeAgo,
IncubationAlert,
GlPagination,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['candidates', 'metricNames', 'paramNames', 'pagination'],
data() {
return {
@ -21,6 +26,9 @@ export default {
computed: {
fields() {
return [
{ key: 'name', label: this.$options.i18n.nameLabel },
{ key: 'created_at', label: this.$options.i18n.createdAtLabel },
{ key: 'user', label: this.$options.i18n.userLabel },
...this.paramNames,
...this.metricNames,
{ key: 'details', label: '' },
@ -47,6 +55,10 @@ export default {
emptyStateLabel: __('This experiment has no logged candidates'),
artifactsLabel: __('Artifacts'),
detailsLabel: __('Details'),
userLabel: __('User'),
createdAtLabel: __('Created at'),
nameLabel: __('Name'),
noDataContent: __('-'),
},
};
</script>
@ -64,17 +76,46 @@ export default {
:items="candidates"
:empty-text="$options.i18n.emptyStateLabel"
show-empty
class="gl-mt-0!"
small
class="gl-mt-0! ml-candidate-table"
>
<template #cell()="data">
<div v-gl-tooltip.hover :title="data.value">{{ data.value }}</div>
</template>
<template #cell(artifact)="data">
<gl-link v-if="data.value" :href="data.value" target="_blank">{{
$options.i18n.artifactsLabel
}}</gl-link>
<gl-link
v-if="data.value"
v-gl-tooltip.hover
:href="data.value"
target="_blank"
:title="$options.i18n.artifactsLabel"
>{{ $options.i18n.artifactsLabel }}</gl-link
>
<div v-else v-gl-tooltip.hover :title="$options.i18n.artifactsLabel">
{{ $options.i18n.noDataContent }}
</div>
</template>
<template #cell(details)="data">
<gl-link :href="data.value">{{ $options.i18n.detailsLabel }}</gl-link>
<gl-link v-gl-tooltip.hover :href="data.value" :title="$options.i18n.detailsLabel">{{
$options.i18n.detailsLabel
}}</gl-link>
</template>
<template #cell(created_at)="data">
<time-ago v-gl-tooltip.hover :time="data.value" :title="data.value" />
</template>
<template #cell(user)="data">
<gl-link
v-if="data.value"
v-gl-tooltip.hover
:href="data.value.path"
:title="data.value.username"
>@{{ data.value.username }}</gl-link
>
<div v-else>{{ $options.i18n.noDataContent }}</div>
</template>
</gl-table>

View File

@ -1,8 +1,9 @@
import axios from '~/lib/utils/axios_utils';
import { backOff } from '~/lib/utils/common_utils';
import statusCodes, {
import {
HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_NO_CONTENT,
HTTP_STATUS_SERVICE_UNAVAILABLE,
HTTP_STATUS_UNPROCESSABLE_ENTITY,
} from '~/lib/utils/http_status';
import { PROMETHEUS_TIMEOUT } from '../constants';
@ -39,7 +40,7 @@ export const getPrometheusQueryData = (prometheusEndpoint, params, opts) =>
if (
response.status === HTTP_STATUS_BAD_REQUEST ||
response.status === HTTP_STATUS_UNPROCESSABLE_ENTITY ||
response.status === statusCodes.SERVICE_UNAVAILABLE
response.status === HTTP_STATUS_SERVICE_UNAVAILABLE
) {
const { data } = response;
if (data?.status === 'error' && data?.error) {

View File

@ -1,7 +1,7 @@
import { pick } from 'lodash';
import Vue from 'vue';
import { BACKOFF_TIMEOUT } from '~/lib/utils/common_utils';
import httpStatusCodes, { HTTP_STATUS_BAD_REQUEST } from '~/lib/utils/http_status';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_SERVICE_UNAVAILABLE } from '~/lib/utils/http_status';
import { dashboardEmptyStates, endpointKeys, initialStateKeys, metricStates } from '../constants';
import * as types from './mutation_types';
import { mapToDashboardViewModel, mapPanelToViewModel, normalizeQueryResponseData } from './utils';
@ -43,7 +43,7 @@ const emptyStateFromError = (error) => {
// Axios error responses
const { response } = error;
if (response && response.status === httpStatusCodes.SERVICE_UNAVAILABLE) {
if (response && response.status === HTTP_STATUS_SERVICE_UNAVAILABLE) {
return metricStates.CONNECTION_FAILED;
} else if (response && response.status === HTTP_STATUS_BAD_REQUEST) {
// Note: "error.response.data.error" may contain Prometheus error information

View File

@ -55,6 +55,7 @@ export default {
:action-cancel="$options.modal.cancelAction"
:title="$options.i18n.DELETE_PACKAGES_MODAL_TITLE"
@primary="$emit('confirm')"
@cancel="$emit('cancel')"
>
<span>{{ description }}</span>
</gl-modal>

View File

@ -5,10 +5,14 @@ import DeletePackageModal from '~/packages_and_registries/shared/components/dele
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import {
DELETE_PACKAGE_TRACKING_ACTION,
DELETE_PACKAGES_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGES_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
CANCEL_DELETE_PACKAGES_TRACKING_ACTION,
PACKAGE_ERROR_STATUS,
} from '~/packages_and_registries/package_registry/constants';
import { packageTypeToTrackCategory } from '~/packages_and_registries/package_registry/utils';
@ -18,6 +22,7 @@ export default {
name: 'PackagesList',
components: {
GlAlert,
DeleteModal,
DeletePackageModal,
PackagesListLoader,
PackagesListRow,
@ -44,6 +49,7 @@ export default {
data() {
return {
itemToBeDeleted: null,
itemsToBeDeleted: [],
errorPackages: [],
};
},
@ -92,7 +98,18 @@ export default {
this.setItemToBeDeleted(item);
return;
}
this.$emit('delete', items);
this.itemsToBeDeleted = items;
this.track(REQUEST_DELETE_PACKAGES_TRACKING_ACTION);
this.$refs.deletePackagesModal.show();
},
deleteItemsConfirmation() {
this.$emit('delete', this.itemsToBeDeleted);
this.track(DELETE_PACKAGES_TRACKING_ACTION);
this.itemsToBeDeleted = [];
},
deleteItemsCanceled() {
this.track(CANCEL_DELETE_PACKAGES_TRACKING_ACTION);
this.itemsToBeDeleted = [];
},
deleteItemConfirmation() {
this.$emit('package:delete', this.itemToBeDeleted);
@ -159,6 +176,13 @@ export default {
@ok="deleteItemConfirmation"
@cancel="deleteItemCanceled"
/>
<delete-modal
ref="deletePackagesModal"
:items-to-be-deleted="itemsToBeDeleted"
@confirm="deleteItemsConfirmation"
@cancel="deleteItemsCanceled"
/>
</template>
</div>
</template>

View File

@ -110,6 +110,11 @@ export const FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE = s__(
export const FETCH_PACKAGE_METADATA_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while fetching the package metadata.',
);
export const DELETE_PACKAGES_TRACKING_ACTION = 'delete_packages';
export const REQUEST_DELETE_PACKAGES_TRACKING_ACTION = 'request_delete_packages';
export const CANCEL_DELETE_PACKAGES_TRACKING_ACTION = 'cancel_delete_packages';
export const DELETE_PACKAGES_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while deleting packages.',
);

View File

@ -1,6 +1,6 @@
<script>
import { GlAlert, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { createAlert, VARIANT_INFO } from '~/flash';
import { createAlert, VARIANT_INFO, VARIANT_SUCCESS, VARIANT_DANGER } from '~/flash';
import { historyReplaceState } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages_and_registries/shared/constants';
@ -20,7 +20,6 @@ import DeletePackage from '~/packages_and_registries/package_registry/components
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import PackageList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
export default {
components: {
@ -31,14 +30,12 @@ export default {
PackageList,
PackageTitle,
PackageSearch,
DeleteModal,
DeletePackage,
},
inject: ['emptyListIllustration', 'isGroupPage', 'fullPath'],
data() {
return {
alertVariables: null,
itemsToBeDeleted: [],
packages: {},
sort: '',
filters: {},
@ -117,15 +114,13 @@ export default {
historyReplaceState(cleanUrl);
}
},
async confirmDelete() {
const { itemsToBeDeleted } = this;
this.itemsToBeDeleted = [];
async deletePackages(packageEntities) {
this.mutationLoading = true;
try {
const { data } = await this.$apollo.mutate({
mutation: destroyPackagesMutation,
variables: {
ids: itemsToBeDeleted.map((i) => i.id),
ids: packageEntities.map((i) => i.id),
},
awaitRefetchQueries: true,
refetchQueries: [
@ -140,22 +135,18 @@ export default {
throw new Error(data.destroyPackages.errors[0]);
}
this.showAlert({
variant: 'success',
variant: VARIANT_SUCCESS,
message: DELETE_PACKAGES_SUCCESS_MESSAGE,
});
} catch {
this.showAlert({
variant: 'danger',
variant: VARIANT_DANGER,
message: DELETE_PACKAGES_ERROR_MESSAGE,
});
} finally {
this.mutationLoading = false;
}
},
showDeletePackagesModal(toBeDeleted) {
this.itemsToBeDeleted = toBeDeleted;
this.$refs.deletePackagesModal.show();
},
handleSearchUpdate({ sort, filters }) {
this.sort = sort;
this.filters = { ...filters };
@ -236,7 +227,7 @@ export default {
@prev-page="fetchPreviousPage"
@next-page="fetchNextPage"
@package:delete="deletePackage"
@delete="showDeletePackagesModal"
@delete="deletePackages"
>
<template #empty-state>
<gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration">
@ -255,11 +246,5 @@ export default {
</package-list>
</template>
</delete-package>
<delete-modal
ref="deletePackagesModal"
:items-to-be-deleted="itemsToBeDeleted"
@confirm="confirmDelete"
/>
</div>
</template>

View File

@ -79,8 +79,11 @@ export default {
</script>
<template>
<div
class="gl-w-full gl-display-flex gl-align-items-baseline"
:class="{ 'gl-border-t gl-py-3 gl-pl-7': level === 2 }"
class="gl-w-full gl-display-flex"
:class="{
'gl-border-t gl-py-3 gl-pl-7 gl-align-items-baseline': level === 2,
'gl-align-items-center': level === 3,
}"
>
<status-icon
v-if="statusIconName && !header"

View File

@ -15,6 +15,20 @@
}
}
table.ml-candidate-table {
table-layout: fixed;
tr td,
tr th {
padding: $gl-padding-8;
> * {
@include gl-display-block;
@include gl-text-truncate;
}
}
}
table.candidate-details {
td {
padding: $gl-spacing-scale-3;

View File

@ -23,7 +23,7 @@ module Projects
page = 1 if page == 0
@candidates = @experiment.candidates
.including_metrics_and_params
.including_relationships
.page(page)
.per(MAX_CANDIDATES_PER_PAGE)

View File

@ -16,11 +16,10 @@ module Mutations
def resolve(ids:)
raise_resource_not_available_error!(TOO_MANY_IDS_ERROR) if ids.size > MAX_PACKAGES
ids = GitlabSchema.parse_gids(ids, expected_type: ::Packages::Package)
.map(&:model_id)
model_ids = ids.map(&:model_id)
service = ::Packages::MarkPackagesForDestructionService.new(
packages: packages_from(ids),
packages: packages_from(model_ids),
current_user: current_user
)
result = service.execute

View File

@ -65,17 +65,6 @@ module MarkupHelper
tags = %w(a gl-emoji b strong i em pre code p span)
if Feature.disabled?(:two_line_mention_enabled, current_user)
includes_code = false
if is_todo
fragment = Nokogiri::HTML.fragment(md)
includes_code = fragment.css('code').any?
md = fragment
end
end
context = markdown_field_render_context(object, attribute, options)
context.reverse_merge!(truncate_visible_max_chars: max_chars || md.length)
@ -91,9 +80,6 @@ module MarkupHelper
)
)
if is_todo && !includes_code && Feature.disabled?(:two_line_mention_enabled, current_user)
text = "<span class=\"gl-relative\">\"</span>#{text}<span class=\"gl-relative\">\"</span>"
end
# since <img> tags are stripped, this can leave empty <a> tags hanging around
# (as our markdown wraps images in links)
strip_empty_link_tags(text).html_safe

View File

@ -11,7 +11,10 @@ module Projects
**candidate.params.to_h { |p| [p.name, p.value] },
**candidate.latest_metrics.to_h { |m| [m.name, number_with_precision(m.value, precision: 4)] },
artifact: link_to_artifact(candidate),
details: link_to_details(candidate)
details: link_to_details(candidate),
name: candidate.name,
created_at: candidate.created_at,
user: user_info(candidate)
}
end
@ -58,6 +61,17 @@ module Projects
project_ml_experiment_path(experiment.project, experiment.iid)
end
def user_info(candidate)
user = candidate.user
return unless user.present?
{
username: user.username,
path: user_path(user)
}
end
end
end
end

View File

@ -18,7 +18,7 @@ module Ml
attribute :iid, default: -> { SecureRandom.uuid }
scope :including_metrics_and_params, -> { includes(:latest_metrics, :params) }
scope :including_relationships, -> { includes(:latest_metrics, :params, :user) }
delegate :project_id, :project, to: :experiment

View File

@ -1,5 +1,3 @@
- two_line_mention = Feature.enabled?(:two_line_mention_enabled, current_user)
%li.todo.gl-hover-border-blue-200.gl-hover-bg-blue-50.gl-hover-cursor-pointer.gl-relative{ class: "todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo) }
.gl-display-flex.gl-flex-direction-column.gl-sm-flex-direction-row.gl-sm-align-items-center
.todo-item.gl-overflow-hidden.gl-overflow-x-auto.gl-align-self-center.gl-w-full{ data: { qa_selector: "todo_item_container" } }
@ -32,20 +30,13 @@
= link_to_author(todo, self_added: todo.self_added?)
- else
= _('(removed)')
- if todo.note.present? && two_line_mention
- if todo.note.present?
\:
%span.action-name{ data: { qa_selector: "todo_action_name_content" } }<
- if two_line_mention
- if !todo.note.present?
= todo_action_name(todo)
- unless todo.self_assigned?
\.
- else
- if !todo.note.present?
= todo_action_name(todo)
- if todo.note.present?
\:
- unless todo.note.present? || todo.self_assigned?
- unless todo.self_assigned?
\.
- if todo.self_assigned?
@ -53,9 +44,8 @@
= todo_self_addressing(todo)
\.
- if todo.note.present?
%span.action-description{ :class => ("gl-font-style-italic" if !two_line_mention) }<
- max_chars = two_line_mention ? 125 : 100
= first_line_in_markdown(todo, :body, max_chars, is_todo: true, project: todo.project, group: todo.group)
%span.action-description<
= first_line_in_markdown(todo, :body, 125, is_todo: true, project: todo.project, group: todo.group)
.todo-timestamp.gl-white-space-nowrap.gl-sm-ml-3.gl-mt-2.gl-mb-2.gl-sm-my-0.gl-px-2.gl-sm-px-0
%span.todo-timestamp.gl-font-sm.gl-text-secondary

View File

@ -1,8 +0,0 @@
---
name: two_line_mention_enabled
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106689
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/385889
milestone: '15.7'
type: development
group: group::project management
default_enabled: false

View File

@ -14,24 +14,22 @@ features of GitLab.
To reduce memory use, Puma forks worker processes. Each time a worker is created,
it shares memory with the primary process. The worker uses additional memory only
when it changes or adds to its memory pages.
when it changes or adds to its memory pages. This can lead to Puma workers using
more physical memory over time as workers handle additional web requests. The amount of memory
used over time depends on the use of GitLab. The more features used by GitLab users,
the higher the expected memory use over time.
Memory use increases over time, but you can use Puma Worker Killer to recover memory.
To stop uncontrolled memory growth, the GitLab Rails application runs a supervision thread
that automatically restarts workers if they exceed a given resident set size (RSS) threshold
for a certain amount of time.
By default:
- The [Puma Worker Killer](https://github.com/schneems/puma_worker_killer) restarts a worker if it
exceeds a [memory limit](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/cluster/puma_worker_killer_initializer.rb).
- Rolling restarts of Puma workers are performed every 12 hours.
### Change the memory limit setting
To change the memory limit setting:
GitLab sets a default of `1200Mb` for the memory limit. To override the default value,
set `per_worker_max_memory_mb` to the new RSS limit in megabytes:
1. Edit `/etc/gitlab/gitlab.rb`:
```ruby
puma['per_worker_max_memory_mb'] = 1024
puma['per_worker_max_memory_mb'] = 1024 # 1GB
```
1. Reconfigure GitLab:
@ -40,48 +38,40 @@ To change the memory limit setting:
sudo gitlab-ctl reconfigure
```
When workers are killed and replaced, capacity to run GitLab is reduced,
and CPU is consumed. Set `per_worker_max_memory_mb` to a higher value if the worker killer
is replacing workers too often.
When workers are restarted, capacity to run GitLab is reduced for a short
period of time. Set `per_worker_max_memory_mb` to a higher value if workers are replaced too often.
Worker count is calculated based on CPU cores. A small GitLab deployment
with 4-8 workers may experience performance issues if workers are being restarted
too often (once or more per minute).
A higher value of `1200` or more would be beneficial if the server has free memory.
A higher value of `1200` or more could be beneficial if the server has free memory.
### Monitor worker memory
### Monitor worker restarts
The worker killer checks memory every 20 seconds.
GitLab emits log events if workers are restarted due to high memory use.
To monitor the worker killer, use [the Puma log](../logs/index.md#puma_stdoutlog) `/var/log/gitlab/puma/puma_stdout.log`.
For example:
The following is an example of one of these log events in `/var/log/gitlab/gitlab-rails/application_json.log`:
```plaintext
PumaWorkerKiller: Out of memory. 4 workers consuming total: 4871.23828125 MB
out of max: 4798.08 MB. Sending TERM to pid 26668 consuming 1001.00390625 MB.
```json
{
"severity": "WARN",
"time": "2023-01-04T09:45:16.173Z",
"correlation_id": null,
"pid": 2725,
"worker_id": "puma_0",
"memwd_handler_class": "Gitlab::Memory::Watchdog::PumaHandler",
"memwd_sleep_time_s": 5,
"memwd_rss_bytes": 1077682176,
"memwd_max_rss_bytes": 629145600,
"memwd_max_strikes": 5,
"memwd_cur_strikes": 6,
"message": "rss memory limit exceeded"
}
```
From this output:
- The formula that calculates the maximum memory value results in workers
being killed before they reach the `per_worker_max_memory_mb` value.
- In GitLab 13.4 and earlier, the default values for the formula were 550 MB for the primary
and 850 MB for each worker.
- In GitLab 13.5 and later, the values are primary: 800 MB, worker: 1024 MB.
- The threshold for workers to be killed is set at 98% of the limit:
```plaintext
0.98 * ( 800 + ( worker_processes * 1024MB ) )
```
- In the log output above, `0.98 * ( 800 + ( 4 * 1024 ) )` returns the
`max: 4798.08 MB` value.
Increasing the maximum to `1200`, for example, would set a `max: 5488 MB` value.
Workers use additional memory on top of the shared memory. The amount of memory
depends on a site's use of GitLab.
`memwd_rss_bytes` is the actual amount of memory consumed, and `memwd_max_rss_bytes` is the
RSS limit set through `per_worker_max_memory_mb`.
## Change the worker timeout
@ -146,7 +136,7 @@ for details.
When running Puma in single mode, some features are not supported:
- [Phased restart](https://gitlab.com/gitlab-org/gitlab/-/issues/300665)
- [Puma Worker Killer](https://gitlab.com/gitlab-org/gitlab/-/issues/300664)
- [Memory killers](#reducing-memory-use)
To learn more, visit [epic 5303](https://gitlab.com/groups/gitlab-org/-/epics/5303).

View File

@ -215,7 +215,7 @@ To access private container registries, the GitLab Runner process can use:
To define which option should be used, the runner process reads the configuration in this order:
- A `DOCKER_AUTH_CONFIG` [CI/CD variable](../variables/index.md).
- A `DOCKER_AUTH_CONFIG` [CI/CD variable](../variables/index.md) of the [type `Variable`](../variables/index.md#cicd-variable-types).
- A `DOCKER_AUTH_CONFIG` environment variable set in the runner's `config.toml` file.
- A `config.json` file in `$HOME/.docker` directory of the user running the process.
If the `--user` flag is provided to run the child processes as unprivileged user,

View File

@ -247,8 +247,8 @@ works.
### Puma per worker maximum memory
By default, each Puma worker is limited to 1024 MB of memory.
This setting [can be adjusted](../administration/operations/puma.md#change-the-memory-limit-setting) and should be considered
By default, each Puma worker is limited to 1.2 GB of memory.
You can [adjust this memory setting](../administration/operations/puma.md#reducing-memory-use) and should do so
if you must increase the number of Puma workers.
## Redis and Sidekiq

View File

@ -54,7 +54,7 @@ To add a new application for your user:
- The OAuth 2 Client ID in the **Application ID** field.
- The OAuth 2 Client Secret, accessible:
- In the **Secret** field in GitLab 14.1 and earlier.
- Using the **Copy** button on the **Secret** field
- By selecting **Copy** in the **Secret** field
[in GitLab 14.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/332844).
## Group owned applications
@ -63,7 +63,7 @@ To add a new application for your user:
To add a new application for a group:
1. Navigate to the desired group.
1. Go to the desired group.
1. On the left sidebar, select **Settings > Applications**.
1. Enter a **Name**, **Redirect URI** and OAuth 2 scopes as defined in [Authorized Applications](#authorized-applications).
The **Redirect URI** is the URL where users are sent after they authorize with GitLab.
@ -72,7 +72,7 @@ To add a new application for a group:
- The OAuth 2 Client ID in the **Application ID** field.
- The OAuth 2 Client Secret, accessible:
- In the **Secret** field in GitLab 14.1 and earlier.
- Using the **Copy** button on the **Secret** field
- By selecting **Copy** in the **Secret** field
[in GitLab 14.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/332844).
## Instance-wide applications

View File

@ -1,5 +1,5 @@
variables:
DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.42.1'
DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.45.0'
.dast-auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${DAST_AUTO_DEPLOY_IMAGE_VERSION}"

View File

@ -1,5 +1,5 @@
variables:
AUTO_DEPLOY_IMAGE_VERSION: 'v2.42.1'
AUTO_DEPLOY_IMAGE_VERSION: 'v2.45.0'
.auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}"

View File

@ -1,5 +1,5 @@
variables:
AUTO_DEPLOY_IMAGE_VERSION: 'v2.42.1'
AUTO_DEPLOY_IMAGE_VERSION: 'v2.45.0'
.auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}"

View File

@ -44,28 +44,28 @@ module Gitlab
TRANSLATION_LEVELS = {
'bg' => 0,
'cs_CZ' => 0,
'da_DK' => 36,
'de' => 17,
'da_DK' => 35,
'de' => 16,
'en' => 100,
'eo' => 0,
'es' => 35,
'es' => 34,
'fil_PH' => 0,
'fr' => 94,
'fr' => 98,
'gl_ES' => 0,
'id_ID' => 0,
'it' => 1,
'ja' => 30,
'ja' => 29,
'ko' => 20,
'nb_NO' => 24,
'nl_NL' => 0,
'pl_PL' => 3,
'pt_BR' => 57,
'ro_RO' => 96,
'ro_RO' => 94,
'ru' => 26,
'si_LK' => 11,
'tr_TR' => 11,
'uk' => 52,
'zh_CN' => 97,
'tr_TR' => 10,
'uk' => 54,
'zh_CN' => 98,
'zh_HK' => 1,
'zh_TW' => 99
}.freeze

View File

@ -1375,6 +1375,9 @@ msgstr ""
msgid ", or "
msgstr ""
msgid "-"
msgstr ""
msgid "- %{policy_name} (notifying after %{elapsed_time} minutes unless %{status})"
msgstr ""
@ -11821,6 +11824,9 @@ msgstr ""
msgid "Created a branch and a merge request to resolve this issue."
msgstr ""
msgid "Created at"
msgstr ""
msgid "Created branch '%{branch_name}' and a merge request to resolve this issue."
msgstr ""

View File

@ -122,7 +122,7 @@
"dateformat": "^5.0.1",
"deckar01-task_list": "^2.3.1",
"diff": "^3.4.0",
"dompurify": "^2.4.2",
"dompurify": "^2.4.3",
"dropzone": "^4.2.0",
"editorconfig": "^0.15.3",
"emoji-regex": "^10.0.0",

View File

@ -158,11 +158,9 @@ RSpec.describe 'Dashboard Todos', feature_category: :team_planning do
context 'when todo has a note' do
let(:note) { create(:note, project: project, note: "Check out stuff", noteable: create(:issue, project: project)) }
let!(:todo) { create(:todo, :mentioned, user: user, project: project, author: author, note: note, target: note.noteable) }
let(:two_line_mention_enabled_enabled) { true }
before do
sign_in(user)
stub_feature_flags(two_line_mention_enabled: two_line_mention_enabled_enabled)
visit dashboard_todos_path
end
@ -171,14 +169,6 @@ RSpec.describe 'Dashboard Todos', feature_category: :team_planning do
expect(page).to have_no_content('"Check out stuff"')
expect(page).to have_content('Check out stuff')
end
context 'when two_line_mention_enabled_enabled is disabled' do
let(:two_line_mention_enabled_enabled) { false }
it 'shows mention previews on one line' do
expect(page).to have_content("#{author.name} mentioned you: \"Check out stuff\"")
end
end
end
end

View File

@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import httpStatus, {
HTTP_STATUS_ACCEPTED,
HTTP_STATUS_CREATED,
HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_NO_CONTENT,
HTTP_STATUS_NOT_FOUND,
} from '~/lib/utils/http_status';
@ -699,7 +700,7 @@ describe('Api', () => {
describe('when an error occurs while fetching an issue template', () => {
it('rejects the Promise', () => {
mock.onGet(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return new Promise((resolve) => {
Api.issueTemplate(namespace, project, templateKey, templateType, () => {
@ -737,7 +738,7 @@ describe('Api', () => {
describe('when an error occurs while fetching issue templates', () => {
it('rejects the Promise', () => {
mock.onGet(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
Api.issueTemplates(namespace, project, templateType, () => {
expect(mock.history.get).toHaveLength(1);
@ -1032,7 +1033,7 @@ describe('Api', () => {
describe('when an error occurs while fetching releases', () => {
it('rejects the Promise', () => {
mock.onGet(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Api.releases(dummyProjectPath).catch(() => {
expect(mock.history.get).toHaveLength(1);
@ -1056,7 +1057,7 @@ describe('Api', () => {
describe('when an error occurs while fetching the release', () => {
it('rejects the Promise', () => {
mock.onGet(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Api.release(dummyProjectPath, dummyTagName).catch(() => {
expect(mock.history.get).toHaveLength(1);
@ -1084,7 +1085,7 @@ describe('Api', () => {
describe('when an error occurs while creating the release', () => {
it('rejects the Promise', () => {
mock.onPost(expectedUrl, release).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
mock.onPost(expectedUrl, release).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Api.createRelease(dummyProjectPath, release).catch(() => {
expect(mock.history.post).toHaveLength(1);
@ -1112,7 +1113,7 @@ describe('Api', () => {
describe('when an error occurs while updating the release', () => {
it('rejects the Promise', () => {
mock.onPut(expectedUrl, release).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
mock.onPut(expectedUrl, release).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Api.updateRelease(dummyProjectPath, dummyTagName, release).catch(() => {
expect(mock.history.put).toHaveLength(1);
@ -1140,7 +1141,7 @@ describe('Api', () => {
describe('when an error occurs while creating the Release', () => {
it('rejects the Promise', () => {
mock.onPost(expectedUrl, expectedLink).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
mock.onPost(expectedUrl, expectedLink).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Api.createReleaseLink(dummyProjectPath, dummyTagName, expectedLink).catch(() => {
expect(mock.history.post).toHaveLength(1);
@ -1165,7 +1166,7 @@ describe('Api', () => {
describe('when an error occurs while deleting the Release', () => {
it('rejects the Promise', () => {
mock.onDelete(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
mock.onDelete(expectedUrl).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Api.deleteReleaseLink(dummyProjectPath, dummyTagName, dummyLinkId).catch(() => {
expect(mock.history.delete).toHaveLength(1);
@ -1207,7 +1208,7 @@ describe('Api', () => {
describe('when an error occurs while getting a raw file', () => {
it('rejects the Promise', () => {
mock.onPost(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
mock.onPost(expectedUrl).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Api.getRawFile(dummyProjectPath, dummyFilePath).catch(() => {
expect(mock.history.get).toHaveLength(1);
@ -1239,7 +1240,7 @@ describe('Api', () => {
describe('when an error occurs while getting a raw file', () => {
it('rejects the Promise', () => {
mock.onPost(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
mock.onPost(expectedUrl).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Api.createProjectMergeRequest(dummyProjectPath).catch(() => {
expect(mock.history.post).toHaveLength(1);

View File

@ -7,8 +7,9 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import PipelinesTable from '~/commit/pipelines/pipelines_table.vue';
import httpStatusCodes, {
import {
HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_UNAUTHORIZED,
} from '~/lib/utils/http_status';
import { createAlert } from '~/flash';
@ -246,10 +247,10 @@ describe('Pipelines table in Commits and Merge requests', () => {
'An error occurred while trying to run a new pipeline for this merge request.';
it.each`
status | message
${HTTP_STATUS_BAD_REQUEST} | ${defaultMsg}
${HTTP_STATUS_UNAUTHORIZED} | ${permissionsMsg}
${httpStatusCodes.INTERNAL_SERVER_ERROR} | ${defaultMsg}
status | message
${HTTP_STATUS_BAD_REQUEST} | ${defaultMsg}
${HTTP_STATUS_UNAUTHORIZED} | ${permissionsMsg}
${HTTP_STATUS_INTERNAL_SERVER_ERROR} | ${defaultMsg}
`('displays permissions error message', async ({ status, message }) => {
const response = { response: { status } };

View File

@ -8,7 +8,7 @@ import Video from '~/content_editor/extensions/video';
import Link from '~/content_editor/extensions/link';
import Loading from '~/content_editor/extensions/loading';
import { VARIANT_DANGER } from '~/flash';
import httpStatus from '~/lib/utils/http_status';
import httpStatus, { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor, createDocBuilder } from '../test_utils';
import {
@ -167,7 +167,7 @@ describe('content_editor/extensions/attachment', () => {
describe('when uploading request fails', () => {
beforeEach(() => {
mock.onPost().reply(httpStatus.INTERNAL_SERVER_ERROR);
mock.onPost().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('resets the doc to original state', async () => {
@ -246,7 +246,7 @@ describe('content_editor/extensions/attachment', () => {
describe('when uploading request fails', () => {
beforeEach(() => {
mock.onPost().reply(httpStatus.INTERNAL_SERVER_ERROR);
mock.onPost().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('resets the doc to orginal state', async () => {

View File

@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { dismiss } from '~/feature_highlight/feature_highlight_helper';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes, { HTTP_STATUS_CREATED } from '~/lib/utils/http_status';
import { HTTP_STATUS_CREATED, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
jest.mock('~/flash');
@ -11,7 +11,6 @@ describe('feature highlight helper', () => {
let mockAxios;
const endpoint = '/-/callouts/dismiss';
const highlightId = '123';
const { INTERNAL_SERVER_ERROR } = httpStatusCodes;
beforeEach(() => {
mockAxios = new MockAdapter(axios);
@ -28,7 +27,9 @@ describe('feature highlight helper', () => {
});
it('triggers flash when dismiss request fails', async () => {
mockAxios.onPost(endpoint, { feature_name: highlightId }).replyOnce(INTERNAL_SERVER_ERROR);
mockAxios
.onPost(endpoint, { feature_name: highlightId })
.replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await dismiss(endpoint, highlightId);

View File

@ -22,7 +22,7 @@ import {
billingPlanNames,
} from '~/integrations/constants';
import { createStore } from '~/integrations/edit/store';
import httpStatus from '~/lib/utils/http_status';
import httpStatus, { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import {
mockIntegrationProps,
@ -456,11 +456,11 @@ describe('IntegrationForm', () => {
});
describe.each`
scenario | replyStatus | errorMessage | serviceResponse | expectToast | expectSentry
${'when "test settings" request fails'} | ${httpStatus.INTERNAL_SERVER_ERROR} | ${undefined} | ${undefined} | ${I18N_DEFAULT_ERROR_MESSAGE} | ${true}
${'when "test settings" returns an error'} | ${httpStatus.OK} | ${'an error'} | ${undefined} | ${'an error'} | ${false}
${'when "test settings" returns an error with details'} | ${httpStatus.OK} | ${'an error.'} | ${'extra info'} | ${'an error. extra info'} | ${false}
${'when "test settings" succeeds'} | ${httpStatus.OK} | ${undefined} | ${undefined} | ${I18N_SUCCESSFUL_CONNECTION_MESSAGE} | ${false}
scenario | replyStatus | errorMessage | serviceResponse | expectToast | expectSentry
${'when "test settings" request fails'} | ${HTTP_STATUS_INTERNAL_SERVER_ERROR} | ${undefined} | ${undefined} | ${I18N_DEFAULT_ERROR_MESSAGE} | ${true}
${'when "test settings" returns an error'} | ${httpStatus.OK} | ${'an error'} | ${undefined} | ${'an error'} | ${false}
${'when "test settings" returns an error with details'} | ${httpStatus.OK} | ${'an error.'} | ${'extra info'} | ${'an error. extra info'} | ${false}
${'when "test settings" succeeds'} | ${httpStatus.OK} | ${undefined} | ${undefined} | ${I18N_SUCCESSFUL_CONNECTION_MESSAGE} | ${false}
`(
'$scenario',
({ replyStatus, errorMessage, serviceResponse, expectToast, expectSentry }) => {
@ -491,7 +491,7 @@ describe('IntegrationForm', () => {
const mockResetPath = '/reset';
beforeEach(async () => {
mockAxios.onPost(mockResetPath).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
mockAxios.onPost(mockResetPath).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent({
customStateProps: {
resetPath: mockResetPath,

View File

@ -8,7 +8,7 @@ import IntegrationOverrides from '~/integrations/overrides/components/integratio
import IntegrationTabs from '~/integrations/overrides/components/integration_tabs.vue';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import httpStatus, { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
@ -125,7 +125,7 @@ describe('IntegrationOverrides', () => {
describe('when request fails', () => {
beforeEach(async () => {
jest.spyOn(Sentry, 'captureException');
mockAxios.onGet(defaultProps.overridesPath).reply(httpStatus.INTERNAL_SERVER_ERROR);
mockAxios.onGet(defaultProps.overridesPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent();
await waitForPromises();

View File

@ -24,7 +24,11 @@ import {
import eventHub from '~/invite_members/event_hub';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
import axios from '~/lib/utils/axios_utils';
import httpStatus, { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_CREATED } from '~/lib/utils/http_status';
import {
HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_CREATED,
HTTP_STATUS_INTERNAL_SERVER_ERROR,
} from '~/lib/utils/http_status';
import { getParameterValues } from '~/lib/utils/url_utility';
import {
displaySuccessfulInvitationAlert,
@ -549,7 +553,7 @@ describe('InviteMembersModal', () => {
it('displays the generic error for http server error', async () => {
mockInvitationsApi(
httpStatus.INTERNAL_SERVER_ERROR,
HTTP_STATUS_INTERNAL_SERVER_ERROR,
'Request failed with status code 500',
);

View File

@ -95,8 +95,8 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
<table
aria-busy="false"
aria-colcount="6"
class="table b-table gl-table gl-mt-0! table-sm"
aria-colcount="9"
class="table b-table gl-table gl-mt-0! ml-candidate-table table-sm"
role="table"
>
<!---->
@ -117,7 +117,7 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
scope="col"
>
<div>
L1 Ratio
Name
</div>
</th>
<th
@ -127,7 +127,7 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
scope="col"
>
<div>
Rmse
Created at
</div>
</th>
<th
@ -137,7 +137,7 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
scope="col"
>
<div>
Auc
User
</div>
</th>
<th
@ -147,11 +147,41 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
scope="col"
>
<div>
Mae
L1 Ratio
</div>
</th>
<th
aria-colindex="5"
class=""
role="columnheader"
scope="col"
>
<div>
Rmse
</div>
</th>
<th
aria-colindex="6"
class=""
role="columnheader"
scope="col"
>
<div>
Auc
</div>
</th>
<th
aria-colindex="7"
class=""
role="columnheader"
scope="col"
>
<div>
Mae
</div>
</th>
<th
aria-colindex="8"
aria-label="Details"
class=""
role="columnheader"
@ -160,7 +190,7 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
<div />
</th>
<th
aria-colindex="6"
aria-colindex="9"
aria-label="Artifact"
class=""
role="columnheader"
@ -183,39 +213,97 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
class=""
role="cell"
>
0.4
<div
title="aCandidate"
>
aCandidate
</div>
</td>
<td
aria-colindex="2"
class=""
role="cell"
>
1
<time
class=""
datetime="2023-01-05T14:07:02.975Z"
title="2023-01-05T14:07:02.975Z"
>
in 2 years
</time>
</td>
<td
aria-colindex="3"
class=""
role="cell"
/>
>
<a
class="gl-link"
href="/root"
title="root"
>
@root
</a>
</td>
<td
aria-colindex="4"
class=""
role="cell"
/>
>
<div
title="0.4"
>
0.4
</div>
</td>
<td
aria-colindex="5"
class=""
role="cell"
>
<div
title="1"
>
1
</div>
</td>
<td
aria-colindex="6"
class=""
role="cell"
>
<div
title=""
>
</div>
</td>
<td
aria-colindex="7"
class=""
role="cell"
>
<div
title=""
>
</div>
</td>
<td
aria-colindex="8"
class=""
role="cell"
>
<a
class="gl-link"
href="link_to_candidate1"
title="Details"
>
Details
</a>
</td>
<td
aria-colindex="6"
aria-colindex="9"
class=""
role="cell"
>
@ -224,6 +312,7 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
href="link_to_artifact"
rel="noopener"
target="_blank"
title="Artifacts"
>
Artifacts
</a>
@ -238,42 +327,104 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
class=""
role="cell"
>
0.5
<div
title=""
>
</div>
</td>
<td
aria-colindex="2"
class=""
role="cell"
/>
>
<time
class=""
datetime="2023-01-05T14:07:02.975Z"
title="2023-01-05T14:07:02.975Z"
>
in 2 years
</time>
</td>
<td
aria-colindex="3"
class=""
role="cell"
>
0.3
<div>
-
</div>
</td>
<td
aria-colindex="4"
class=""
role="cell"
/>
>
<div
title="0.5"
>
0.5
</div>
</td>
<td
aria-colindex="5"
class=""
role="cell"
>
<div
title=""
>
</div>
</td>
<td
aria-colindex="6"
class=""
role="cell"
>
<div
title="0.3"
>
0.3
</div>
</td>
<td
aria-colindex="7"
class=""
role="cell"
>
<div
title=""
>
</div>
</td>
<td
aria-colindex="8"
class=""
role="cell"
>
<a
class="gl-link"
href="link_to_candidate2"
title="Details"
>
Details
</a>
</td>
<td
aria-colindex="6"
aria-colindex="9"
class=""
role="cell"
/>
>
<div
title="Artifacts"
>
-
</div>
</td>
</tr>
<tr
class=""
@ -284,42 +435,104 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
class=""
role="cell"
>
0.5
<div
title=""
>
</div>
</td>
<td
aria-colindex="2"
class=""
role="cell"
/>
>
<time
class=""
datetime="2023-01-05T14:07:02.975Z"
title="2023-01-05T14:07:02.975Z"
>
in 2 years
</time>
</td>
<td
aria-colindex="3"
class=""
role="cell"
>
0.3
<div>
-
</div>
</td>
<td
aria-colindex="4"
class=""
role="cell"
/>
>
<div
title="0.5"
>
0.5
</div>
</td>
<td
aria-colindex="5"
class=""
role="cell"
>
<div
title=""
>
</div>
</td>
<td
aria-colindex="6"
class=""
role="cell"
>
<div
title="0.3"
>
0.3
</div>
</td>
<td
aria-colindex="7"
class=""
role="cell"
>
<div
title=""
>
</div>
</td>
<td
aria-colindex="8"
class=""
role="cell"
>
<a
class="gl-link"
href="link_to_candidate3"
title="Details"
>
Details
</a>
</td>
<td
aria-colindex="6"
aria-colindex="9"
class=""
role="cell"
/>
>
<div
title="Artifacts"
>
-
</div>
</td>
</tr>
<tr
class=""
@ -330,42 +543,104 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
class=""
role="cell"
>
0.5
<div
title=""
>
</div>
</td>
<td
aria-colindex="2"
class=""
role="cell"
/>
>
<time
class=""
datetime="2023-01-05T14:07:02.975Z"
title="2023-01-05T14:07:02.975Z"
>
in 2 years
</time>
</td>
<td
aria-colindex="3"
class=""
role="cell"
>
0.3
<div>
-
</div>
</td>
<td
aria-colindex="4"
class=""
role="cell"
/>
>
<div
title="0.5"
>
0.5
</div>
</td>
<td
aria-colindex="5"
class=""
role="cell"
>
<div
title=""
>
</div>
</td>
<td
aria-colindex="6"
class=""
role="cell"
>
<div
title="0.3"
>
0.3
</div>
</td>
<td
aria-colindex="7"
class=""
role="cell"
>
<div
title=""
>
</div>
</td>
<td
aria-colindex="8"
class=""
role="cell"
>
<a
class="gl-link"
href="link_to_candidate4"
title="Details"
>
Details
</a>
</td>
<td
aria-colindex="6"
aria-colindex="9"
class=""
role="cell"
/>
>
<div
title="Artifacts"
>
-
</div>
</td>
</tr>
<tr
class=""
@ -376,42 +651,104 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
class=""
role="cell"
>
0.5
<div
title=""
>
</div>
</td>
<td
aria-colindex="2"
class=""
role="cell"
/>
>
<time
class=""
datetime="2023-01-05T14:07:02.975Z"
title="2023-01-05T14:07:02.975Z"
>
in 2 years
</time>
</td>
<td
aria-colindex="3"
class=""
role="cell"
>
0.3
<div>
-
</div>
</td>
<td
aria-colindex="4"
class=""
role="cell"
/>
>
<div
title="0.5"
>
0.5
</div>
</td>
<td
aria-colindex="5"
class=""
role="cell"
>
<div
title=""
>
</div>
</td>
<td
aria-colindex="6"
class=""
role="cell"
>
<div
title="0.3"
>
0.3
</div>
</td>
<td
aria-colindex="7"
class=""
role="cell"
>
<div
title=""
>
</div>
</td>
<td
aria-colindex="8"
class=""
role="cell"
>
<a
class="gl-link"
href="link_to_candidate5"
title="Details"
>
Details
</a>
</td>
<td
aria-colindex="6"
aria-colindex="9"
class=""
role="cell"
/>
>
<div
title="Artifacts"
>
-
</div>
</td>
</tr>
<!---->
<!---->

View File

@ -46,11 +46,47 @@ describe('MlExperiment', () => {
const createWrapperWithCandidates = (pagination = defaultPagination) => {
return createWrapper(
[
{ rmse: 1, l1_ratio: 0.4, details: 'link_to_candidate1', artifact: 'link_to_artifact' },
{ auc: 0.3, l1_ratio: 0.5, details: 'link_to_candidate2' },
{ auc: 0.3, l1_ratio: 0.5, details: 'link_to_candidate3' },
{ auc: 0.3, l1_ratio: 0.5, details: 'link_to_candidate4' },
{ auc: 0.3, l1_ratio: 0.5, details: 'link_to_candidate5' },
{
rmse: 1,
l1_ratio: 0.4,
details: 'link_to_candidate1',
artifact: 'link_to_artifact',
name: 'aCandidate',
created_at: '2023-01-05T14:07:02.975Z',
user: { username: 'root', path: '/root' },
},
{
auc: 0.3,
l1_ratio: 0.5,
details: 'link_to_candidate2',
created_at: '2023-01-05T14:07:02.975Z',
name: null,
user: null,
},
{
auc: 0.3,
l1_ratio: 0.5,
details: 'link_to_candidate3',
created_at: '2023-01-05T14:07:02.975Z',
name: null,
user: null,
},
{
auc: 0.3,
l1_ratio: 0.5,
details: 'link_to_candidate4',
created_at: '2023-01-05T14:07:02.975Z',
name: null,
user: null,
},
{
auc: 0.3,
l1_ratio: 0.5,
details: 'link_to_candidate5',
created_at: '2023-01-05T14:07:02.975Z',
name: null,
user: null,
},
],
['rmse', 'auc', 'mae'],
['l1_ratio'],

View File

@ -5,6 +5,7 @@ import * as commonUtils from '~/lib/utils/common_utils';
import statusCodes, {
HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_NO_CONTENT,
HTTP_STATUS_SERVICE_UNAVAILABLE,
HTTP_STATUS_UNAUTHORIZED,
HTTP_STATUS_UNPROCESSABLE_ENTITY,
} from '~/lib/utils/http_status';
@ -138,7 +139,7 @@ describe('monitoring metrics_requests', () => {
code | reason
${HTTP_STATUS_BAD_REQUEST} | ${'Parameters are missing or incorrect'}
${HTTP_STATUS_UNPROCESSABLE_ENTITY} | ${"Expression can't be executed"}
${statusCodes.SERVICE_UNAVAILABLE} | ${'Query timed out or aborted'}
${HTTP_STATUS_SERVICE_UNAVAILABLE} | ${'Query timed out or aborted'}
`('rejects with details: "$reason" after getting an HTTP $code error', ({ code, reason }) => {
mock.onGet(prometheusEndpoint).reply(code, {
status: 'error',

View File

@ -1,4 +1,4 @@
import httpStatusCodes, { HTTP_STATUS_BAD_REQUEST } from '~/lib/utils/http_status';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_SERVICE_UNAVAILABLE } from '~/lib/utils/http_status';
import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
import * as types from '~/monitoring/stores/mutation_types';
import mutations from '~/monitoring/stores/mutations';
@ -317,7 +317,7 @@ describe('Monitoring mutations', () => {
metricId,
error: {
response: {
status: httpStatusCodes.SERVICE_UNAVAILABLE,
status: HTTP_STATUS_SERVICE_UNAVAILABLE,
},
},
});

View File

@ -63,6 +63,14 @@ describe('DeleteModal', () => {
expect(wrapper.emitted('confirm')).toHaveLength(1);
});
it('emits cancel when cancel event is emitted', () => {
expect(wrapper.emitted('cancel')).toBeUndefined();
findModal().vm.$emit('cancel');
expect(wrapper.emitted('cancel')).toHaveLength(1);
});
it('show calls gl-modal show', () => {
findModal().vm.show();

View File

@ -1,14 +1,19 @@
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue';
import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import {
DELETE_PACKAGE_TRACKING_ACTION,
DELETE_PACKAGES_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGES_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
CANCEL_DELETE_PACKAGES_TRACKING_ACTION,
} from '~/packages_and_registries/package_registry/constants';
import PackagesList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
import Tracking from '~/tracking';
@ -44,6 +49,7 @@ describe('packages_list', () => {
const findRegistryList = () => wrapper.findComponent(RegistryList);
const findPackagesListRow = () => wrapper.findComponent(PackagesListRow);
const findErrorPackageAlert = () => wrapper.findComponent(GlAlert);
const findDeletePackagesModal = () => wrapper.findComponent(DeleteModal);
const mountComponent = (props) => {
wrapper = shallowMountExtended(PackagesList, {
@ -53,6 +59,11 @@ describe('packages_list', () => {
},
stubs: {
DeletePackageModal,
DeleteModal: stubComponent(DeleteModal, {
methods: {
show: jest.fn(),
},
}),
GlSprintf,
RegistryList,
},
@ -125,20 +136,48 @@ describe('packages_list', () => {
});
});
describe('when the user can destroy the package', () => {
beforeEach(async () => {
describe.each`
description | finderFunction | deletePayload
${'when the user can destroy the package'} | ${findPackagesListRow} | ${firstPackage}
${'when the user can bulk destroy packages and deletes only one package'} | ${findRegistryList} | ${[firstPackage]}
`('$description', ({ finderFunction, deletePayload }) => {
let eventSpy;
const category = 'UI::NpmPackages';
beforeEach(() => {
eventSpy = jest.spyOn(Tracking, 'event');
mountComponent();
await findPackagesListRow().vm.$emit('delete', firstPackage);
finderFunction().vm.$emit('delete', deletePayload);
});
it('passes itemToBeDeleted to the modal', () => {
expect(findPackageListDeleteModal().props('itemToBeDeleted')).toStrictEqual(firstPackage);
});
it('emits package:delete when modal confirms', async () => {
await findPackageListDeleteModal().vm.$emit('ok');
it('requesting delete tracks the right action', () => {
expect(eventSpy).toHaveBeenCalledWith(
category,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
expect.any(Object),
);
});
expect(wrapper.emitted('package:delete')[0]).toEqual([firstPackage]);
describe('when modal confirms', () => {
beforeEach(() => {
findPackageListDeleteModal().vm.$emit('ok');
});
it('emits package:delete when modal confirms', () => {
expect(wrapper.emitted('package:delete')[0]).toEqual([firstPackage]);
});
it('tracks the right action', () => {
expect(eventSpy).toHaveBeenCalledWith(
category,
DELETE_PACKAGE_TRACKING_ACTION,
expect.any(Object),
);
});
});
it.each(['ok', 'cancel'])('resets itemToBeDeleted when modal emits %s', async (event) => {
@ -146,26 +185,73 @@ describe('packages_list', () => {
expect(findPackageListDeleteModal().props('itemToBeDeleted')).toBeNull();
});
it('canceling delete tracks the right action', () => {
findPackageListDeleteModal().vm.$emit('cancel');
expect(eventSpy).toHaveBeenCalledWith(
category,
CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
expect.any(Object),
);
});
});
describe('when the user can bulk destroy packages', () => {
let eventSpy;
const items = [firstPackage, secondPackage];
beforeEach(() => {
eventSpy = jest.spyOn(Tracking, 'event');
mountComponent();
findRegistryList().vm.$emit('delete', items);
});
it('passes itemToBeDeleted to the modal when there is only one package', async () => {
await findRegistryList().vm.$emit('delete', [firstPackage]);
expect(findPackageListDeleteModal().props('itemToBeDeleted')).toStrictEqual(firstPackage);
it('passes itemsToBeDeleted to the modal', () => {
expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual(items);
expect(wrapper.emitted('delete')).toBeUndefined();
});
it('emits delete when there is more than one package', () => {
const items = [firstPackage, secondPackage];
findRegistryList().vm.$emit('delete', items);
it('requesting delete tracks the right action', () => {
expect(eventSpy).toHaveBeenCalledWith(
undefined,
REQUEST_DELETE_PACKAGES_TRACKING_ACTION,
expect.any(Object),
);
});
expect(wrapper.emitted('delete')).toHaveLength(1);
expect(wrapper.emitted('delete')[0]).toEqual([items]);
describe('when modal confirms', () => {
beforeEach(() => {
findDeletePackagesModal().vm.$emit('confirm');
});
it('emits delete event', () => {
expect(wrapper.emitted('delete')[0]).toEqual([items]);
});
it('tracks the right action', () => {
expect(eventSpy).toHaveBeenCalledWith(
undefined,
DELETE_PACKAGES_TRACKING_ACTION,
expect.any(Object),
);
});
});
it.each(['confirm', 'cancel'])('resets itemsToBeDeleted when modal emits %s', async (event) => {
await findDeletePackagesModal().vm.$emit(event);
expect(findDeletePackagesModal().props('itemsToBeDeleted')).toHaveLength(0);
});
it('canceling delete tracks the right action', () => {
findDeletePackagesModal().vm.$emit('cancel');
expect(eventSpy).toHaveBeenCalledWith(
undefined,
CANCEL_DELETE_PACKAGES_TRACKING_ACTION,
expect.any(Object),
);
});
});
@ -223,44 +309,4 @@ describe('packages_list', () => {
expect(wrapper.emitted('next-page')).toHaveLength(1);
});
});
describe('tracking', () => {
let eventSpy;
const category = 'UI::NpmPackages';
beforeEach(() => {
eventSpy = jest.spyOn(Tracking, 'event');
mountComponent();
findPackagesListRow().vm.$emit('delete', firstPackage);
return nextTick();
});
it('requesting the delete tracks the right action', () => {
expect(eventSpy).toHaveBeenCalledWith(
category,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
expect.any(Object),
);
});
it('confirming delete tracks the right action', () => {
findPackageListDeleteModal().vm.$emit('ok');
expect(eventSpy).toHaveBeenCalledWith(
category,
DELETE_PACKAGE_TRACKING_ACTION,
expect.any(Object),
);
});
it('canceling delete tracks the right action', () => {
findPackageListDeleteModal().vm.$emit('cancel');
expect(eventSpy).toHaveBeenCalledWith(
category,
CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
expect.any(Object),
);
});
});
});

View File

@ -1,17 +1,14 @@
import { GlAlert, GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { stubComponent } from 'helpers/stub_component';
import ListPage from '~/packages_and_registries/package_registry/pages/list.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import OriginalPackageList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import {
PROJECT_RESOURCE_TYPE,
GROUP_RESOURCE_TYPE,
@ -62,7 +59,6 @@ describe('PackagesListApp', () => {
const findListComponent = () => wrapper.findComponent(PackageList);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findDeletePackage = () => wrapper.findComponent(DeletePackage);
const findDeletePackagesModal = () => wrapper.findComponent(DeleteModal);
const mountComponent = ({
resolver = jest.fn().mockResolvedValue(packagesListQuery()),
@ -87,11 +83,6 @@ describe('PackagesListApp', () => {
GlLink,
PackageList,
DeletePackage,
DeleteModal: stubComponent(DeleteModal, {
methods: {
show: jest.fn(),
},
}),
},
});
};
@ -296,18 +287,6 @@ describe('PackagesListApp', () => {
describe('bulk delete package', () => {
const items = [{ id: '1' }, { id: '2' }];
it('deletePackage is bound to package-list package:delete event', async () => {
mountComponent();
await waitForFirstRequest();
findListComponent().vm.$emit('delete', [{ id: '1' }, { id: '2' }]);
await waitForPromises();
expect(findDeletePackagesModal().props('itemsToBeDeleted')).toEqual(items);
});
it('calls mutation with the right values and shows success alert', async () => {
const mutationResolver = jest.fn().mockResolvedValue(packagesDestroyMutation());
mountComponent({
@ -318,8 +297,6 @@ describe('PackagesListApp', () => {
findListComponent().vm.$emit('delete', items);
findDeletePackagesModal().vm.$emit('confirm');
expect(mutationResolver).toHaveBeenCalledWith({
ids: items.map((item) => item.id),
});
@ -341,8 +318,6 @@ describe('PackagesListApp', () => {
findListComponent().vm.$emit('delete', items);
findDeletePackagesModal().vm.$emit('confirm');
await waitForPromises();
expect(findAlert().exists()).toBe(true);

View File

@ -5,7 +5,7 @@ import MockAdapter from 'axios-mock-adapter';
import WikiContent from '~/pages/shared/wikis/components/wiki_content.vue';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import httpStatus, { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import waitForPromises from 'helpers/wait_for_promises';
import { handleLocationHash } from '~/lib/utils/common_utils';
@ -88,7 +88,7 @@ describe('pages/shared/wikis/components/wiki_content', () => {
describe('when loading content fails', () => {
beforeEach(() => {
mock.onGet(PATH).replyOnce(httpStatus.INTERNAL_SERVER_ERROR, '');
mock.onGet(PATH).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, '');
buildWrapper();
return waitForPromises();
});

View File

@ -8,7 +8,10 @@ import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_help
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes, { HTTP_STATUS_BAD_REQUEST } from '~/lib/utils/http_status';
import httpStatusCodes, {
HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_INTERNAL_SERVER_ERROR,
} from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue';
import ciConfigVariablesQuery from '~/pipeline_new/graphql/queries/ci_config_variables.graphql';
@ -365,7 +368,7 @@ describe('Pipeline New Form', () => {
beforeEach(() => {
mock
.onGet(projectRefsEndpoint, { params: { search: '' } })
.reply(httpStatusCodes.INTERNAL_SERVER_ERROR);
.reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
findRefsDropdown().vm.$emit('loadingError');
});
@ -449,9 +452,7 @@ describe('Pipeline New Form', () => {
describe('when the error response cannot be handled', () => {
beforeEach(async () => {
mock
.onPost(pipelinesPath)
.reply(httpStatusCodes.INTERNAL_SERVER_ERROR, 'something went wrong');
mock.onPost(pipelinesPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, 'something went wrong');
findForm().vm.$emit('submit', dummySubmitEvent);

View File

@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import httpStatusCodes, { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import RefsDropdown from '~/pipeline_new/components/refs_dropdown.vue';
@ -166,7 +166,7 @@ describe('Pipeline New Form', () => {
beforeEach(async () => {
mock
.onGet(projectRefsEndpoint, { params: { search: '' } })
.reply(httpStatusCodes.INTERNAL_SERVER_ERROR);
.reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
findDropdown().vm.$emit('shown');
await waitForPromises();

View File

@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { loadCommits, isRequested, resetRequestedCommits } from '~/repository/commits_service';
import httpStatus from '~/lib/utils/http_status';
import httpStatus, { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import { createAlert } from '~/flash';
import { I18N_COMMIT_DATA_FETCH_ERROR } from '~/repository/constants';
import { refWithSpecialCharMock } from './mock_data';
@ -71,7 +71,7 @@ describe('commits service', () => {
it('calls `createAlert` when the request fails', async () => {
const invalidPath = '/#@ some/path';
const invalidUrl = `${url}${invalidPath}`;
mock.onGet(invalidUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR, [], {});
mock.onGet(invalidUrl).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, [], {});
await requestCommits(1, 'my-project', invalidPath);

View File

@ -25,7 +25,7 @@ import CodeIntelligence from '~/code_navigation/components/app.vue';
import * as urlUtility from '~/lib/utils/url_utility';
import { isLoggedIn, handleLocationHash } from '~/lib/utils/common_utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import httpStatusCodes from '~/lib/utils/http_status';
import httpStatusCodes, { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import LineHighlighter from '~/blob/line_highlighter';
import { LEGACY_FILE_TYPES } from '~/repository/constants';
import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
@ -368,7 +368,7 @@ describe('Blob content viewer component', () => {
it('does not load a CodeIntelligence component when no viewers are loaded', async () => {
const url = 'some_file.js?format=json&viewer=rich';
mockAxios.onGet(url).replyOnce(httpStatusCodes.INTERNAL_SERVER_ERROR);
mockAxios.onGet(url).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await createComponent({ blob: { ...richViewerMock, fileType: 'unknown' } });
expect(findCodeIntelligence().exists()).toBe(false);

View File

@ -8,7 +8,10 @@ import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
import httpStatusCodes, { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import httpStatusCodes, {
HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_NO_CONTENT,
} from '~/lib/utils/http_status';
import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
import { failedReport } from 'jest/ci/reports/mock_data/mock_data';
@ -91,7 +94,7 @@ describe('Test report extension', () => {
});
it('with an error response, displays failed to load text', async () => {
mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR);
mockApi(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent();
await waitForPromises();

View File

@ -6,7 +6,7 @@ import axios from '~/lib/utils/axios_utils';
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
import accessibilityExtension from '~/vue_merge_request_widget/extensions/accessibility';
import httpStatusCodes from '~/lib/utils/http_status';
import httpStatusCodes, { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import { accessibilityReportResponseErrors, accessibilityReportResponseSuccess } from './mock_data';
describe('Accessibility extension', () => {
@ -53,7 +53,7 @@ describe('Accessibility extension', () => {
});
it('displays failed loading text', async () => {
mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR);
mockApi(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent();

View File

@ -7,7 +7,10 @@ import axios from '~/lib/utils/axios_utils';
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
import codeQualityExtension from '~/vue_merge_request_widget/extensions/code_quality';
import httpStatusCodes, { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import httpStatusCodes, {
HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_NO_CONTENT,
} from '~/lib/utils/http_status';
import {
i18n,
codeQualityPrefixes,
@ -76,7 +79,7 @@ describe('Code Quality extension', () => {
});
it('displays failed loading text', async () => {
mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR);
mockApi(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent();

View File

@ -4,7 +4,7 @@ import testAction from 'helpers/vuex_action_helper';
import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data';
import Api from '~/api';
import { createAlert } from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import httpStatusCodes, { HTTP_STATUS_SERVICE_UNAVAILABLE } from '~/lib/utils/http_status';
import * as actions from '~/vue_shared/components/filtered_search_bar/store/modules/filters/actions';
import * as types from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutation_types';
import initialState from '~/vue_shared/components/filtered_search_bar/store/modules/filters/state';
@ -143,7 +143,7 @@ describe('Filters actions', () => {
describe('error', () => {
beforeEach(() => {
mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
mock.onAny().replyOnce(HTTP_STATUS_SERVICE_UNAVAILABLE);
});
it('dispatches RECEIVE_BRANCHES_ERROR', () => {
@ -155,7 +155,7 @@ describe('Filters actions', () => {
{ type: types.REQUEST_BRANCHES },
{
type: types.RECEIVE_BRANCHES_ERROR,
payload: httpStatusCodes.SERVICE_UNAVAILABLE,
payload: HTTP_STATUS_SERVICE_UNAVAILABLE,
},
],
[],
@ -215,7 +215,7 @@ describe('Filters actions', () => {
describe('error', () => {
beforeEach(() => {
mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
mock.onAny().replyOnce(HTTP_STATUS_SERVICE_UNAVAILABLE);
});
it('dispatches RECEIVE_AUTHORS_ERROR and groupEndpoint set', () => {
@ -227,7 +227,7 @@ describe('Filters actions', () => {
{ type: types.REQUEST_AUTHORS },
{
type: types.RECEIVE_AUTHORS_ERROR,
payload: httpStatusCodes.SERVICE_UNAVAILABLE,
payload: HTTP_STATUS_SERVICE_UNAVAILABLE,
},
],
[],
@ -246,7 +246,7 @@ describe('Filters actions', () => {
{ type: types.REQUEST_AUTHORS },
{
type: types.RECEIVE_AUTHORS_ERROR,
payload: httpStatusCodes.SERVICE_UNAVAILABLE,
payload: HTTP_STATUS_SERVICE_UNAVAILABLE,
},
],
[],
@ -282,7 +282,7 @@ describe('Filters actions', () => {
describe('error', () => {
beforeEach(() => {
mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
mock.onAny().replyOnce(HTTP_STATUS_SERVICE_UNAVAILABLE);
});
it('dispatches RECEIVE_MILESTONES_ERROR', () => {
@ -294,7 +294,7 @@ describe('Filters actions', () => {
{ type: types.REQUEST_MILESTONES },
{
type: types.RECEIVE_MILESTONES_ERROR,
payload: httpStatusCodes.SERVICE_UNAVAILABLE,
payload: HTTP_STATUS_SERVICE_UNAVAILABLE,
},
],
[],
@ -352,7 +352,7 @@ describe('Filters actions', () => {
describe('error', () => {
let restoreVersion;
beforeEach(() => {
mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
mock.onAny().replyOnce(HTTP_STATUS_SERVICE_UNAVAILABLE);
restoreVersion = gon.api_version;
gon.api_version = 'v1';
});
@ -370,7 +370,7 @@ describe('Filters actions', () => {
{ type: types.REQUEST_ASSIGNEES },
{
type: types.RECEIVE_ASSIGNEES_ERROR,
payload: httpStatusCodes.SERVICE_UNAVAILABLE,
payload: HTTP_STATUS_SERVICE_UNAVAILABLE,
},
],
[],
@ -389,7 +389,7 @@ describe('Filters actions', () => {
{ type: types.REQUEST_ASSIGNEES },
{
type: types.RECEIVE_ASSIGNEES_ERROR,
payload: httpStatusCodes.SERVICE_UNAVAILABLE,
payload: HTTP_STATUS_SERVICE_UNAVAILABLE,
},
],
[],
@ -425,7 +425,7 @@ describe('Filters actions', () => {
describe('error', () => {
beforeEach(() => {
mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
mock.onAny().replyOnce(HTTP_STATUS_SERVICE_UNAVAILABLE);
});
it('dispatches RECEIVE_LABELS_ERROR', () => {
@ -437,7 +437,7 @@ describe('Filters actions', () => {
{ type: types.REQUEST_LABELS },
{
type: types.RECEIVE_LABELS_ERROR,
payload: httpStatusCodes.SERVICE_UNAVAILABLE,
payload: HTTP_STATUS_SERVICE_UNAVAILABLE,
},
],
[],

View File

@ -18,7 +18,7 @@ RSpec.describe Projects::Ml::ExperimentsHelper, feature_category: :mlops do
end
let_it_be(:candidate1) do
create(:ml_candidates, experiment: experiment, user: project.creator).tap do |c|
create(:ml_candidates, experiment: experiment, user: project.creator, name: 'candidate1').tap do |c|
c.params.build([{ name: 'param2', value: 'p3' }, { name: 'param3', value: 'p4' }])
c.metrics.create!(name: 'metric3', value: 0.4)
end
@ -27,18 +27,39 @@ RSpec.describe Projects::Ml::ExperimentsHelper, feature_category: :mlops do
let_it_be(:candidates) { [candidate0, candidate1] }
describe '#candidates_table_items' do
subject { helper.candidates_table_items(candidates) }
subject { Gitlab::Json.parse(helper.candidates_table_items(candidates)) }
it 'creates the correct model for the table' do
expected_value = [
it 'creates the correct model for the table', :aggregate_failures do
expected_values = [
{ 'param1' => 'p1', 'param2' => 'p2', 'metric1' => '0.1000', 'metric2' => '0.2000', 'metric3' => '0.3000',
'artifact' => "/#{project.full_path}/-/packages/#{candidate0.artifact.id}",
'details' => "/#{project.full_path}/-/ml/candidates/#{candidate0.iid}" },
'details' => "/#{project.full_path}/-/ml/candidates/#{candidate0.iid}",
'name' => candidate0.name,
'created_at' => candidate0.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
'user' => { 'username' => candidate0.user.username, 'path' => "/#{candidate0.user.username}" } },
{ 'param2' => 'p3', 'param3' => 'p4', 'metric3' => '0.4000',
'artifact' => nil, 'details' => "/#{project.full_path}/-/ml/candidates/#{candidate1.iid}" }
'artifact' => nil, 'details' => "/#{project.full_path}/-/ml/candidates/#{candidate1.iid}",
'name' => candidate1.name,
'created_at' => candidate1.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'),
'user' => { 'username' => candidate1.user.username, 'path' => "/#{candidate1.user.username}" } }
]
expect(Gitlab::Json.parse(subject)).to match_array(expected_value)
subject.sort_by! { |s| s[:name] }
expect(subject[0]).to eq(expected_values[0])
expect(subject[1]).to eq(expected_values[1])
end
context 'when candidate does not have user' do
let(:candidates) { [candidate0] }
before do
allow(candidate0).to receive(:user).and_return(nil)
end
it 'has the user property, but is nil' do
expect(subject[0]['user']).to be_nil
end
end
end

View File

@ -121,12 +121,13 @@ RSpec.describe Ml::Candidate, factory_default: :keep do
end
end
describe "#including_metrics_and_params" do
subject { described_class.including_metrics_and_params.find_by(id: candidate.id) }
describe "#including_relationships" do
subject { described_class.including_relationships.find_by(id: candidate.id) }
it 'loads latest metrics and params', :aggregate_failures do
expect(subject.association_cached?(:latest_metrics)).to be(true)
expect(subject.association_cached?(:params)).to be(true)
expect(subject.association_cached?(:user)).to be(true)
end
end
end

View File

@ -5097,10 +5097,10 @@ dompurify@2.3.8:
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.8.tgz#224fe9ae57d7ebd9a1ae1ac18c1c1ca3f532226f"
integrity sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw==
dompurify@^2.4.1, dompurify@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.2.tgz#c3409b49357804c9b00e1fbebea81f26514c5bc3"
integrity sha512-ckbbxcGpfTJ7SNHC2yT2pHSCYxo2oQgSfdoDHQANzMzQyGzVmalF9W/B+X97Cdik5xFwWtwJP232gIP2+1kNEA==
dompurify@^2.4.1, dompurify@^2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.3.tgz#f4133af0e6a50297fc8874e2eaedc13a3c308c03"
integrity sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==
domutils@^2.5.2, domutils@^2.6.0:
version "2.6.0"