Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-08-03 15:09:44 +00:00
parent a70e2c0418
commit 31979cb323
33 changed files with 854 additions and 119 deletions

View file

@ -1,5 +1,7 @@
export const ANY_AUTHOR = 'Any';
export const NO_LABEL = 'No label';
export const DEBOUNCE_DELAY = 200;
export const SortDirection = {

View file

@ -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"

View file

@ -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>

View file

@ -274,8 +274,6 @@
svg {
height: 15px;
width: 15px;
position: relative;
top: 2px;
}
svg,

View file

@ -48,4 +48,12 @@ svg {
@include svg-size(#{$svg-size}px);
}
}
&.s12 {
vertical-align: -1px;
}
&.s16 {
vertical-align: -3px;
}
}

View file

@ -159,7 +159,7 @@
font-weight: bold;
.icon {
font-size: $gl-font-size-large;
vertical-align: -1px;
}
.home-panel-topic-list {

View file

@ -138,12 +138,6 @@
}
.tree-item {
.file-icon,
.folder-icon {
position: relative;
top: 2px;
}
.link-container {
padding: 0;

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -0,0 +1,5 @@
---
title: Support getting a todo for an alert in GraphQL API
merge_request: 34789
author:
type: added

View file

@ -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)**

View file

@ -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.

View file

@ -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
"""

View file

@ -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",

View file

@ -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

View file

@ -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.

View file

@ -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).

View file

@ -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

View file

@ -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 ""

View file

@ -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",

View file

@ -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)

View file

@ -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);

View file

@ -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',
],
[

View file

@ -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);');
});
});
});

View file

@ -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 })

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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"