Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a70e2c0418
commit
31979cb323
33 changed files with 854 additions and 119 deletions
|
@ -1,5 +1,7 @@
|
|||
export const ANY_AUTHOR = 'Any';
|
||||
|
||||
export const NO_LABEL = 'No label';
|
||||
|
||||
export const DEBOUNCE_DELAY = 200;
|
||||
|
||||
export const SortDirection = {
|
||||
|
|
|
@ -184,6 +184,21 @@ export default {
|
|||
this.recentSearches = resultantSearches;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* When user hits Enter/Return key while typing tokens, we emit `onFilter`
|
||||
* event immediately so at that time, we don't want to keep tokens dropdown
|
||||
* visible on UI so this is essentially a hack which allows us to do that
|
||||
* until `GlFilteredSearch` natively supports this.
|
||||
* See this discussion https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36421#note_385729546
|
||||
*/
|
||||
blurSearchInput() {
|
||||
const searchInputEl = this.$refs.filteredSearchInput.$el.querySelector(
|
||||
'.gl-filtered-search-token-segment-input',
|
||||
);
|
||||
if (searchInputEl) {
|
||||
searchInputEl.blur();
|
||||
}
|
||||
},
|
||||
handleSortOptionClick(sortBy) {
|
||||
this.selectedSortOption = sortBy;
|
||||
this.$emit('onSort', sortBy.sortDirection[this.selectedSortDirection]);
|
||||
|
@ -217,6 +232,7 @@ export default {
|
|||
// https://gitlab.com/gitlab-org/gitlab-foss/issues/30821
|
||||
});
|
||||
}
|
||||
this.blurSearchInput();
|
||||
this.$emit('onFilter', filters);
|
||||
},
|
||||
},
|
||||
|
@ -226,6 +242,7 @@ export default {
|
|||
<template>
|
||||
<div class="vue-filtered-search-bar-container d-md-flex">
|
||||
<gl-filtered-search
|
||||
ref="filteredSearchInput"
|
||||
v-model="filterValue"
|
||||
:placeholder="searchInputPlaceholder"
|
||||
:available-tokens="tokens"
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
<script>
|
||||
import {
|
||||
GlToken,
|
||||
GlFilteredSearchToken,
|
||||
GlFilteredSearchSuggestion,
|
||||
GlDropdownDivider,
|
||||
GlLoadingIcon,
|
||||
} from '@gitlab/ui';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import createFlash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
|
||||
import { NO_LABEL, DEBOUNCE_DELAY } from '../constants';
|
||||
|
||||
export default {
|
||||
noLabel: NO_LABEL,
|
||||
components: {
|
||||
GlToken,
|
||||
GlFilteredSearchToken,
|
||||
GlFilteredSearchSuggestion,
|
||||
GlDropdownDivider,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
props: {
|
||||
config: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
labels: this.config.initialLabels || [],
|
||||
loading: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentValue() {
|
||||
return this.value.data.toLowerCase();
|
||||
},
|
||||
activeLabel() {
|
||||
// Strip double quotes
|
||||
const strippedCurrentValue = this.currentValue.includes(' ')
|
||||
? this.currentValue.substring(1, this.currentValue.length - 1)
|
||||
: this.currentValue;
|
||||
|
||||
return this.labels.find(label => label.title.toLowerCase() === strippedCurrentValue);
|
||||
},
|
||||
containerStyle() {
|
||||
if (this.activeLabel) {
|
||||
const { color, textColor } = convertObjectPropsToCamelCase(this.activeLabel);
|
||||
|
||||
return { backgroundColor: color, color: textColor };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
active: {
|
||||
immediate: true,
|
||||
handler(newValue) {
|
||||
if (!newValue && !this.labels.length) {
|
||||
this.fetchLabelBySearchTerm(this.value.data);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchLabelBySearchTerm(searchTerm) {
|
||||
this.loading = true;
|
||||
this.config
|
||||
.fetchLabels(searchTerm)
|
||||
.then(res => {
|
||||
// We'd want to avoid doing this check but
|
||||
// labels.json and /groups/:id/labels & /projects/:id/labels
|
||||
// return response differently.
|
||||
this.labels = Array.isArray(res) ? res : res.data;
|
||||
})
|
||||
.catch(() => createFlash(__('There was a problem fetching labels.')))
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
searchLabels: debounce(function debouncedSearch({ data }) {
|
||||
this.fetchLabelBySearchTerm(data);
|
||||
}, DEBOUNCE_DELAY),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-filtered-search-token
|
||||
:config="config"
|
||||
v-bind="{ ...$props, ...$attrs }"
|
||||
v-on="$listeners"
|
||||
@input="searchLabels"
|
||||
>
|
||||
<template #view-token="{ inputValue, cssClasses, listeners }">
|
||||
<gl-token variant="search-value" :class="cssClasses" :style="containerStyle" v-on="listeners">
|
||||
~{{ activeLabel ? activeLabel.title : inputValue }}
|
||||
</gl-token>
|
||||
</template>
|
||||
<template #suggestions>
|
||||
<gl-filtered-search-suggestion :value="$options.noLabel">
|
||||
{{ __('No label') }}
|
||||
</gl-filtered-search-suggestion>
|
||||
<gl-dropdown-divider />
|
||||
<gl-loading-icon v-if="loading" />
|
||||
<template v-else>
|
||||
<gl-filtered-search-suggestion v-for="label in labels" :key="label.id" :value="label.title">
|
||||
<div class="gl-display-flex">
|
||||
<span
|
||||
:style="{ backgroundColor: label.color }"
|
||||
class="gl-display-inline-block mr-2 p-2"
|
||||
></span>
|
||||
<div>{{ label.title }}</div>
|
||||
</div>
|
||||
</gl-filtered-search-suggestion>
|
||||
</template>
|
||||
</template>
|
||||
</gl-filtered-search-token>
|
||||
</template>
|
|
@ -274,8 +274,6 @@
|
|||
svg {
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
svg,
|
||||
|
|
|
@ -48,4 +48,12 @@ svg {
|
|||
@include svg-size(#{$svg-size}px);
|
||||
}
|
||||
}
|
||||
|
||||
&.s12 {
|
||||
vertical-align: -1px;
|
||||
}
|
||||
|
||||
&.s16 {
|
||||
vertical-align: -3px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,7 +159,7 @@
|
|||
font-weight: bold;
|
||||
|
||||
.icon {
|
||||
font-size: $gl-font-size-large;
|
||||
vertical-align: -1px;
|
||||
}
|
||||
|
||||
.home-panel-topic-list {
|
||||
|
|
|
@ -138,12 +138,6 @@
|
|||
}
|
||||
|
||||
.tree-item {
|
||||
.file-icon,
|
||||
.folder-icon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.link-container {
|
||||
padding: 0;
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
# action_id: integer
|
||||
# author_id: integer
|
||||
# project_id; integer
|
||||
# target_id; integer
|
||||
# state: 'pending' (default) or 'done'
|
||||
# type: 'Issue' or 'MergeRequest' or ['Issue', 'MergeRequest']
|
||||
#
|
||||
|
@ -23,7 +24,7 @@ class TodosFinder
|
|||
|
||||
NONE = '0'
|
||||
|
||||
TODO_TYPES = Set.new(%w(Issue MergeRequest DesignManagement::Design)).freeze
|
||||
TODO_TYPES = Set.new(%w(Issue MergeRequest DesignManagement::Design AlertManagement::Alert)).freeze
|
||||
|
||||
attr_accessor :current_user, :params
|
||||
|
||||
|
@ -47,6 +48,7 @@ class TodosFinder
|
|||
items = by_action(items)
|
||||
items = by_author(items)
|
||||
items = by_state(items)
|
||||
items = by_target_id(items)
|
||||
items = by_types(items)
|
||||
items = by_group(items)
|
||||
# Filtering by project HAS TO be the last because we use
|
||||
|
@ -198,6 +200,12 @@ class TodosFinder
|
|||
items.with_states(params[:state])
|
||||
end
|
||||
|
||||
def by_target_id(items)
|
||||
return items if params[:target_id].blank?
|
||||
|
||||
items.for_target(params[:target_id])
|
||||
end
|
||||
|
||||
def by_types(items)
|
||||
if types.any?
|
||||
items.for_type(types)
|
||||
|
|
|
@ -4,7 +4,7 @@ module Resolvers
|
|||
class TodoResolver < BaseResolver
|
||||
type Types::TodoType, null: true
|
||||
|
||||
alias_method :user, :object
|
||||
alias_method :target, :object
|
||||
|
||||
argument :action, [Types::TodoActionEnum],
|
||||
required: false,
|
||||
|
@ -31,9 +31,10 @@ module Resolvers
|
|||
description: 'The type of the todo'
|
||||
|
||||
def resolve(**args)
|
||||
return Todo.none if user != context[:current_user]
|
||||
return Todo.none unless current_user.present? && target.present?
|
||||
return Todo.none if target.is_a?(User) && target != current_user
|
||||
|
||||
TodosFinder.new(user, todo_finder_params(args)).execute
|
||||
TodosFinder.new(current_user, todo_finder_params(args)).execute
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -46,6 +47,15 @@ module Resolvers
|
|||
author_id: args[:author_id],
|
||||
action_id: args[:action],
|
||||
project_id: args[:project_id]
|
||||
}.merge(target_params)
|
||||
end
|
||||
|
||||
def target_params
|
||||
return {} unless TodosFinder::TODO_TYPES.include?(target.class.name)
|
||||
|
||||
{
|
||||
type: target.class.name,
|
||||
target_id: target.id
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -97,6 +97,12 @@ module Types
|
|||
description: 'URL for metrics embed for the alert',
|
||||
resolve: -> (alert, _args, _context) { alert.present.metrics_dashboard_url }
|
||||
|
||||
field :todos,
|
||||
Types::TodoType.connection_type,
|
||||
null: true,
|
||||
description: 'Todos of the current user for the alert',
|
||||
resolver: Resolvers::TodoResolver
|
||||
|
||||
def notes
|
||||
object.ordered_notes
|
||||
end
|
||||
|
|
|
@ -45,7 +45,7 @@ module IconsHelper
|
|||
ActionController::Base.helpers.image_path('file_icons.svg', host: sprite_base_url)
|
||||
end
|
||||
|
||||
def sprite_icon(icon_name, size: nil, css_class: nil)
|
||||
def sprite_icon(icon_name, size: DEFAULT_ICON_SIZE, css_class: nil)
|
||||
if known_sprites&.exclude?(icon_name)
|
||||
exception = ArgumentError.new("#{icon_name} is not a known icon in @gitlab-org/gitlab-svg")
|
||||
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(exception)
|
||||
|
@ -117,7 +117,9 @@ module IconsHelper
|
|||
'earth'
|
||||
end
|
||||
|
||||
sprite_icon(name, size: DEFAULT_ICON_SIZE, css_class: 'gl-vertical-align-text-bottom')
|
||||
css_class = options.delete(:class)
|
||||
|
||||
sprite_icon(name, size: DEFAULT_ICON_SIZE, css_class: css_class)
|
||||
end
|
||||
|
||||
def file_type_icon_class(type, mode, name)
|
||||
|
|
|
@ -163,7 +163,8 @@ module TodosHelper
|
|||
{ id: '', text: 'Any Type' },
|
||||
{ id: 'Issue', text: 'Issue' },
|
||||
{ id: 'MergeRequest', text: 'Merge Request' },
|
||||
{ id: 'DesignManagement::Design', text: 'Design' }
|
||||
{ id: 'DesignManagement::Design', text: 'Design' },
|
||||
{ id: 'AlertManagement::Alert', text: 'Alert' }
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
%h1.home-panel-title.gl-mt-3.gl-mb-2{ data: { qa_selector: 'project_name_content' } }
|
||||
= @project.name
|
||||
%span.visibility-icon.text-secondary.gl-ml-2.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
|
||||
= visibility_level_icon(@project.visibility_level, options: {class: 'icon'})
|
||||
= visibility_level_icon(@project.visibility_level, options: { class: 'icon' })
|
||||
= render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: @project
|
||||
.home-panel-metadata.d-flex.flex-wrap.text-secondary
|
||||
- if can?(current_user, :read_project, @project)
|
||||
|
|
5
changelogs/unreleased/graphql-get-alert-todo.yml
Normal file
5
changelogs/unreleased/graphql-get-alert-todo.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Support getting a todo for an alert in GraphQL API
|
||||
merge_request: 34789
|
||||
author:
|
||||
type: added
|
|
@ -548,7 +548,7 @@ or more LDAP group links](#adding-group-links-starter-only).
|
|||
|
||||
### Adding group links **(STARTER ONLY)**
|
||||
|
||||
For information on adding group links via CNs and filters, refer to [the GitLab groups documentation](../../../user/group/index.md#manage-group-memberships-via-ldap).
|
||||
For information on adding group links via CNs and filters, refer to [the GitLab groups documentation](../../../user/group/index.md#manage-group-memberships-via-ldap-starter-only).
|
||||
|
||||
### Administrator sync **(STARTER ONLY)**
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ stage: Enablement
|
|||
group: Distribution
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# Reference architectures
|
||||
|
||||
You can set up GitLab on a single server or scale it up to serve many users.
|
||||
|
@ -20,10 +21,8 @@ you scale GitLab accordingly.
|
|||
Testing on these reference architectures were performed with
|
||||
[GitLab's Performance Tool](https://gitlab.com/gitlab-org/quality/performance)
|
||||
at specific coded workloads, and the throughputs used for testing were
|
||||
calculated based on sample customer data. After selecting the reference
|
||||
architecture that matches your scale, refer to
|
||||
[Configure GitLab to Scale](#configure-gitlab-to-scale) to see the components
|
||||
involved, and how to configure them.
|
||||
calculated based on sample customer data. Select the
|
||||
[reference architecture](#available-reference-architectures) that matches your scale.
|
||||
|
||||
Each endpoint type is tested with the following number of requests per second (RPS)
|
||||
per 1,000 users:
|
||||
|
@ -152,42 +151,7 @@ is recommended.
|
|||
instance to other geographical locations as a read-only fully operational instance
|
||||
that can also be promoted in case of disaster.
|
||||
|
||||
## Configure GitLab to scale
|
||||
|
||||
NOTE: **Note:**
|
||||
From GitLab 13.0, using NFS for Git repositories is deprecated. In GitLab 14.0,
|
||||
support for NFS for Git repositories is scheduled to be removed. Upgrade to
|
||||
[Gitaly Cluster](../gitaly/praefect.md) as soon as possible.
|
||||
|
||||
The following components are the ones you need to configure in order to scale
|
||||
GitLab. They are listed in the order you'll typically configure them if they are
|
||||
required by your [reference architecture](#reference-architectures) of choice.
|
||||
|
||||
Most of them are bundled in the GitLab deb/rpm package (called Omnibus GitLab),
|
||||
but depending on your system architecture, you may require some components which are
|
||||
not included in it. If required, those should be configured before
|
||||
setting up components provided by GitLab. Advice on how to select the right
|
||||
solution for your organization is provided in the configuration instructions
|
||||
column.
|
||||
|
||||
| Component | Description | Configuration instructions | Bundled with Omnibus GitLab |
|
||||
|-----------|-------------|----------------------------|
|
||||
| Load balancer(s) ([6](#footnotes)) | Handles load balancing, typically when you have multiple GitLab application services nodes | [Load balancer configuration](../high_availability/load_balancer.md) ([6](#footnotes)) | No |
|
||||
| Object storage service ([4](#footnotes)) | Recommended store for shared data objects | [Object Storage configuration](../object_storage.md) | No |
|
||||
| NFS ([5](#footnotes)) ([7](#footnotes)) | Shared disk storage service. Can be used as an alternative Object Storage. Required for GitLab Pages | [NFS configuration](../high_availability/nfs.md) | No |
|
||||
| [Consul](../../development/architecture.md#consul) ([3](#footnotes)) | Service discovery and health checks/failover | [Consul configuration](../high_availability/consul.md) **(PREMIUM ONLY)** | Yes |
|
||||
| [PostgreSQL](../../development/architecture.md#postgresql) | Database | [PostgreSQL configuration](https://docs.gitlab.com/omnibus/settings/database.html) | Yes |
|
||||
| [PgBouncer](../../development/architecture.md#pgbouncer) | Database connection pooler | [PgBouncer configuration](../postgresql/pgbouncer.md) **(PREMIUM ONLY)** | Yes |
|
||||
| Repmgr | PostgreSQL cluster management and failover | [PostgreSQL and Repmgr configuration](../postgresql/replication_and_failover.md) | Yes |
|
||||
| Patroni | An alternative PostgreSQL cluster management and failover | [PostgreSQL and Patroni configuration](../postgresql/replication_and_failover.md#patroni) | Yes |
|
||||
| [Redis](../../development/architecture.md#redis) ([3](#footnotes)) | Key/value store for fast data lookup and caching | [Redis configuration](../high_availability/redis.md) | Yes |
|
||||
| Redis Sentinel | Redis | [Redis Sentinel configuration](../high_availability/redis.md) | Yes |
|
||||
| [Gitaly](../../development/architecture.md#gitaly) ([2](#footnotes)) ([7](#footnotes)) | Provides access to Git repositories | [Gitaly configuration](../gitaly/index.md#run-gitaly-on-its-own-server) | Yes |
|
||||
| [Sidekiq](../../development/architecture.md#sidekiq) | Asynchronous/background jobs | [Sidekiq configuration](../high_availability/sidekiq.md) | Yes |
|
||||
| [GitLab application services](../../development/architecture.md#unicorn)([1](#footnotes)) | Puma/Unicorn, Workhorse, GitLab Shell - serves front-end requests (UI, API, Git over HTTP/SSH) | [GitLab app scaling configuration](../high_availability/gitlab.md) | Yes |
|
||||
| [Prometheus](../../development/architecture.md#prometheus) and [Grafana](../../development/architecture.md#grafana) | GitLab environment monitoring | [Monitoring node for scaling](../high_availability/monitoring_node.md) | Yes |
|
||||
|
||||
### Configuring select components with Cloud Native Helm
|
||||
## Configuring select components with Cloud Native Helm
|
||||
|
||||
We also provide [Helm charts](https://docs.gitlab.com/charts/) as a Cloud Native installation
|
||||
method for GitLab. For the reference architectures, select components can be set up in this
|
||||
|
@ -205,44 +169,3 @@ specs, only translated into Kubernetes resources.
|
|||
For example, if you were to set up a 50k installation with the Rails nodes being run in Helm,
|
||||
then the same amount of resources as given for Omnibus should be given to the Kubernetes
|
||||
cluster with the Rails nodes broken down into a number of smaller Pods across that cluster.
|
||||
|
||||
## Footnotes
|
||||
|
||||
1. In our architectures we run each GitLab Rails node using the Puma webserver
|
||||
and have its number of workers set to 90% of available CPUs along with four threads. For
|
||||
nodes that are running Rails with other components the worker value should be reduced
|
||||
accordingly where we've found 50% achieves a good balance but this is dependent
|
||||
on workload.
|
||||
|
||||
1. Gitaly node requirements are dependent on customer data, specifically the number of
|
||||
projects and their sizes. We recommend that each Gitaly node should store no more than 5TB of data
|
||||
and have the number of [`gitaly-ruby` workers](../gitaly/index.md#gitaly-ruby)
|
||||
set to 20% of available CPUs. Additional nodes should be considered in conjunction
|
||||
with a review of expected data size and spread based on the recommendations above.
|
||||
|
||||
1. Recommended Redis setup differs depending on the size of the architecture.
|
||||
For smaller architectures (less than 3,000 users) a single instance should suffice.
|
||||
For medium sized installs (3,000 - 5,000) we suggest one Redis cluster for all
|
||||
classes and that Redis Sentinel is hosted alongside Consul.
|
||||
For larger architectures (10,000 users or more) we suggest running a separate
|
||||
[Redis Cluster](../redis/replication_and_failover.md#running-multiple-redis-clusters) for the Cache class
|
||||
and another for the Queues and Shared State classes respectively. We also recommend
|
||||
that you run the Redis Sentinel clusters separately for each Redis Cluster.
|
||||
|
||||
1. For data objects such as LFS, Uploads, Artifacts, etc. We recommend an [Object Storage service](../object_storage.md)
|
||||
over NFS where possible, due to better performance.
|
||||
|
||||
1. NFS can be used as an alternative for object storage but this isn't typically
|
||||
recommended for performance reasons. Note however it is required for [GitLab
|
||||
Pages](https://gitlab.com/gitlab-org/gitlab-pages/-/issues/196).
|
||||
|
||||
1. Our architectures have been tested and validated with [HAProxy](https://www.haproxy.org/)
|
||||
as the load balancer. Although other load balancers with similar feature sets
|
||||
could also be used, those load balancers have not been validated.
|
||||
|
||||
1. We strongly recommend that any Gitaly or NFS nodes be set up with SSD disks over
|
||||
HDD with a throughput of at least 8,000 IOPS for read operations and 2,000 IOPS for write
|
||||
as these components have heavy I/O. These IOPS values are recommended only as a starter
|
||||
as with time they may be adjusted higher or lower depending on the scale of your
|
||||
environment's workload. If you're running the environment on a Cloud provider
|
||||
you may need to refer to their documentation on how configure IOPS correctly.
|
||||
|
|
|
@ -319,6 +319,61 @@ type AlertManagementAlert implements Noteable {
|
|||
"""
|
||||
title: String
|
||||
|
||||
"""
|
||||
Todos of the current user for the alert
|
||||
"""
|
||||
todos(
|
||||
"""
|
||||
The action to be filtered
|
||||
"""
|
||||
action: [TodoActionEnum!]
|
||||
|
||||
"""
|
||||
Returns the elements in the list that come after the specified cursor.
|
||||
"""
|
||||
after: String
|
||||
|
||||
"""
|
||||
The ID of an author
|
||||
"""
|
||||
authorId: [ID!]
|
||||
|
||||
"""
|
||||
Returns the elements in the list that come before the specified cursor.
|
||||
"""
|
||||
before: String
|
||||
|
||||
"""
|
||||
Returns the first _n_ elements from the list.
|
||||
"""
|
||||
first: Int
|
||||
|
||||
"""
|
||||
The ID of a group
|
||||
"""
|
||||
groupId: [ID!]
|
||||
|
||||
"""
|
||||
Returns the last _n_ elements from the list.
|
||||
"""
|
||||
last: Int
|
||||
|
||||
"""
|
||||
The ID of a project
|
||||
"""
|
||||
projectId: [ID!]
|
||||
|
||||
"""
|
||||
The state of the todo
|
||||
"""
|
||||
state: [TodoStateEnum!]
|
||||
|
||||
"""
|
||||
The type of the todo
|
||||
"""
|
||||
type: [TodoTargetEnum!]
|
||||
): TodoConnection
|
||||
|
||||
"""
|
||||
Timestamp the alert was last updated
|
||||
"""
|
||||
|
|
|
@ -871,6 +871,167 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "todos",
|
||||
"description": "Todos of the current user for the alert",
|
||||
"args": [
|
||||
{
|
||||
"name": "action",
|
||||
"description": "The action to be filtered",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "TodoActionEnum",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "authorId",
|
||||
"description": "The ID of an author",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "projectId",
|
||||
"description": "The ID of a project",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "groupId",
|
||||
"description": "The ID of a group",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "state",
|
||||
"description": "The state of the todo",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "TodoStateEnum",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"description": "The type of the todo",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "TodoTargetEnum",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "after",
|
||||
"description": "Returns the elements in the list that come after the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "before",
|
||||
"description": "Returns the elements in the list that come before the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "first",
|
||||
"description": "Returns the first _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "last",
|
||||
"description": "Returns the last _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "TodoConnection",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "updatedAt",
|
||||
"description": "Timestamp the alert was last updated",
|
||||
|
|
|
@ -36,6 +36,105 @@ since point releases bundle many changes together. Minimizing the time
|
|||
between when versions are out of sync across the fleet may help mitigate
|
||||
errors caused by upgrades.
|
||||
|
||||
## Requirements for zero downtime upgrades
|
||||
|
||||
One way to guarantee zero downtime upgrades for on-premise instances is following the
|
||||
[expand and contract pattern](https://martinfowler.com/bliki/ParallelChange.html).
|
||||
|
||||
This means that every breaking change is broken down in three phases: expand, migrate, and contract.
|
||||
|
||||
1. **expand**: a breaking change is introduced keeping the software backward-compatible.
|
||||
1. **migrate**: all consumers are updated to make use of the new implementation.
|
||||
1. **contract**: backward compatibility is removed.
|
||||
|
||||
Those three phases **must be part of different milestones**, to allow zero downtime upgrades.
|
||||
|
||||
Depending on the support level for the feature, the contract phase could be delayed until the next major release.
|
||||
|
||||
## Expand and contract examples
|
||||
|
||||
Route changes, changing Sidekiq worker parameters, and database migrations are all perfect examples of a breaking change.
|
||||
Let's see how we can handle them safely.
|
||||
|
||||
### Route changes
|
||||
|
||||
When changing routing we should pay attention to make sure a route generated from the new version can be served by the old one and vice versa.
|
||||
As you can see in [an example later on this page](#some-links-to-issues-and-mrs-were-broken), not doing it can lead to an outage.
|
||||
This type of change may look like an immediate switch between the two implementations. However,
|
||||
especially with the canary stage, there is an extended period of time where both version of the code
|
||||
coexists in production.
|
||||
|
||||
1. **expand**: a new route is added, pointing to the same controller as the old one. But nothing in the application will generate links for the new routes.
|
||||
1. **migrate**: now that every machine in the fleet can understand the new route, we can generate links with the new routing.
|
||||
1. **contract**: the old route can be safely removed. (If the old route was likely to be widely shared, like the link to a repository file, we might want to add redirects and keep the old route for a longer period.)
|
||||
|
||||
### Changing Sidekiq worker's parameters
|
||||
|
||||
This topic is explained in detail in [Sidekiq Compatibility across Updates](sidekiq_style_guide.md#sidekiq-compatibility-across-updates).
|
||||
|
||||
When we need to add a new parameter to a Sidekiq worker class, we can split this into the following steps:
|
||||
|
||||
1. **expand**: the worker class adds a new parameter with a default value.
|
||||
1. **migrate**: we add the new parameter to all the invocations of the worker.
|
||||
1. **contract**: we remove the default value.
|
||||
|
||||
At a first look, it may seem safe to bundle expand and migrate into a single milestone, but this will cause an outage if Puma restarts before Sidekiq.
|
||||
Puma enqueues jobs with an extra parameter that the old Sidekiq cannot handle.
|
||||
|
||||
### Database migrations
|
||||
|
||||
The following graph is a simplified visual representation of a deployment, this will guide us in understanding how expand and contract is implemented in our migrations strategy.
|
||||
|
||||
There's a special consideration here. Using our post-deployment migrations framework allows us to bundle all three phases into one milestone.
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title Deployment
|
||||
dateFormat HH:mm
|
||||
|
||||
section Deploy box
|
||||
Run migrations :done, migr, after schemaA, 2m
|
||||
Run post-deployment migrations :postmigr, after mcvn , 2m
|
||||
|
||||
section Database
|
||||
Schema A :done, schemaA, 00:00 , 1h
|
||||
Schema B :crit, schemaB, after migr, 58m
|
||||
Schema C. : schmeaC, after postmigr, 1h
|
||||
|
||||
section Machine A
|
||||
Version N :done, mavn, 00:00 , 75m
|
||||
Version N+1 : after mavn, 105m
|
||||
|
||||
section Machine B
|
||||
Version N :done, mbvn, 00:00 , 105m
|
||||
Version N+1 : mbdone, after mbvn, 75m
|
||||
|
||||
section Machine C
|
||||
Version N :done, mcvn, 00:00 , 2h
|
||||
Version N+1 : mbcdone, after mcvn, 1h
|
||||
```
|
||||
|
||||
If we look at this schema from a database point of view, we can see two deployments feed into a single GitLab deployment:
|
||||
|
||||
1. from `Schema A` to `Schema B`
|
||||
1. from `Schema B` to `Schema C`
|
||||
|
||||
And these deployments align perfectly with application changes.
|
||||
|
||||
1. At the beginning we have `Version N` on `Schema A`.
|
||||
1. Then we have a _long_ transition periond with both `Version N` and `Version N+1` on `Schema B`.
|
||||
1. When we only have `Version N+1` on `Schema B` the schema changes again.
|
||||
1. Finally we have `Version N+1` on `Schema C`.
|
||||
|
||||
With all those details in mind, let's imagine we need to replace a query, and this query has an index to support it.
|
||||
|
||||
1. **expand**: this is the from `Schema A` to `Schema B` deployment. We add the new index, but the application will ignore it for now
|
||||
1. **migrate**: this is the `Version N` to `Version N+1` application deployment. The new code is deployed, at this point in time only the new query will run.
|
||||
1. **contract**: from `Schema B` to `Schema C` (post-deployment migration). Nothing uses the old index anymore, we can safely remove it.
|
||||
|
||||
This is only an example. More complex migrations, especially when background migrations are needed will
|
||||
still require more than one milestone. For details please refer to our [migration style guide](migration_style_guide.md).
|
||||
|
||||
## Examples of previous incidents
|
||||
|
||||
### Some links to issues and MRs were broken
|
||||
|
|
|
@ -334,7 +334,7 @@ To share a given group, for example, 'Frontend' with another group, for example,
|
|||
|
||||
All the members of the 'Engineering' group will have been added to 'Frontend'.
|
||||
|
||||
## Manage group memberships via LDAP
|
||||
## Manage group memberships via LDAP **(STARTER ONLY)**
|
||||
|
||||
Group syncing allows LDAP groups to be mapped to GitLab groups. This provides more control over per-group user management. To configure group syncing edit the `group_base` **DN** (`'OU=Global Groups,OU=GitLab INT,DC=GitLab,DC=org'`). This **OU** contains all groups that will be associated with GitLab groups.
|
||||
|
||||
|
|
|
@ -268,7 +268,7 @@ Group SAML on a self-managed instance is limited when compared to the recommende
|
|||
[instance-wide SAML](../../../integration/saml.md). The recommended solution allows you to take advantage of:
|
||||
|
||||
- [LDAP compatibility](../../../administration/auth/ldap/index.md).
|
||||
- [LDAP Group Sync](../index.md#manage-group-memberships-via-ldap).
|
||||
- [LDAP Group Sync](../index.md#manage-group-memberships-via-ldap-starter-only).
|
||||
- [Required groups](../../../integration/saml.md#required-groups-starter-only).
|
||||
- [Admin groups](../../../integration/saml.md#admin-groups-starter-only).
|
||||
- [Auditor groups](../../../integration/saml.md#auditor-groups-starter-only).
|
||||
|
|
|
@ -474,7 +474,7 @@ for details about the pipelines security model.
|
|||
## LDAP users permissions
|
||||
|
||||
Since GitLab 8.15, LDAP user permissions can now be manually overridden by an admin user.
|
||||
Read through the documentation on [LDAP users permissions](group/index.md#manage-group-memberships-via-ldap) to learn more.
|
||||
Read through the documentation on [LDAP users permissions](group/index.md#manage-group-memberships-via-ldap-starter-only) to learn more.
|
||||
|
||||
## Project aliases
|
||||
|
||||
|
|
|
@ -7507,6 +7507,12 @@ msgstr ""
|
|||
msgid "DastProfiles|Do you want to discard this site profile?"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|Edit feature will come soon. Please create a new profile if changes needed"
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|Error fetching the profiles list. Please check your network connection and try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "DastProfiles|Manage Profiles"
|
||||
msgstr ""
|
||||
|
||||
|
@ -14222,6 +14228,9 @@ msgstr ""
|
|||
msgid "Live preview"
|
||||
msgstr ""
|
||||
|
||||
msgid "Load more"
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading"
|
||||
msgstr ""
|
||||
|
||||
|
@ -24161,6 +24170,9 @@ msgstr ""
|
|||
msgid "There was a problem fetching groups."
|
||||
msgstr ""
|
||||
|
||||
msgid "There was a problem fetching labels."
|
||||
msgstr ""
|
||||
|
||||
msgid "There was a problem fetching project branches."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
"@babel/preset-env": "^7.10.1",
|
||||
"@gitlab/at.js": "1.5.5",
|
||||
"@gitlab/svgs": "1.156.0",
|
||||
"@gitlab/ui": "17.40.0",
|
||||
"@gitlab/ui": "17.42.0",
|
||||
"@gitlab/visual-review-tools": "1.6.1",
|
||||
"@rails/actioncable": "^6.0.3-1",
|
||||
"@sentry/browser": "^5.10.2",
|
||||
|
|
|
@ -232,6 +232,26 @@ RSpec.describe TodosFinder do
|
|||
expect(todos).to match_array([todo2, todo1])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by target id' do
|
||||
it 'returns the expected todos for the target' do
|
||||
todos = finder.new(user, { target_id: issue.id }).execute
|
||||
|
||||
expect(todos).to match_array([todo1])
|
||||
end
|
||||
|
||||
it 'returns the expected todos for multiple target ids' do
|
||||
todos = finder.new(user, { target_id: [issue.id, merge_request.id] }).execute
|
||||
|
||||
expect(todos).to match_array([todo1, todo2])
|
||||
end
|
||||
|
||||
it 'returns the expected todos for empty target id collection' do
|
||||
todos = finder.new(user, { target_id: [] }).execute
|
||||
|
||||
expect(todos).to match_array([todo1, todo2])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'external authorization' do
|
||||
|
@ -307,9 +327,9 @@ RSpec.describe TodosFinder do
|
|||
it 'returns the expected types' do
|
||||
expected_result =
|
||||
if Gitlab.ee?
|
||||
%w[Epic Issue MergeRequest DesignManagement::Design]
|
||||
%w[Epic Issue MergeRequest DesignManagement::Design AlertManagement::Alert]
|
||||
else
|
||||
%w[Issue MergeRequest DesignManagement::Design]
|
||||
%w[Issue MergeRequest DesignManagement::Design AlertManagement::Alert]
|
||||
end
|
||||
|
||||
expect(described_class.todo_types).to contain_exactly(*expected_result)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import {
|
||||
GlFilteredSearch,
|
||||
GlButtonGroup,
|
||||
|
@ -16,13 +16,16 @@ import RecentSearchesService from '~/filtered_search/services/recent_searches_se
|
|||
import { mockAvailableTokens, mockSortOptions, mockHistoryItems } from './mock_data';
|
||||
|
||||
const createComponent = ({
|
||||
shallow = true,
|
||||
namespace = 'gitlab-org/gitlab-test',
|
||||
recentSearchesStorageKey = 'requirements',
|
||||
tokens = mockAvailableTokens,
|
||||
sortOptions = mockSortOptions,
|
||||
searchInputPlaceholder = 'Filter requirements',
|
||||
} = {}) =>
|
||||
shallowMount(FilteredSearchBarRoot, {
|
||||
} = {}) => {
|
||||
const mountMethod = shallow ? shallowMount : mount;
|
||||
|
||||
return mountMethod(FilteredSearchBarRoot, {
|
||||
propsData: {
|
||||
namespace,
|
||||
recentSearchesStorageKey,
|
||||
|
@ -31,6 +34,7 @@ const createComponent = ({
|
|||
searchInputPlaceholder,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('FilteredSearchBarRoot', () => {
|
||||
let wrapper;
|
||||
|
@ -54,13 +58,13 @@ describe('FilteredSearchBarRoot', () => {
|
|||
describe('computed', () => {
|
||||
describe('tokenSymbols', () => {
|
||||
it('returns a map containing type and symbols from `tokens` prop', () => {
|
||||
expect(wrapper.vm.tokenSymbols).toEqual({ author_username: '@' });
|
||||
expect(wrapper.vm.tokenSymbols).toEqual({ author_username: '@', label_name: '~' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('tokenTitles', () => {
|
||||
it('returns a map containing type and title from `tokens` prop', () => {
|
||||
expect(wrapper.vm.tokenTitles).toEqual({ author_username: 'Author' });
|
||||
expect(wrapper.vm.tokenTitles).toEqual({ author_username: 'Author', label_name: 'Label' });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -233,6 +237,14 @@ describe('FilteredSearchBarRoot', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('calls `blurSearchInput` method to remove focus from filter input field', () => {
|
||||
jest.spyOn(wrapper.vm, 'blurSearchInput');
|
||||
|
||||
wrapper.find(GlFilteredSearch).vm.$emit('submit', mockFilters);
|
||||
|
||||
expect(wrapper.vm.blurSearchInput).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('emits component event `onFilter` with provided filters param', () => {
|
||||
wrapper.vm.handleFilterSubmit(mockFilters);
|
||||
|
||||
|
@ -260,13 +272,28 @@ describe('FilteredSearchBarRoot', () => {
|
|||
expect(glFilteredSearchEl.props('historyItems')).toEqual(mockHistoryItems);
|
||||
});
|
||||
|
||||
it('renders search history items dropdown with formatting done using token symbols', async () => {
|
||||
const wrapperFullMount = createComponent({ shallow: false });
|
||||
wrapperFullMount.vm.recentSearchesStore.addRecentSearch(mockHistoryItems[0]);
|
||||
|
||||
await wrapperFullMount.vm.$nextTick();
|
||||
|
||||
const searchHistoryItemsEl = wrapperFullMount.findAll(
|
||||
'.gl-search-box-by-click-menu .gl-search-box-by-click-history-item',
|
||||
);
|
||||
|
||||
expect(searchHistoryItemsEl.at(0).text()).toBe('Author := @tobyLabel := ~Bug"duo"');
|
||||
|
||||
wrapperFullMount.destroy();
|
||||
});
|
||||
|
||||
it('renders sort dropdown component', () => {
|
||||
expect(wrapper.find(GlButtonGroup).exists()).toBe(true);
|
||||
expect(wrapper.find(GlDropdown).exists()).toBe(true);
|
||||
expect(wrapper.find(GlDropdown).props('text')).toBe(mockSortOptions[0].title);
|
||||
});
|
||||
|
||||
it('renders dropdown items', () => {
|
||||
it('renders sort dropdown items', () => {
|
||||
const dropdownItemsEl = wrapper.findAll(GlDropdownItem);
|
||||
|
||||
expect(dropdownItemsEl).toHaveLength(mockSortOptions.length);
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import Api from '~/api';
|
||||
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
|
||||
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
|
||||
|
||||
import { mockLabels } from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
|
||||
|
||||
export const mockAuthor1 = {
|
||||
id: 1,
|
||||
|
@ -42,7 +45,18 @@ export const mockAuthorToken = {
|
|||
fetchAuthors: Api.projectUsers.bind(Api),
|
||||
};
|
||||
|
||||
export const mockAvailableTokens = [mockAuthorToken];
|
||||
export const mockLabelToken = {
|
||||
type: 'label_name',
|
||||
icon: 'labels',
|
||||
title: 'Label',
|
||||
unique: false,
|
||||
symbol: '~',
|
||||
token: LabelToken,
|
||||
operators: [{ value: '=', description: 'is', default: 'true' }],
|
||||
fetchLabels: () => Promise.resolve(mockLabels),
|
||||
};
|
||||
|
||||
export const mockAvailableTokens = [mockAuthorToken, mockLabelToken];
|
||||
|
||||
export const mockHistoryItems = [
|
||||
[
|
||||
|
@ -53,6 +67,13 @@ export const mockHistoryItems = [
|
|||
operator: '=',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'label_name',
|
||||
value: {
|
||||
data: 'Bug',
|
||||
operator: '=',
|
||||
},
|
||||
},
|
||||
'duo',
|
||||
],
|
||||
[
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
import createFlash from '~/flash';
|
||||
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
|
||||
|
||||
import {
|
||||
mockRegularLabel,
|
||||
mockLabels,
|
||||
} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
|
||||
|
||||
import { mockLabelToken } from '../mock_data';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
const createComponent = ({ config = mockLabelToken, value = { data: '' }, active = false } = {}) =>
|
||||
mount(LabelToken, {
|
||||
propsData: {
|
||||
config,
|
||||
value,
|
||||
active,
|
||||
},
|
||||
provide: {
|
||||
portalName: 'fake target',
|
||||
alignSuggestions: function fakeAlignSuggestions() {},
|
||||
},
|
||||
stubs: {
|
||||
Portal: {
|
||||
template: '<div><slot></slot></div>',
|
||||
},
|
||||
GlFilteredSearchSuggestionList: {
|
||||
template: '<div></div>',
|
||||
methods: {
|
||||
getValue: () => '=',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('LabelToken', () => {
|
||||
let mock;
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('computed', () => {
|
||||
beforeEach(async () => {
|
||||
// Label title with spaces is always enclosed in quotations by component.
|
||||
wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } });
|
||||
|
||||
wrapper.setData({
|
||||
labels: mockLabels,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
describe('currentValue', () => {
|
||||
it('returns lowercase string for `value.data`', () => {
|
||||
expect(wrapper.vm.currentValue).toBe('"foo label"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('activeLabel', () => {
|
||||
it('returns object for currently present `value.data`', () => {
|
||||
expect(wrapper.vm.activeLabel).toEqual(mockRegularLabel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('containerStyle', () => {
|
||||
it('returns object containing `backgroundColor` and `color` properties based on `activeLabel` value', () => {
|
||||
expect(wrapper.vm.containerStyle).toEqual({
|
||||
backgroundColor: mockRegularLabel.color,
|
||||
color: mockRegularLabel.textColor,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty object when `activeLabel` is not set', async () => {
|
||||
wrapper.setData({
|
||||
labels: [],
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.containerStyle).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('fetchLabelBySearchTerm', () => {
|
||||
it('calls `config.fetchLabels` with provided searchTerm param', () => {
|
||||
jest.spyOn(wrapper.vm.config, 'fetchLabels');
|
||||
|
||||
wrapper.vm.fetchLabelBySearchTerm('foo');
|
||||
|
||||
expect(wrapper.vm.config.fetchLabels).toHaveBeenCalledWith('foo');
|
||||
});
|
||||
|
||||
it('sets response to `labels` when request is succesful', () => {
|
||||
jest.spyOn(wrapper.vm.config, 'fetchLabels').mockResolvedValue(mockLabels);
|
||||
|
||||
wrapper.vm.fetchLabelBySearchTerm('foo');
|
||||
|
||||
return waitForPromises().then(() => {
|
||||
expect(wrapper.vm.labels).toEqual(mockLabels);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls `createFlash` with flash error message when request fails', () => {
|
||||
jest.spyOn(wrapper.vm.config, 'fetchLabels').mockRejectedValue({});
|
||||
|
||||
wrapper.vm.fetchLabelBySearchTerm('foo');
|
||||
|
||||
return waitForPromises().then(() => {
|
||||
expect(createFlash).toHaveBeenCalledWith('There was a problem fetching labels.');
|
||||
});
|
||||
});
|
||||
|
||||
it('sets `loading` to false when request completes', () => {
|
||||
jest.spyOn(wrapper.vm.config, 'fetchLabels').mockRejectedValue({});
|
||||
|
||||
wrapper.vm.fetchLabelBySearchTerm('foo');
|
||||
|
||||
return waitForPromises().then(() => {
|
||||
expect(wrapper.vm.loading).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } });
|
||||
|
||||
wrapper.setData({
|
||||
labels: mockLabels,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('renders gl-filtered-search-token component', () => {
|
||||
expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders token item when value is selected', () => {
|
||||
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
|
||||
|
||||
expect(tokenSegments).toHaveLength(3); // Label, =, "Foo Label"
|
||||
expect(tokenSegments.at(2).text()).toBe(`~${mockRegularLabel.title}`); // "Foo Label"
|
||||
expect(
|
||||
tokenSegments
|
||||
.at(2)
|
||||
.find('.gl-token')
|
||||
.attributes('style'),
|
||||
).toBe('background-color: rgb(186, 218, 85); color: rgb(255, 255, 255);');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -99,7 +99,7 @@ RSpec.describe Resolvers::TodoResolver do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when no user is provided' do
|
||||
context 'when no target is provided' do
|
||||
it 'returns no todos' do
|
||||
todos = resolve(described_class, obj: nil, args: {}, ctx: { current_user: current_user })
|
||||
|
||||
|
@ -107,7 +107,7 @@ RSpec.describe Resolvers::TodoResolver do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when provided user is not current user' do
|
||||
context 'when target user is not the current user' do
|
||||
it 'returns no todos' do
|
||||
other_user = create(:user)
|
||||
|
||||
|
@ -116,6 +116,16 @@ RSpec.describe Resolvers::TodoResolver do
|
|||
expect(todos).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when request is for a todo target' do
|
||||
it 'returns only the todos for the target' do
|
||||
target = issue_todo_pending.target
|
||||
|
||||
todos = resolve(described_class, obj: target, args: {}, ctx: { current_user: current_user })
|
||||
|
||||
expect(todos).to contain_exactly(issue_todo_pending)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def resolve_todos(args = {}, context = { current_user: current_user })
|
||||
|
|
|
@ -28,6 +28,7 @@ RSpec.describe GitlabSchema.types['AlertManagementAlert'] do
|
|||
notes
|
||||
discussions
|
||||
metrics_dashboard_url
|
||||
todos
|
||||
]
|
||||
|
||||
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||
|
|
|
@ -48,8 +48,13 @@ RSpec.describe IconsHelper do
|
|||
describe 'sprite_icon' do
|
||||
icon_name = 'clock'
|
||||
|
||||
it 'returns svg icon html' do
|
||||
it 'returns svg icon html with DEFAULT_ICON_SIZE' do
|
||||
expect(sprite_icon(icon_name).to_s)
|
||||
.to eq "<svg class=\"s#{IconsHelper::DEFAULT_ICON_SIZE}\" data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
|
||||
end
|
||||
|
||||
it 'returns svg icon html without size class' do
|
||||
expect(sprite_icon(icon_name, size: nil).to_s)
|
||||
.to eq "<svg data-testid=\"#{icon_name}-icon\"><use xlink:href=\"#{icons_path}##{icon_name}\"></use></svg>"
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'getting Alert Management Alert Assignees' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:alert) { create(:alert_management_alert, project: project) }
|
||||
let_it_be(:other_alert) { create(:alert_management_alert, project: project) }
|
||||
let_it_be(:todo) { create(:todo, :pending, target: alert, user: current_user, project: project) }
|
||||
let_it_be(:other_todo) { create(:todo, :pending, target: other_alert, user: current_user, project: project) }
|
||||
|
||||
let(:fields) do
|
||||
<<~QUERY
|
||||
nodes {
|
||||
iid
|
||||
todos {
|
||||
nodes {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
QUERY
|
||||
end
|
||||
|
||||
let(:graphql_query) do
|
||||
graphql_query_for(
|
||||
'project',
|
||||
{ 'fullPath' => project.full_path },
|
||||
query_graphql_field('alertManagementAlerts', {}, fields)
|
||||
)
|
||||
end
|
||||
|
||||
let(:gql_alerts) { graphql_data.dig('project', 'alertManagementAlerts', 'nodes') }
|
||||
let(:gql_todos) { gql_alerts.map { |gql_alert| [gql_alert['iid'], gql_alert['todos']['nodes']] }.to_h }
|
||||
let(:gql_alert_todo) { gql_todos[alert.iid.to_s].first }
|
||||
let(:gql_other_alert_todo) { gql_todos[other_alert.iid.to_s].first }
|
||||
|
||||
before do
|
||||
project.add_developer(current_user)
|
||||
end
|
||||
|
||||
it 'includes the correct metrics dashboard url' do
|
||||
post_graphql(graphql_query, current_user: current_user)
|
||||
|
||||
expect(gql_alert_todo['id']).to eq(todo.to_global_id.to_s)
|
||||
expect(gql_other_alert_todo['id']).to eq(other_todo.to_global_id.to_s)
|
||||
end
|
||||
end
|
|
@ -848,10 +848,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.156.0.tgz#2af56246b5d71000ec81abb1281e811a921cdfd1"
|
||||
integrity sha512-+b670Sxkjo80Wb4GKMZQ+xvuwu9sVvql8aS9nzw63FLn84QyqXS+jMjvyDqPAW5kly6B1Eg4Kljq0YawJ0ySBg==
|
||||
|
||||
"@gitlab/ui@17.40.0":
|
||||
version "17.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.40.0.tgz#70c89a31d5e98382a9b30aaeac13caf924f38b6d"
|
||||
integrity sha512-0Jf1TwE572cZBgWubCkD9F7YE4Vok+r4Jbtx9ORBxmLaxq1XKpGf/TAd3iMftRQ+pr4T011/z0rJYLqde1mUgw==
|
||||
"@gitlab/ui@17.42.0":
|
||||
version "17.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.42.0.tgz#410fe3c1dc78e32620dcaf295091676fe95e1297"
|
||||
integrity sha512-LJEpmmPgdXvEkfXm0ePCF/DuRuAP26bI5+gp8SZ3tk2xk0jT7Y/O2sOrnYvPqWl4W5BekRj/x8xrzfR9n+bvTQ==
|
||||
dependencies:
|
||||
"@babel/standalone" "^7.0.0"
|
||||
"@gitlab/vue-toasted" "^1.3.0"
|
||||
|
|
Loading…
Reference in a new issue