Commit a42f54a3 authored by Mike Greiling's avatar Mike Greiling

Merge branch 'ce-to-ee-2018-09-18' into 'master'

CE upstream - 2018-09-18 18:21 UTC

See merge request gitlab-org/gitlab-ee!7406
parents 8c373e82 dd4a78d9
...@@ -82,14 +82,31 @@ The plan for the upcoming milestone must be finalized by the 1st of the month, o ...@@ -82,14 +82,31 @@ The plan for the upcoming milestone must be finalized by the 1st of the month, o
## Feature freeze on the 7th for the release on the 22nd ## Feature freeze on the 7th for the release on the 22nd
After 7th at 23:59 (Pacific Time Zone) of each month, RC1 of the upcoming release (to be shipped on the 22nd) is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it. After 7th at 23:59 (Pacific Time Zone) of each month, RC1 of the upcoming
Merge requests may still be merged into master during this period, release (to be shipped on the 22nd) is created and deployed to GitLab.com and
but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch. the stable branch for this release is frozen, which means master is no longer
merged into it. Merge requests may still be merged into master during this
By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things. period, but they will go into the _next_ release, unless they are manually
cherry-picked into the stable branch.
Any release candidate that gets created after this date can become a final release,
hence the name release candidate. By freezing the stable branches 2 weeks prior to a release, we reduce the risk
of a last minute merge request potentially breaking things.
Any release candidate that gets created after this date can become a final
release, hence the name release candidate.
### Feature flags
Merge requests that make changes hidden behind a feature flag, or remove an
existing feature flag because a feature is deemed stable, may be merged (and
picked into the stable branches) up to the 19th of the month. Such merge
requests should have the ~"feature flag" label assigned, and don't require a
corresponding exception request to be created.
While rare, release managers may decide to reject picking a change into a stable
branch, even when feature flags are used. This might be necessary if the changes
are deemed problematic, too invasive, or there simply isn't enough time to
properly test how the changes behave on GitLab.com.
### Between the 1st and the 7th ### Between the 1st and the 7th
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
/* eslint-disable vue/require-default-prop */ /* eslint-disable vue/require-default-prop */
import { s__, sprintf } from '../../locale'; import { s__, sprintf } from '../../locale';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue'; import loadingButton from '../../vue_shared/components/loading_button.vue';
import { import {
APPLICATION_STATUS, APPLICATION_STATUS,
...@@ -13,6 +14,7 @@ ...@@ -13,6 +14,7 @@
export default { export default {
components: { components: {
loadingButton, loadingButton,
identicon,
}, },
props: { props: {
id: { id: {
...@@ -31,6 +33,16 @@ ...@@ -31,6 +33,16 @@
type: String, type: String,
required: false, required: false,
}, },
logoUrl: {
type: String,
required: false,
default: null,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
status: { status: {
type: String, type: String,
required: false, required: false,
...@@ -60,6 +72,18 @@ ...@@ -60,6 +72,18 @@
isKnownStatus() { isKnownStatus() {
return Object.values(APPLICATION_STATUS).includes(this.status); return Object.values(APPLICATION_STATUS).includes(this.status);
}, },
isInstalled() {
return (
this.status === APPLICATION_STATUS.INSTALLED || this.status === APPLICATION_STATUS.UPDATED
);
},
hasLogo() {
return !!this.logoUrl;
},
identiconId() {
// generate a deterministic integer id for the identicon background
return this.id.charCodeAt(0);
},
rowJsClass() { rowJsClass() {
return `js-cluster-application-row-${this.id}`; return `js-cluster-application-row-${this.id}`;
}, },
...@@ -128,37 +152,81 @@ ...@@ -128,37 +152,81 @@
<template> <template>
<div <div
:class="rowJsClass" :class="[
class="gl-responsive-table-row gl-responsive-table-row-col-span" rowJsClass,
isInstalled && 'cluster-application-installed',
disabled && 'cluster-application-disabled'
]"
class="cluster-application-row gl-responsive-table-row gl-responsive-table-row-col-span"
> >
<div <div
class="gl-responsive-table-row-layout" class="gl-responsive-table-row-layout"
role="row" role="row"
> >
<div
class="table-section append-right-8 section-align-top"
role="gridcell"
>
<img
v-if="hasLogo"
:src="logoUrl"
:alt="`${title} logo`"
class="cluster-application-logo avatar s40"
/>
<identicon
v-else
:entity-id="identiconId"
:entity-name="title"
size-class="s40"
/>
</div>
<div
class="table-section cluster-application-description section-wrap"
role="gridcell"
>
<strong>
<a <a
v-if="titleLink" v-if="titleLink"
:href="titleLink" :href="titleLink"
target="blank" target="blank"
rel="noopener noreferrer" rel="noopener noreferrer"
role="gridcell" class="js-cluster-application-title"
class="table-section section-15 section-align-top js-cluster-application-title"
> >
{{ title }} {{ title }}
</a> </a>
<span <span
v-else v-else
class="table-section section-15 section-align-top js-cluster-application-title" class="js-cluster-application-title"
> >
{{ title }} {{ title }}
</span> </span>
</strong>
<slot name="description"></slot>
<div <div
class="table-section section-wrap" v-if="hasError || isUnknownStatus"
role="gridcell" class="cluster-application-error text-danger prepend-top-10"
> >
<slot name="description"></slot> <p class="js-cluster-application-general-error-message append-bottom-0">
{{ generalErrorDescription }}
</p>
<ul v-if="statusReason || requestReason">
<li
v-if="statusReason"
class="js-cluster-application-status-error-message"
>
{{ statusReason }}
</li>
<li
v-if="requestReason"
class="js-cluster-application-request-error-message"
>
{{ requestReason }}
</li>
</ul>
</div>
</div> </div>
<div <div
:class="{ 'section-20': showManageButton, 'section-15': !showManageButton }" :class="{ 'section-25': showManageButton, 'section-15': !showManageButton }"
class="table-section table-button-footer section-align-top" class="table-section table-button-footer section-align-top"
role="gridcell" role="gridcell"
> >
...@@ -168,6 +236,7 @@ ...@@ -168,6 +236,7 @@
> >
<a <a
:href="manageLink" :href="manageLink"
:class="{ disabled: disabled }"
class="btn" class="btn"
> >
{{ manageButtonLabel }} {{ manageButtonLabel }}
...@@ -176,7 +245,7 @@ ...@@ -176,7 +245,7 @@
<div class="btn-group table-action-buttons"> <div class="btn-group table-action-buttons">
<loading-button <loading-button
:loading="installButtonLoading" :loading="installButtonLoading"
:disabled="installButtonDisabled" :disabled="disabled || installButtonDisabled"
:label="installButtonLabel" :label="installButtonLabel"
class="js-cluster-application-install-button" class="js-cluster-application-install-button"
@click="installClicked" @click="installClicked"
...@@ -184,35 +253,5 @@ ...@@ -184,35 +253,5 @@
</div> </div>
</div> </div>
</div> </div>
<div
v-if="hasError || isUnknownStatus"
class="gl-responsive-table-row-layout"
role="row"
>
<div
class="alert alert-danger alert-block append-bottom-0 clusters-error-alert"
role="gridcell"
>
<div>
<p class="js-cluster-application-general-error-message">
{{ generalErrorDescription }}
</p>
<ul v-if="statusReason || requestReason">
<li
v-if="statusReason"
class="js-cluster-application-status-error-message"
>
{{ statusReason }}
</li>
<li
v-if="requestReason"
class="js-cluster-application-request-error-message"
>
{{ requestReason }}
</li>
</ul>
</div>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import helmInstallIllustration from '@gitlab-org/gitlab-svgs/illustrations/kubernetes-installation.svg';
import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png';
import gitlabLogo from 'images/cluster_app_logos/gitlab.png';
import helmLogo from 'images/cluster_app_logos/helm.png';
import jeagerLogo from 'images/cluster_app_logos/jeager.png';
import jupyterhubLogo from 'images/cluster_app_logos/jupyterhub.png';
import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png';
import meltanoLogo from 'images/cluster_app_logos/meltano.png';
import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import { s__, sprintf } from '../../locale'; import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue'; import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
...@@ -37,21 +46,21 @@ export default { ...@@ -37,21 +46,21 @@ export default {
default: '', default: '',
}, },
}, },
data: () => ({
elasticsearchLogo,
gitlabLogo,
helmLogo,
jeagerLogo,
jupyterhubLogo,
kubernetesLogo,
meltanoLogo,
prometheusLogo,
}),
computed: { computed: {
generalApplicationDescription() { helmInstalled() {
return sprintf( return (
_.escape( this.applications.helm.status === APPLICATION_STATUS.INSTALLED ||
s__( this.applications.helm.status === APPLICATION_STATUS.UPDATED
`ClusterIntegration|Install applications on your Kubernetes cluster.
Read more about %{helpLink}`,
),
),
{
helpLink: `<a href="${this.helpPath}">
${_.escape(s__('ClusterIntegration|installing applications'))}
</a>`,
},
false,
); );
}, },
ingressId() { ingressId() {
...@@ -128,34 +137,35 @@ export default { ...@@ -128,34 +137,35 @@ export default {
return this.applications.jupyter.hostname; return this.applications.jupyter.hostname;
}, },
}, },
created() {
this.helmInstallIllustration = helmInstallIllustration;
},
}; };
</script> </script>
<template> <template>
<section <section id="cluster-applications">
id="cluster-applications"
class="settings no-animate expanded"
>
<div class="settings-header">
<h4> <h4>
{{ s__('ClusterIntegration|Applications') }} {{ s__('ClusterIntegration|Applications') }}
</h4> </h4>
<p <p class="append-bottom-0">
class="append-bottom-0" {{ s__(`ClusterIntegration|Choose which applications to install on your Kubernetes cluster.
v-html="generalApplicationDescription" Helm Tiller is required to install any of the following applications.`) }}
> <a :href="helpPath">
{{ __('More information') }}
</a>
</p> </p>
</div>
<div class="settings-content"> <div class="cluster-application-list prepend-top-10">
<div class="append-bottom-20">
<application-row <application-row
id="helm" id="helm"
:logo-url="helmLogo"
:title="applications.helm.title" :title="applications.helm.title"
:status="applications.helm.status" :status="applications.helm.status"
:status-reason="applications.helm.statusReason" :status-reason="applications.helm.statusReason"
:request-status="applications.helm.requestStatus" :request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason" :request-reason="applications.helm.requestReason"
class="rounded-top"
title-link="https://docs.helm.sh/" title-link="https://docs.helm.sh/"
> >
<div slot="description"> <div slot="description">
...@@ -165,13 +175,27 @@ export default { ...@@ -165,13 +175,27 @@ export default {
and manages releases of your charts.`) }} and manages releases of your charts.`) }}
</div> </div>
</application-row> </application-row>
<div
v-show="!helmInstalled"
class="cluster-application-warning"
>
<div
class="svg-container"
v-html="helmInstallIllustration"
>
</div>
{{ s__(`ClusterIntegration|You must first install Helm Tiller before
installing the applications below`) }}
</div>
<application-row <application-row
:id="ingressId" :id="ingressId"
:logo-url="kubernetesLogo"
:title="applications.ingress.title" :title="applications.ingress.title"
:status="applications.ingress.status" :status="applications.ingress.status"
:status-reason="applications.ingress.statusReason" :status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus" :request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason" :request-reason="applications.ingress.requestReason"
:disabled="!helmInstalled"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
> >
<div slot="description"> <div slot="description">
...@@ -246,7 +270,6 @@ export default { ...@@ -246,7 +270,6 @@ export default {
</template> </template>
<div <div
v-else
v-html="ingressDescription" v-html="ingressDescription"
> >
</div> </div>
...@@ -254,12 +277,14 @@ export default { ...@@ -254,12 +277,14 @@ export default {
</application-row> </application-row>
<application-row <application-row
id="prometheus" id="prometheus"
:logo-url="prometheusLogo"
:title="applications.prometheus.title" :title="applications.prometheus.title"
:manage-link="managePrometheusPath" :manage-link="managePrometheusPath"
:status="applications.prometheus.status" :status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason" :status-reason="applications.prometheus.statusReason"
:request-status="applications.prometheus.requestStatus" :request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason" :request-reason="applications.prometheus.requestReason"
:disabled="!helmInstalled"
title-link="https://prometheus.io/docs/introduction/overview/" title-link="https://prometheus.io/docs/introduction/overview/"
> >
<div <div
...@@ -270,11 +295,13 @@ export default { ...@@ -270,11 +295,13 @@ export default {
</application-row> </application-row>
<application-row <application-row
id="runner" id="runner"
:logo-url="gitlabLogo"
:title="applications.runner.title" :title="applications.runner.title"
:status="applications.runner.status" :status="applications.runner.status"
:status-reason="applications.runner.statusReason" :status-reason="applications.runner.statusReason"
:request-status="applications.runner.requestStatus" :request-status="applications.runner.requestStatus"
:request-reason="applications.runner.requestReason" :request-reason="applications.runner.requestReason"
:disabled="!helmInstalled"
title-link="https://docs.gitlab.com/runner/" title-link="https://docs.gitlab.com/runner/"
> >
<div slot="description"> <div slot="description">
...@@ -286,12 +313,15 @@ export default { ...@@ -286,12 +313,15 @@ export default {
</application-row> </application-row>
<application-row <application-row
id="jupyter" id="jupyter"
:logo-url="jupyterhubLogo"
:title="applications.jupyter.title" :title="applications.jupyter.title"
:status="applications.jupyter.status" :status="applications.jupyter.status"
:status-reason="applications.jupyter.statusReason" :status-reason="applications.jupyter.statusReason"
:request-status="applications.jupyter.requestStatus" :request-status="applications.jupyter.requestStatus"
:request-reason="applications.jupyter.requestReason" :request-reason="applications.jupyter.requestReason"
:install-application-request-params="{ hostname: applications.jupyter.hostname }" :install-application-request-params="{ hostname: applications.jupyter.hostname }"
:disabled="!helmInstalled"
class="hide-bottom-border rounded-bottom"
title-link="https://jupyterhub.readthedocs.io/en/stable/" title-link="https://jupyterhub.readthedocs.io/en/stable/"
> >
<div slot="description"> <div slot="description">
...@@ -341,11 +371,6 @@ export default { ...@@ -341,11 +371,6 @@ export default {
</template> </template>
</div> </div>
</application-row> </application-row>
<!--
NOTE: Don't forget to update `clusters.scss`
min-height for this block and uncomment `application_spec` tests
-->
</div>
</div> </div>
</section> </section>
</template> </template>
...@@ -4,9 +4,60 @@ ...@@ -4,9 +4,60 @@
} }
} }
.cluster-applications-table { .cluster-application-row {
// Wait for the Vue to kick-in and render the applications block background: $gray-lighter;
min-height: 628px;
&.cluster-application-installed {
background: none;
}
.settings-message {
padding: $gl-vert-padding $gl-padding-8;
}
}
@media (min-width: map-get($grid-breakpoints, md)) {
.cluster-application-list {
border: 1px solid $border-color;
border-radius: $border-radius-default;
}
.cluster-application-row {
border-bottom: 1px solid $border-color;
padding: $gl-padding;
}
}
.cluster-application-logo {
border: 3px solid $white-light;
box-shadow: 0 0 0 1px $gray-normal;
&.avatar:hover {
border-color: $white-light;
}
}
.cluster-application-warning {
font-weight: bold;
text-align: center;
padding: $gl-padding;
border-bottom: 1px solid $white-normal;
.svg-container {
display: inline-block;
vertical-align: middle;
margin-right: $gl-padding-8;
width: 40px;
height: 40px;
}
}
.cluster-application-description {
flex: 1;
}
.cluster-application-disabled {
opacity: 0.5;
} }
.clusters-dropdown-menu { .clusters-dropdown-menu {
......
...@@ -15,7 +15,7 @@ module ApplicationHelper ...@@ -15,7 +15,7 @@ module ApplicationHelper
# Check if a particular controller is the current one # Check if a particular controller is the current one
# #
# args - One or more controller names to check # args - One or more controller names to check (using path notation when inside namespaces)
# #
# Examples # Examples
# #
...@@ -23,6 +23,11 @@ module ApplicationHelper ...@@ -23,6 +23,11 @@ module ApplicationHelper
# current_controller?(:tree) # => true # current_controller?(:tree) # => true
# current_controller?(:commits) # => false # current_controller?(:commits) # => false
# current_controller?(:commits, :tree) # => true # current_controller?(:commits, :tree) # => true
#
# # On Admin::ApplicationController
# current_controller?(:application) # => true
# current_controller?('admin/application') # => true
# current_controller?('gitlab/application') # => false
def current_controller?(*args) def current_controller?(*args)
args.any? do |v| args.any? do |v|
v.to_s.downcase == controller.controller_name || v.to_s.downcase == controller.controller_path v.to_s.downcase == controller.controller_name || v.to_s.downcase == controller.controller_path
......
...@@ -8,7 +8,7 @@ module TabHelper ...@@ -8,7 +8,7 @@ module TabHelper
# element is the value passed to the block. # element is the value passed to the block.
# #
# options - The options hash used to determine if the element is "active" (default: {}) # options - The options hash used to determine if the element is "active" (default: {})
# :controller - One or more controller names to check (optional). # :controller - One or more controller names to check, use path notation when namespaced (optional).
# :action - One or more action names to check (optional). # :action - One or more action names to check (optional).
# :path - A shorthand path, such as 'dashboard#index', to check (optional). # :path - A shorthand path, such as 'dashboard#index', to check (optional).
# :html_options - Extra options to be passed to the list element (optional). # :html_options - Extra options to be passed to the list element (optional).
...@@ -42,6 +42,20 @@ module TabHelper ...@@ -42,6 +42,20 @@ module TabHelper
# nav_link(controller: :tree, html_options: {class: 'home'}) { "Hello" } # nav_link(controller: :tree, html_options: {class: 'home'}) { "Hello" }
# # => '<li class="home active">Hello</li>' # # => '<li class="home active">Hello</li>'
# #
# # For namespaced controllers like Admin::AppearancesController#show
#
# # Controller and namespace matches
# nav_link(controller: 'admin/appearances') { "Hello" }
# # => '<li class="active">Hello</li>'
#
# # Controller and namespace matches but action doesn't
# nav_link(controller: 'admin/appearances', action: :edit) { "Hello" }
# # => '<li>Hello</li>'
#
# # Shorthand path with namespace
# nav_link(path: 'admin/appearances#show') { "Hello"}
# # => '<li class="active">Hello</li>'
#
# Returns a list item element String # Returns a list item element String
def nav_link(options = {}, &block) def nav_link(options = {}, &block)
klass = active_nav_link?(options) ? 'active' : '' klass = active_nav_link?(options) ? 'active' : ''
......
...@@ -13,7 +13,11 @@ class DashboardGroupMilestone < GlobalMilestone ...@@ -13,7 +13,11 @@ class DashboardGroupMilestone < GlobalMilestone
end end
def self.build_collection(groups) def self.build_collection(groups)
MilestonesFinder.new(group_ids: groups.select(:id)).execute.map { |m| new(m) } # rubocop: disable CodeReuse/Finder Milestone.of_groups(groups.select(:id))
.reorder_by_due_date_asc
.order_by_name_asc
.active
.map { |m| new(m) }
end end
override :group_milestone? override :group_milestone?
......
...@@ -50,6 +50,9 @@ class Milestone < ActiveRecord::Base ...@@ -50,6 +50,9 @@ class Milestone < ActiveRecord::Base
where(conditions.reduce(:or)) where(conditions.reduce(:or))
end end
scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) }
scope :reorder_by_due_date_asc, -> { reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) }
validates :group, presence: true, unless: :project validates :group, presence: true, unless: :project
validates :project, presence: true, unless: :group validates :project, presence: true, unless: :group
...@@ -153,7 +156,7 @@ class Milestone < ActiveRecord::Base ...@@ -153,7 +156,7 @@ class Milestone < ActiveRecord::Base
sorted = sorted =
case method.to_s case method.to_s
when 'due_date_asc' when 'due_date_asc'
reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) reorder_by_due_date_asc
when 'due_date_desc' when 'due_date_desc'
reorder(Gitlab::Database.nulls_last_order('due_date', 'DESC')) reorder(Gitlab::Database.nulls_last_order('due_date', 'DESC'))
when 'name_asc' when 'name_asc'
......
---
title: Updated icons used in filtered search dropdowns
merge_request: 21694
author:
type: changed
---
title: Improve install flow of Kubernetes cluster apps
merge_request: 21567
author:
type: changed
...@@ -65,13 +65,18 @@ In the rare case that you need the feature flag to be on automatically, use ...@@ -65,13 +65,18 @@ In the rare case that you need the feature flag to be on automatically, use
Feature.enabled?(:feature_flag, project, default_enabled: true) Feature.enabled?(:feature_flag, project, default_enabled: true)
``` ```
For more information about rolling out changes using feature flags, refer to the
[Rolling out changes using feature flags](rolling_out_changes_using_feature_flags.md)
guide.
### Specs ### Specs
In the test environment `Feature.enabled?` is stubbed to always respond to `true`, In the test environment `Feature.enabled?` is stubbed to always respond to `true`,
so we make sure behavior under feature flag doesn't go untested in some non-specific so we make sure behavior under feature flag doesn't go untested in some non-specific
contexts. contexts.
If you need to test the feature flag in a different state, you need to stub it with: Whenever a feature flag is present, make sure to test _both_ states of the
feature flag. You can stub a feature flag as follows:
```ruby ```ruby
stub_feature_flags(my_feature_flag: false) stub_feature_flags(my_feature_flag: false)
......
# Rolling out changes using feature flags
[Feature flags](feature_flags.md) can be used to gradually roll out changes, be
it a new feature, or a performance improvement. By using feature flags, we can
comfortably measure the impact of our changes, while still being able to easily
disable those changes, without having to revert an entire release.
## When to use feature flags
Starting with GitLab 11.4, developers are required to use feature flags for
non-trivial changes. Such changes include:
* New features (e.g. a new merge request widget, epics, etc).
* Complex performance improvements that may require additional testing in
production, such as rewriting complex queries.
* Invasive changes to the user interface, such as a new navigation bar or the
removal of a sidebar.
* Adding support for importing projects from a third-party service.
In all cases, those working on the changes can best decide if a feature flag is
necessary. For example, changing the color of a button doesn't need a feature
flag, while changing the navigation bar definitely needs one. In case you are
uncertain if a feature flag is necessary, simply ask about this in the merge
request, and those reviewing the changes will likely provide you with an answer.
When using a feature flag for UI elements, make sure to _also_ use a feature
flag for the underlying backend code, if there is any. This ensures there is
absolutely no way to use the feature until it is enabled.
## The cost of feature flags
When reading the above, one might be tempted to think this procedure is going to
add a lot of work. Fortunately, this is not the case, and we'll show why. For
this example we'll specify the cost of the work to do as a number, ranging from
0 to infinity. The greater the number, the more expensive the work is. The cost
does _not_ translate to time, it's just a way of measuring complexity of one
change relative to another.
Let's say we are building a new feature, and we have determined that the cost of
this is 10. We have also determined that the cost of adding a feature flag check
in a variety of places is 1. If we do not use feature flags, and our feature
works as intended, our total cost is 10. This however is the best case scenario.
Optimising for the best case scenario is guaranteed to lead to trouble, whereas
optimising for the worst case scenario is almost always better.
To illustrate this, let's say our feature causes an outage, and there's no
immediate way to resolve it. This means we'd have to take the following steps to
resolve the outage:
1. Revert the release.
1. Perform any cleanups that might be necessary, depending on the changes that
were made.
1. Revert the commit, ensuring the "master" branch remains stable. This is
especially necessary if solving the problem can take days or even weeks.
1. Pick the revert commit into the appropriate stable branches, ensuring we
don't block any future releases until the problem is resolved.
As history has shown, these steps are time consuming, complex, often involve
many developers, and worst of all: our users will have a bad experience using
GitLab.com until the problem is resolved.
Now let's say that all of this has an associated cost of 10. This means that in
the worst case scenario, which we should optimise for, our total cost is now 20.
If we had used a feature flag, things would have been very different. We don't
need to revert a release, and because feature flags are disabled by default we
don't need to revert and pick any Git commits. In fact, all we have to do is
disable the feature, and _maybe_ perform some cleanup. Let's say that the cost
of this is 1. In this case, our best case cost is 11: 10 to build the feature,
and 1 to add the feature flag. The worst case cost is now 12: 10 to build the
feature, 1 to add the feature flag, and 1 to disable it.
Here we can see that in the best case scenario the work necessary is only a tiny
bit more compared to not using a feature flag. Meanwhile, the process of
reverting our changes has been made significantly cheaper, to the point of being
trivial.
In other words, feature flags do not slow down the development process. Instead,
they speed up the process as managing incidents now becomes _much_ easier. Once
continuous deployments are easier to perform, the time to iterate on a feature
is reduced even further, as you no longer need to wait weeks before your changes
are available on GitLab.com.
## Rolling out changes
The procedure of using feature flags is straightforward, and similar to not
using them. You add the necessary tests (make sure to test both the on and off
states of your feature flag(s)), make sure they all pass, have the code
reviewed, etc. You then submit your merge request, and add the ~"feature flag"
label. This label is used to signal to release managers that your changes are
hidden behind a feature flag and that it is safe to pick the MR into a stable
branch, without the need for an exception request.
When the changes are deployed it is time to start rolling out the feature to our
users. The exact procedure of rolling out a change is unspecified, as this can
vary from change to change. However, in general we recommend rolling out changes
incrementally, instead of enabling them for everybody right away. We also
recommend you to _not_ enable a feature _before_ the code is being deployed.
This allows you to separate rolling out a feature from a deploy, making it
easier to measure the impact of both separately.
GitLab's feature library (using
[Flipper](https://github.com/jnunemaker/flipper), and covered in the [Feature
Flags](feature_flags.md) guide) supports rolling out changes to a percentage of
users. This in turn can be controlled using [GitLab
chatops](https://docs.gitlab.com/ee/ci/chatops/).
For example, to enable a feature for 25% of all users, run the following in
Slack:
```
/chatops run feature set new_navigation_bar 25
```
This will enable the feature for GitLab.com, with `new_navigation_bar` being the
name of the feature. We can also enable the feature for <https://dev.gitlab.org>
or <https://staging.gitlab.com>:
```
/chatops run feature set new_navigation_bar 25 --dev
/chatops run feature set new_navigation_bar 25 --staging
```
If you are not certain what percentages to use, simply use the following steps:
1. 25%
1. 50%
1. 75%
1. 100%
Between every step you'll want to wait a little while and monitor the
appropriate graphs on <https://dashboards.gitlab.net>. The exact time to wait
may differ. For some features a few minutes is enough, while for others you may
want to wait several hours or even days. This is entirely up to you, just make
sure it is clearly communicated to your team, and the Production team if you
anticipate any potential problems.
Once a change is deemed stable, submit a new merge request to remove the
feature flag. This ensures the change is available to all users and self-hosted
instances. Make sure to add the ~"feature flag" label to this merge request so
release managers are aware the changes are hidden behind a feature flag. If the
merge request has to be picked into a stable branch (e.g. after the 7th), make
sure to also add the appropriate "Pick into X" label (e.g. "Pick into 11.4").
One might be tempted to think this will delay the release of a feature by at
least one month (= one release). This is not the case. A feature flag does not
have to stick around for a specific amount of time (e.g. at least one release),
instead they should stick around until the feature is deemed stable. Stable
means it works on GitLab.com without causing any problems, such as outages. In
most cases this will translate to a feature (with a feature flag) being shipped
in RC1, followed by the feature flag being removed in RC2. This in turn means
the feature will be stable by the time we publish a stable package around the
22nd of the month.
...@@ -1637,6 +1637,9 @@ msgstr "" ...@@ -1637,6 +1637,9 @@ msgstr ""
msgid "ClusterIntegration|Certificate Authority bundle (PEM format)" msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
msgstr "" msgstr ""
msgid "ClusterIntegration|Choose which applications to install on your Kubernetes cluster. Helm Tiller is required to install any of the following applications."
msgstr ""
msgid "ClusterIntegration|Choose which of your environments will use this cluster." msgid "ClusterIntegration|Choose which of your environments will use this cluster."
msgstr "" msgstr ""
...@@ -1736,9 +1739,6 @@ msgstr "" ...@@ -1736,9 +1739,6 @@ msgstr ""
msgid "ClusterIntegration|Install Prometheus" msgid "ClusterIntegration|Install Prometheus"
msgstr "" msgstr ""
msgid "ClusterIntegration|Install applications on your Kubernetes cluster. Read more about %{helpLink}"
msgstr ""
msgid "ClusterIntegration|Installed" msgid "ClusterIntegration|Installed"
msgstr "" msgstr ""
...@@ -1952,6 +1952,9 @@ msgstr "" ...@@ -1952,6 +1952,9 @@ msgstr ""
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "" msgstr ""
msgid "ClusterIntegration|You must first install Helm Tiller before installing the applications below"
msgstr ""
msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr "" msgstr ""
...@@ -1970,9 +1973,6 @@ msgstr "" ...@@ -1970,9 +1973,6 @@ msgstr ""
msgid "ClusterIntegration|help page" msgid "ClusterIntegration|help page"
msgstr "" msgstr ""
msgid "ClusterIntegration|installing applications"
msgstr ""
msgid "ClusterIntegration|meets the requirements" msgid "ClusterIntegration|meets the requirements"
msgstr "" msgstr ""
......
...@@ -86,7 +86,6 @@ describe 'User Cluster', :js do ...@@ -86,7 +86,6 @@ describe 'User Cluster', :js do
context 'when user disables the cluster' do context 'when user disables the cluster' do
before do before do
page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click
fill_in 'cluster_name', with: 'dev-cluster'
page.within('#cluster-integration') { click_button 'Save changes' } page.within('#cluster-integration') { click_button 'Save changes' }
end end
......
...@@ -3,48 +3,66 @@ require 'spec_helper' ...@@ -3,48 +3,66 @@ require 'spec_helper'
describe ApplicationHelper do describe ApplicationHelper do
describe 'current_controller?' do describe 'current_controller?' do
it 'returns true when controller matches argument' do before do
stub_controller_name('foo') stub_controller_name('foo')
end
expect(helper.current_controller?(:foo)).to eq true it 'returns true when controller matches argument' do
expect(helper.current_controller?(:foo)).to be_truthy
end end
it 'returns false when controller does not match argument' do it 'returns false when controller does not match argument' do
stub_controller_name('foo') expect(helper.current_controller?(:bar)).to be_falsey
expect(helper.current_controller?(:bar)).to eq false
end end
it 'takes any number of arguments' do it 'takes any number of arguments' do
stub_controller_name('foo') expect(helper.current_controller?(:baz, :bar)).to be_falsey
expect(helper.current_controller?(:baz, :bar, :foo)).to be_truthy
end
context 'when namespaced' do
before do
stub_controller_path('bar/foo')
end
it 'returns true when controller matches argument' do
expect(helper.current_controller?(:foo)).to be_truthy
end
expect(helper.current_controller?(:baz, :bar)).to eq false it 'returns true when controller and namespace matches argument in path notation' do
expect(helper.current_controller?(:baz, :bar, :foo)).to eq true expect(helper.current_controller?('bar/foo')).to be_truthy
end
it 'returns false when namespace doesnt match' do
expect(helper.current_controller?('foo/foo')).to be_falsey
end
end end
def stub_controller_name(value) def stub_controller_name(value)
allow(helper.controller).to receive(:controller_name).and_return(value) allow(helper.controller).to receive(:controller_name).and_return(value)
end end
def stub_controller_path(value)
allow(helper.controller).to receive(:controller_path).and_return(value)
end
end end
describe 'current_action?' do describe 'current_action?' do
it 'returns true when action matches' do before do
stub_action_name('foo') stub_action_name('foo')
end
expect(helper.current_action?(:foo)).to eq true it 'returns true when action matches' do
expect(helper.current_action?(:foo)).to be_truthy
end end
it 'returns false when action does not match' do it 'returns false when action does not match' do
stub_action_name('foo') expect(helper.current_action?(:bar)).to be_falsey
expect(helper.current_action?(:bar)).to eq false
end end
it 'takes any number of arguments' do it 'takes any number of arguments' do
stub_action_name('foo') expect(helper.current_action?(:baz, :bar)).to be_falsey
expect(helper.current_action?(:baz, :bar, :foo)).to be_truthy
expect(helper.current_action?(:baz, :bar)).to eq false
expect(helper.current_action?(:baz, :bar, :foo)).to eq true
end end
def stub_action_name(value) def stub_action_name(value)
...@@ -100,8 +118,7 @@ describe ApplicationHelper do ...@@ -100,8 +118,7 @@ describe ApplicationHelper do
end end
it 'accepts a custom html_class' do it 'accepts a custom html_class' do
expect(element(html_class: 'custom_class').attr('class')) expect(element(html_class: 'custom_class').attr('class')).to eq 'js-timeago custom_class'
.to eq 'js-timeago custom_class'
end end
it 'accepts a custom tooltip placement' do it 'accepts a custom tooltip placement' do
...@@ -114,6 +131,7 @@ describe ApplicationHelper do ...@@ -114,6 +131,7 @@ describe ApplicationHelper do
it 'add class for the short format' do it 'add class for the short format' do
timeago_element = element(short_format: 'short') timeago_element = element(short_format: 'short')
expect(timeago_element.attr('class')).to eq 'js-short-timeago' expect(timeago_element.attr('class')).to eq 'js-short-timeago'
expect(timeago_element.next_element).to eq nil expect(timeago_element.next_element).to eq nil
end end
...@@ -128,11 +146,9 @@ describe ApplicationHelper do ...@@ -128,11 +146,9 @@ describe ApplicationHelper do
context 'when alternate support url is specified' do context 'when alternate support url is specified' do
let(:alternate_url) { 'http://company.example.com/getting-help' } let(:alternate_url) { 'http://company.example.com/getting-help' }
before do it 'returns the alternate support url' do
stub_application_setting(help_page_support_url: alternate_url) stub_application_setting(help_page_support_url: alternate_url)
end
it 'returns the alternate support url' do
expect(helper.support_url).to eq(alternate_url) expect(helper.support_url).to eq(alternate_url)
end end
end end
...@@ -155,9 +171,12 @@ describe ApplicationHelper do ...@@ -155,9 +171,12 @@ describe ApplicationHelper do
describe '#autocomplete_data_sources' do describe '#autocomplete_data_sources' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:noteable_type) { Issue } let(:noteable_type) { Issue }
it 'returns paths for autocomplete_sources_controller' do it 'returns paths for autocomplete_sources_controller' do
sources = helper.autocomplete_data_sources(project, noteable_type) sources = helper.autocomplete_data_sources(project, noteable_type)
expect(sources.keys).to match_array([:members, :issues, :mergeRequests, :labels, :milestones, :commands]) expect(sources.keys).to match_array([:members, :issues, :mergeRequests, :labels, :milestones, :commands])
sources.keys.each do |key| sources.keys.each do |key|
expect(sources[key]).not_to be_nil expect(sources[key]).not_to be_nil
end end
......
...@@ -9,33 +9,73 @@ describe TabHelper do ...@@ -9,33 +9,73 @@ describe TabHelper do
allow(self).to receive(:action_name).and_return('foo') allow(self).to receive(:action_name).and_return('foo')
end end
context 'with the content of the li' do
it "captures block output" do it "captures block output" do
expect(nav_link { "Testing Blocks" }).to match(/Testing Blocks/) expect(nav_link { "Testing Blocks" }).to match(/Testing Blocks/)
end end
end
context 'with controller param' do
it "performs checks on the current controller" do it "performs checks on the current controller" do
expect(nav_link(controller: :foo)).to match(/<li class="active">/) expect(nav_link(controller: :foo)).to match(/<li class="active">/)
expect(nav_link(controller: :bar)).not_to match(/active/) expect(nav_link(controller: :bar)).not_to match(/active/)
expect(nav_link(controller: [:foo, :bar])).to match(/active/) expect(nav_link(controller: [:foo, :bar])).to match(/active/)
end end
context 'with action param' do
it "performs checks on both controller and action when both are present" do
expect(nav_link(controller: :bar, action: :foo)).not_to match(/active/)
expect(nav_link(controller: :foo, action: :bar)).not_to match(/active/)
expect(nav_link(controller: :foo, action: :foo)).to match(/active/)
end
end
context 'with namespace in path notation' do
before do
allow(controller).to receive(:controller_path).and_return('bar/foo')
end
it 'performs checks on both controller and namespace' do
expect(nav_link(controller: 'foo/foo')).not_to match(/active/)
expect(nav_link(controller: 'bar/foo')).to match(/active/)
end
context 'with action param' do
it "performs checks on both namespace, controller and action when they are all present" do
expect(nav_link(controller: 'foo/foo', action: :foo)).not_to match(/active/)
expect(nav_link(controller: 'bar/foo', action: :bar)).not_to match(/active/)
expect(nav_link(controller: 'bar/foo', action: :foo)).to match(/active/)
end
end
end
end
context 'with action param' do
it "performs checks on the current action" do it "performs checks on the current action" do
expect(nav_link(action: :foo)).to match(/<li class="active">/) expect(nav_link(action: :foo)).to match(/<li class="active">/)
expect(nav_link(action: :bar)).not_to match(/active/) expect(nav_link(action: :bar)).not_to match(/active/)
expect(nav_link(action: [:foo, :bar])).to match(/active/) expect(nav_link(action: [:foo, :bar])).to match(/active/)
end end
it "performs checks on both controller and action when both are present" do
expect(nav_link(controller: :bar, action: :foo)).not_to match(/active/)
expect(nav_link(controller: :foo, action: :bar)).not_to match(/active/)
expect(nav_link(controller: :foo, action: :foo)).to match(/active/)
end end
context 'with path param' do
it "accepts a path shorthand" do it "accepts a path shorthand" do
expect(nav_link(path: 'foo#bar')).not_to match(/active/) expect(nav_link(path: 'foo#bar')).not_to match(/active/)
expect(nav_link(path: 'foo#foo')).to match(/active/) expect(nav_link(path: 'foo#foo')).to match(/active/)
end end
context 'with namespace' do
before do
allow(controller).to receive(:controller_path).and_return('bar/foo')
end
it 'accepts a path shorthand with namespace' do
expect(nav_link(path: 'bar/foo#foo')).to match(/active/)
expect(nav_link(path: 'foo/foo#foo')).not_to match(/active/)
end
end
end
it "passes extra html options to the list element" do it "passes extra html options to the list element" do
expect(nav_link(action: :foo, html_options: { class: 'home' })).to match(/<li class="home active">/) expect(nav_link(action: :foo, html_options: { class: 'home' })).to match(/<li class="home active">/)
expect(nav_link(html_options: { class: 'active' })).to match(/<li class="active">/) expect(nav_link(html_options: { class: 'active' })).to match(/<li class="active">/)
......
...@@ -97,6 +97,24 @@ describe Milestone do ...@@ -97,6 +97,24 @@ describe Milestone do
end end
end end
describe '.order_by_name_asc' do
it 'sorts by name ascending' do
milestone1 = create(:milestone, title: 'Foo')
milestone2 = create(:milestone, title: 'Bar')
expect(described_class.order_by_name_asc).to eq([milestone2, milestone1])
end
end
describe '.reorder_by_due_date_asc' do
it 'reorders the input relation' do
milestone1 = create(:milestone, due_date: Date.new(2018, 9, 30))
milestone2 = create(:milestone, due_date: Date.new(2018, 10, 20))
expect(described_class.reorder_by_due_date_asc).to eq([milestone1, milestone2])
end
end
describe "#percent_complete" do describe "#percent_complete" do
it "does not count open issues" do it "does not count open issues" do
milestone.issues << issue milestone.issues << issue
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment