Commit b2954efd authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'origin/master' into ce-to-ee-2018-10-04

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parents 51d5656f 46614d75
......@@ -211,8 +211,13 @@ module QuickActions
end
params '~label1 ~"label 2"'
condition do
available_labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true).execute
if project
available_labels = LabelsFinder
.new(current_user, project_id: project.id, include_ancestor_groups: true)
.execute
end
project &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
available_labels.any?
end
......@@ -287,6 +292,7 @@ module QuickActions
end
params '#issue | !merge_request'
condition do
[MergeRequest, Issue].include?(issuable.class) &&
current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
end
parse_params do |issuable_param|
......@@ -444,6 +450,7 @@ module QuickActions
end
params '<time(1h30m | -1h30m)> <date(YYYY-MM-DD)>'
condition do
issuable.is_a?(TimeTrackable) &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
parse_params do |raw_time_date|
......@@ -494,7 +501,7 @@ module QuickActions
desc "Lock the discussion"
explanation "Locks the discussion"
condition do
issuable.is_a?(Issuable) &&
[MergeRequest, Issue].include?(issuable.class) &&
issuable.persisted? &&
!issuable.discussion_locked? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
......@@ -506,7 +513,7 @@ module QuickActions
desc "Unlock the discussion"
explanation "Unlocks the discussion"
condition do
issuable.is_a?(Issuable) &&
[MergeRequest, Issue].include?(issuable.class) &&
issuable.persisted? &&
issuable.discussion_locked? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
......
......@@ -195,7 +195,7 @@
= _('Charts')
- if project_nav_tab? :operations
= nav_link(controller: [:environments, :clusters, :user, :gcp]) do
= nav_link(controller: [:environments, :clusters, :user, :gcp, :feature_flags]) do
= link_to metrics_project_environments_path(@project), class: 'shortcuts-operations' do
.nav-icon-container
= sprite_icon('cloud-gear')
......@@ -203,7 +203,7 @@
= _('Operations')
%ul.sidebar-sub-level-items
= nav_link(controller: [:environments, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do
= nav_link(controller: [:environments, :clusters, :user, :gcp, :feature_flags], html_options: { class: "fly-out-top-item" } ) do
= link_to metrics_project_environments_path(@project) do
%strong.fly-out-top-item-name
= _('Operations')
......@@ -249,6 +249,12 @@
%span= _("Got it!")
= sprite_icon('thumb-up')
- if project_nav_tab? :feature_flags
= nav_link(controller: :feature_flags) do
= link_to project_feature_flags_path(@project), title: _('Feature Flags'), class: 'shortcuts-feature-flags' do
%span
= _('Feature Flags')
- if project_nav_tab? :container_registry
= nav_link(controller: %w[projects/registry/repositories]) do
= link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
......
......@@ -102,6 +102,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
get 'members'
get 'labels'
get 'epics'
get 'commands'
end
end
......
......@@ -351,6 +351,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
namespace :ci do
resource :lint, only: [:show, :create]
end
## EE-specific
resources :feature_flags
## EE-specific
end
draw :legacy_builds
......
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180926140319) do
ActiveRecord::Schema.define(version: 20180930171532) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -1943,6 +1943,24 @@ ActiveRecord::Schema.define(version: 20180926140319) do
t.string "nonce", null: false
end
create_table "operations_feature_flags", id: :bigserial, force: :cascade do |t|
t.integer "project_id", null: false
t.boolean "active", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "name", null: false
t.text "description"
end
add_index "operations_feature_flags", ["project_id", "name"], name: "index_operations_feature_flags_on_project_id_and_name", unique: true, using: :btree
create_table "operations_feature_flags_clients", id: :bigserial, force: :cascade do |t|
t.integer "project_id", null: false
t.string "token", null: false
end
add_index "operations_feature_flags_clients", ["project_id", "token"], name: "index_operations_feature_flags_clients_on_project_id_and_token", unique: true, using: :btree
create_table "packages_maven_metadata", id: :bigserial, force: :cascade do |t|
t.integer "package_id", limit: 8, null: false
t.datetime_with_timezone "created_at", null: false
......@@ -2979,7 +2997,6 @@ ActiveRecord::Schema.define(version: 20180926140319) do
t.datetime_with_timezone "updated_at", null: false
t.integer "occurrence_id", limit: 8, null: false
t.integer "identifier_id", limit: 8, null: false
t.boolean "primary", default: false, null: false
end
add_index "vulnerability_occurrence_identifiers", ["identifier_id"], name: "index_vulnerability_occurrence_identifiers_on_identifier_id", using: :btree
......@@ -2994,10 +3011,10 @@ ActiveRecord::Schema.define(version: 20180926140319) do
t.integer "pipeline_id", null: false
t.integer "project_id", null: false
t.integer "scanner_id", limit: 8, null: false
t.binary "first_seen_in_commit_sha", null: false
t.binary "project_fingerprint", null: false
t.binary "location_fingerprint", null: false
t.binary "primary_identifier_fingerprint", null: false
t.string "uuid", limit: 36, null: false
t.string "ref", null: false
t.string "name", null: false
t.string "metadata_version", null: false
......@@ -3005,8 +3022,9 @@ ActiveRecord::Schema.define(version: 20180926140319) do
end
add_index "vulnerability_occurrences", ["pipeline_id"], name: "index_vulnerability_occurrences_on_pipeline_id", using: :btree
add_index "vulnerability_occurrences", ["project_id", "ref", "scanner_id", "primary_identifier_fingerprint", "location_fingerprint"], name: "index_vulnerability_occurrences_on_unique_keys", unique: true, using: :btree
add_index "vulnerability_occurrences", ["project_id", "ref", "primary_identifier_fingerprint", "location_fingerprint", "pipeline_id", "scanner_id"], name: "index_vulnerability_occurrences_on_unique_keys", unique: true, using: :btree
add_index "vulnerability_occurrences", ["scanner_id"], name: "index_vulnerability_occurrences_on_scanner_id", using: :btree
add_index "vulnerability_occurrences", ["uuid"], name: "index_vulnerability_occurrences_on_uuid", unique: true, using: :btree
create_table "vulnerability_scanners", id: :bigserial, force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
......@@ -3233,6 +3251,8 @@ ActiveRecord::Schema.define(version: 20180926140319) do
add_foreign_key "notes", "projects", name: "fk_99e097b079", on_delete: :cascade
add_foreign_key "notification_settings", "users", name: "fk_0c95e91db7", on_delete: :cascade
add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"
add_foreign_key "operations_feature_flags", "projects", on_delete: :cascade
add_foreign_key "operations_feature_flags_clients", "projects", on_delete: :cascade
add_foreign_key "packages_maven_metadata", "packages_packages", column: "package_id", name: "fk_be88aed360", on_delete: :cascade
add_foreign_key "packages_package_files", "packages_packages", column: "package_id", name: "fk_86f0f182f8", on_delete: :cascade
add_foreign_key "packages_packages", "projects", on_delete: :cascade
......
# From Community Edition 11.4 to Enterprise Edition 11.4
This guide assumes you have a correctly configured and tested installation of
GitLab Community Edition 11.4. If you run into any trouble or if you have any
questions please contact us at [support@gitlab.com].
### 0. Backup
Make a backup just in case something goes wrong:
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
For installations using MySQL, this may require granting "LOCK TABLES"
privileges to the GitLab user on the database version.
### 1. Stop server
```bash
sudo service gitlab stop
```
### 2. Get the EE code
```bash
cd /home/git/gitlab
sudo -u git -H git remote add -f ee https://gitlab.com/gitlab-org/gitlab-ee.git
sudo -u git -H git checkout 11-4-stable-ee
```
### 3. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
# MySQL installations (note: the line below states '--without postgres')
sudo -u git -H bundle install --without postgres development test --deployment
# PostgreSQL installations (note: the line below states '--without mysql')
sudo -u git -H bundle install --without mysql development test --deployment
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
# Clean up assets and cache
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
```
### 4. Install `gitlab-elasticsearch-indexer` (optional) **[STARTER ONLY]**
If you're interested in using GitLab's new [elasticsearch repository indexer](../integration/elasticsearch.md#elasticsearch-repository-indexer-beta) (currently in beta)
please follow the instructions on the document linked above and enable the
indexer usage in the GitLab admin settings.
### 5. Start application
```bash
sudo service gitlab start
sudo service nginx restart
```
### 6. Check application status
Check if GitLab and its environment are configured correctly:
```bash
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
```
To make sure you didn't miss anything run a more thorough check with:
```bash
sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
```
If all items are green, then congratulations upgrade complete!
## Things went south? Revert to previous version (Community Edition 11.3)
### 1. Revert the code to the previous version
```bash
cd /home/git/gitlab
sudo -u git -H git checkout 11-4-stable
```
### 2. Restore from the backup
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
```
[support@gitlab.com]: mailto:support@gitlab.com
# Code owners
# Code Owners **[STARTER]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6916)
in [GitLab Starter](https://about.gitlab.com/pricing/) 11.3.
......@@ -21,7 +21,7 @@ When a file matches multiple entries in the `CODEOWNERS` file,
the users from all entries are displayed on the blob page of
the given file.
## The syntax of a code owners file
## The syntax of Code Owners files
Files can be specified using the same kind of patterns you would use
in the `.gitignore` file followed by the `@username` or email of one
......
# Feature Flags
## Client libraries
......@@ -44,7 +44,7 @@ discussions, and descriptions:
| `/remove_due_date` | Remove due date | ✓ | |
| `/weight 0,1,2, ...` | Set weight **[STARTER]** | ✓ | |
| `/clear_weight` | Clears weight **[STARTER]** | ✓ | |
| `/epic <group&epic &#124; Epic URL>` | Add to epic **[ULTIMATE]** | ✓ | |
| `/epic <&epic &#124; group&epic &#124; Epic URL>` | Add to epic **[ULTIMATE]** | ✓ | |
| `/remove_epic` | Removes from epic **[ULTIMATE]** | ✓ | |
| `/confidential` | Make confidential | ✓ | |
| `/duplicate #issue` | Mark this issue as a duplicate of another issue | ✓ |
......
......@@ -13,6 +13,10 @@ class Groups::AutocompleteSourcesController < Groups::ApplicationController
render json: @autocomplete_service.epics
end
def commands
render json: @autocomplete_service.commands(target)
end
private
def load_autocomplete_service
......
class Projects::FeatureFlagsController < Projects::ApplicationController
respond_to :html
before_action :authorize_read_feature_flag!
before_action :authorize_update_feature_flag!, only: [:edit, :update]
before_action :authorize_destroy_feature_flag!, only: [:destroy]
before_action :feature_flag, only: [:edit, :update, :destroy]
def index
@feature_flags = project.operations_feature_flags
.ordered
.page(params[:page]).per(30)
end
def new
@feature_flag = project.operations_feature_flags.new
end
def create
@feature_flag = project.operations_feature_flags.create(create_params)
if @feature_flag.persisted?
redirect_to project_feature_flags_path(@project), status: 302, notice: 'Feature flag was successfully created.'
else
render :new
end
end
def edit
end
def update
if feature_flag.update(update_params)
redirect_to project_feature_flags_path(@project), status: 302, notice: 'Feature flag was successfully updated.'
else
render :edit
end
end
def destroy
if feature_flag.destroy
redirect_to project_feature_flags_path(@project), status: 302, notice: 'Feature flag was successfully removed.'
else
redirect_to project_feature_flags_path(@project), status: 302, alert: 'Feature flag was not removed.'
end
end
protected
def feature_flag
@feature_flag ||= project.operations_feature_flags.find(params[:id])
end
def create_params
params.require(:operations_feature_flag)
.permit(:name, :description, :active)
end
def update_params
params.require(:operations_feature_flag)
.permit(:name, :description, :active)
end
end
......@@ -70,7 +70,8 @@ module EE
{
members: members_group_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
labels: labels_group_autocomplete_sources_path(object),
epics: epics_group_autocomplete_sources_path(object)
epics: epics_group_autocomplete_sources_path(object),
commands: commands_group_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id])
}
end
......
......@@ -25,9 +25,20 @@ module EE
nav_tabs << :packages
end
if can?(current_user, :read_feature_flag, project) && !nav_tabs.include?(:operations)
nav_tabs << :operations
end
nav_tabs
end
override :tab_ability_map
def tab_ability_map
tab_ability_map = super
tab_ability_map[:feature_flags] = :read_feature_flag
tab_ability_map
end
override :project_permissions_settings
def project_permissions_settings(project)
super.merge(
......
# frozen_string_literal: true
module FeatureFlagsHelper
include ::API::Helpers::RelatedResourcesHelpers
def unleash_api_url(project)
expose_url(api_v4_feature_flags_unleash_path(project_id: project.id))
end
def unleash_api_instance_id(project)
project.feature_flags_client_token
end
end
......@@ -108,5 +108,9 @@ module LicenseHelper
!Gitlab::CurrentSettings.should_check_namespace_plan? && show_promotions? && show_callout?('promote_advanced_search_dismissed') && !License.feature_available?(:elastic_search)
end
def promote_feature?(feature_name)
!@project&.group&.feature_available?(feature_name) && show_promotions? && show_callout?(feature_name)
end
extend self
end
......@@ -194,7 +194,7 @@ module EE
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
return reference unless cross_reference?(from) || full
return reference unless (cross_reference?(from) && !group.projects.include?(from)) || full
"#{group.full_path}#{reference}"
end
......
......@@ -50,6 +50,9 @@ module EE
has_many :prometheus_alerts, inverse_of: :project
has_many :prometheus_alert_events, inverse_of: :project
has_many :operations_feature_flags, class_name: 'Operations::FeatureFlag'
has_one :operations_feature_flags_client, class_name: 'Operations::FeatureFlagsClient'
scope :with_shared_runners_limit_enabled, -> { with_shared_runners.non_public_only }
scope :mirror, -> { where(mirror: true) }
......@@ -559,6 +562,11 @@ module EE
change_head(root_ref) if root_ref.present? && root_ref != default_branch
end
def feature_flags_client_token
instance = operations_feature_flags_client || create_operations_feature_flags_client!
instance.token
end
private
def set_override_pull_mirror_available
......
......@@ -67,6 +67,7 @@ class License < ActiveRecord::Base
custom_project_templates
packages
code_owner_as_approver_suggestion
feature_flags
].freeze
EEU_FEATURES = EEP_FEATURES + %i[
......
module Operations
class FeatureFlag < ActiveRecord::Base
self.table_name = 'operations_feature_flags'
belongs_to :project
validates :project, presence: true
validates :name,
presence: true,
length: 2..63,
format: {
with: Gitlab::Regex.feature_flag_regex,
message: Gitlab::Regex.feature_flag_regex_message
}
validates :name, uniqueness: { scope: :project_id }
validates :description, allow_blank: true, length: 0..255
scope :ordered, -> { order(:name) }
end
end
module Operations
class FeatureFlagsClient < ActiveRecord::Base
include TokenAuthenticatable
self.table_name = 'operations_feature_flags_clients'
belongs_to :project
validates :project, presence: true
validates :token, presence: true
add_authentication_token_field :token
before_validation :ensure_token!
def self.find_for_project_and_token(project, token)
return unless project
return unless token
find_by(token: token, project: project)
end
end
end
......@@ -18,7 +18,6 @@ module Vulnerabilities
critical: 7
}.with_indifferent_access.freeze
sha_attribute :first_seen_in_commit_sha
sha_attribute :project_fingerprint
sha_attribute :primary_identifier_fingerprint
sha_attribute :location_fingerprint
......@@ -40,15 +39,15 @@ module Vulnerabilities
validates :scanner, presence: true
validates :project, presence: true
validates :pipeline, presence: true
validates :uuid, presence: true
validates :ref, presence: true
validates :first_seen_in_commit_sha, presence: true
validates :project_fingerprint, presence: true
validates :primary_identifier_fingerprint, presence: true
validates :location_fingerprint, presence: true
# Uniqueness validation doesn't work with binary columns, so save this useless query. It is enforce by DB constraint anyway.
# TODO: find out why it fails
# validates :location_fingerprint, presence: true, uniqueness: { scope: [:primary_identifier_fingerprint, :scanner_id, :ref, :project_id] }
# validates :location_fingerprint, presence: true, uniqueness: { scope: [:primary_identifier_fingerprint, :scanner_id, :ref, :pipeline_id, :project_id] }
validates :name, presence: true
validates :report_type, presence: true
validates :severity, presence: true, inclusion: { in: LEVELS.keys }
......
......@@ -10,6 +10,5 @@ module Vulnerabilities
validates :occurrence, presence: true
validates :identifier, presence: true
validates :identifier_id, uniqueness: { scope: [:occurrence_id] }
validates :occurrence_id, uniqueness: true, if: :primary
end
end
......@@ -8,6 +8,7 @@ module EE
approvers
vulnerability_feedback
license_management
feature_flag
].freeze
prepended do
......@@ -65,6 +66,11 @@ module EE
@subject.feature_available?(:license_management)
end
with_scope :subject
condition(:feature_flags_disabled) do
!@subject.feature_available?(:feature_flags)
end
rule { admin }.enable :change_repository_storage
rule { support_bot }.enable :guest_access
......@@ -99,6 +105,11 @@ module EE
enable :admin_board
enable :admin_vulnerability_feedback
enable :create_package
enable :read_feature_flag
enable :create_feature_flag
enable :update_feature_flag
enable :destroy_feature_flag
enable :admin_feature_flag
end
rule { can?(:public_access) }.enable :read_package
......@@ -117,6 +128,10 @@ module EE
prevent(*create_read_update_admin_destroy(:package))
end
rule { feature_flags_disabled }.policy do
prevent(*create_read_update_admin_destroy(:feature_flag))
end
rule { can?(:maintainer_access) }.policy do
enable :push_code_to_protected_branches
enable :admin_path_locks
......
......@@ -55,7 +55,7 @@ module EE
issuable.project.group&.feature_available?(:epics) &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
params '<group&epic | Epic URL>'
params '<&epic | group&epic | Epic URL>'
command :epic do |epic_param|
@updates[:epic] = extract_epic(epic_param)
end
......
......@@ -16,5 +16,11 @@ module Groups
def labels_as_hash(target)
super(target, group_id: group.id, only_group_labels: true)
end
def commands(noteable)
return [] unless noteable
QuickActions::InterpretService.new(nil, current_user).available_commands(noteable)
end
end
end
- if can?(current_user, :admin_feature_flag, @project)
%button.btn.btn-primary.btn-inverted.append-right-8{ type: 'button', data: { toggle: 'modal', target: '#configure-feature-flags-modal' } }>
= s_('FeatureFlags|Configure')
- if can?(current_user, :admin_feature_flag, @project)
#configure-feature-flags-modal.modal{ tabindex: -1,
role: 'dialog' }
.modal-dialog{ role: 'document' }
.modal-content
.modal-header
%h4.modal-title
= s_('FeatureFlags|Configure feature flags')
%button.close{ type: 'button', data: { dismiss: 'modal' }, aria: { label: _('Close') } }
%span{ "aria-hidden": true } &times;
.modal-body
%p
- client_libraries_url = help_page_path("user/project/operations/feature_flags", anchor: "client-libraries")
= s_('FeatureFlags|Install a %{docs_link_start}compatible client library%{docs_link_end} and specify the API URL, application name, and instance ID during the configuration setup.').html_safe % { docs_link_start: %Q{<a href="#{client_libraries_url}">}.html_safe,
docs_link_end: '</a>'.html_safe }
= link_to s_('FeatureFlags|More information'), help_page_path("user/project/operations/feature_flags")
.form-group
= label_tag :api_url, s_('FeatureFlags|API URL'), class: 'label-bold'
.input-group
= text_field_tag :api_url,
unleash_api_url(@project),
readonly: true,
class: "form-control js-select-on-focus"
%span.input-group-append
= clipboard_button(target: '#api_url',
title: _("Copy URL to clipboard"),
placement: "left",
container: '#configure-feature-flags-modal',
class: "input-group-text btn btn-default")
.form-group
= label_tag :instance_id, s_('FeatureFlags|Instance ID'), class: 'label-bold'
.input-group
= text_field_tag :instance_id,
unleash_api_instance_id(@project),
readonly: true,
class: "form-control js-select-on-focus"
%span.input-group-append
= clipboard_button(target: '#instance_id',
title: _("Copy ID to clipboard"),
placement: "left",
container: '#configure-feature-flags-modal',
class: "input-group-text btn btn-default")
.form-group
= label_tag :application_name, s_('FeatureFlags|Application name'), class: 'label-bold'
.input-group
= text_field_tag :application_name,
"production",
readonly: true,
class: "form-control js-select-on-focus"
%span.input-group-append
= clipboard_button(target: '#application_name',
title: _("Copy name to clipboard"),
placement: "left",
container: '#configure-feature-flags-modal',
class: "input-group-text btn btn-default")
- if can?(current_user, :destroy_feature_flag, @project)
.modal{ id: "delete-feature-flag-modal-#{feature_flag.id}",
tabindex: -1,
role: 'dialog' }
.modal-dialog{ role: 'document' }
.modal-content
.modal-header
%h4.modal-title.d-flex.mw-100
- truncated_feature_flag_name = capture do
%span.text-truncate.prepend-left-4.append-right-4= feature_flag.name
= s_('FeatureFlags|Delete %{feature_flag_name}?').html_safe % { feature_flag_name: truncated_feature_flag_name }
%button.close{ type: 'button', data: { dismiss: 'modal' }, aria: { label: _('Close') } }
%span{ "aria-hidden": true } &times;
.modal-body
%p
- monospace_feature_flag_name = capture do
%span.text-monospace= feature_flag.name
= s_('FeatureFlags|Feature flag %{feature_flag_name} will be removed. Are you sure?').html_safe % { feature_flag_name: monospace_feature_flag_name }
.modal-footer
%button{ type: 'button', data: { dismiss: 'modal' }, class: 'btn btn-default' } Cancel
= button_to 'Delete',
project_feature_flag_path(@project, feature_flag),
title: 'Delete',
method: :delete,
class: 'btn btn-remove'
.border-top
.row.empty-state
.col-12
.svg-content
= image_tag 'illustrations/feature_flag.svg'
.col-12
.text-content
%h4.text-center= s_('FeatureFlags|Get started with feature flags')
%p
= s_('FeatureFlags|Feature flags allow you to configure your code into different flavors by dynamically toggling certain functionality.')
= link_to 'More information', help_page_path("user/project/operations/feature_flags")
= render 'new_feature_flag_button'
= render 'configure_feature_flags_button'
#error_explanation
.alert.alert-danger
- @feature_flag.errors.full_messages.each do |message|
%p= message
- if @feature_flag.errors.any?
= render 'errors'
%fieldset
.row
.form-group.col-md-4
= f.label :name, class: 'label-bold', for: 'feature_flag_name' do
= s_('FeatureFlags|Name')
= f.text_field :name, class: "form-control", id: "feature_flag_name"
.row
.form-group.col-md-4
= f.label :description, class: 'label-bold', for: 'feature_flag_description' do
= s_('FeatureFlags|Description')
= f.text_area :description, class: "form-control", id: "feature_flag_description", rows: 4
.row
.form-group.col-md-1
= f.label :active, class: 'label-bold' do
= s_('FeatureFlags|Status')
.form-check
= f.check_box :active, id: 'feature_flag_status', class: 'form-check-input'
= f.label :active, for: 'feature_flag_status', class: 'form-check-label' do
= s_('FeatureFlags|Active')
.form-actions
- if @feature_flag.persisted?
= f.submit s_('FeatureFlags|Save changes'), class: "btn btn-success"
- else
= f.submit s_('FeatureFlags|Create feature flag'), class: "btn btn-success"
.float-right
= link_to _('Cancel'), project_feature_flags_path(@project), class: 'btn btn-cancel'
- if can?(current_user, :create_feature_flag, @project)
= link_to new_project_feature_flag_path(@project), class: 'btn btn-success' do
= s_('FeatureFlags|New Feature Flag')
.table-holder.border-top
.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-10{ role: 'columnheader' }= s_('FeatureFlags|Status')
.table-section.section-50{ role: 'columnheader' }= s_('FeatureFlags|Feature flag')
- @feature_flags.each do |feature_flag|
= render 'delete_feature_flag_modal', { feature_flag: feature_flag }
.gl-responsive-table-row{ role: 'row' }
.table-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: "rowheader" }= s_('FeatureFlags|Status')
.table-mobile-content
- if feature_flag.active?
%span.badge.badge-success
= s_('FeatureFlags|Active')
- else
%span.badge.badge-danger
= s_('FeatureFlags|Inactive')
.table-section.section-50{ role: 'gridcell' }
.table-mobile-header{ role: "rowheader" }= s_('FeatureFlags|Feature Flag')
.table-mobile-content.d-flex.flex-column
.text-monospace.text-truncate= feature_flag.name
.text-secondary.text-truncate= feature_flag.description
.table-section.section-40.table-button-footer{ role: 'gridcell' }
.table-action-buttons.btn-group
- if can?(current_user, :update_feature_flag, @project)
= link_to edit_project_feature_flag_path(@project, feature_flag),
class: 'btn btn-default has-tooltip',
type: 'button',
title: _('Edit') do
= sprite_icon('pencil', size: 16)
- if can?(current_user, :destroy_feature_flag, @project)
%button.btn.btn-danger.has-tooltip{ type: 'button',
data: { toggle: 'modal',
target: "#delete-feature-flag-modal-#{feature_flag.id}" },
title: _('Delete') }
= sprite_icon('remove', size: 16)
= paginate @feature_flags, theme: "gitlab"
- add_to_breadcrumbs "Feature Flags", project_feature_flags_path(@project)
- breadcrumb_title @feature_flag.name
- page_title s_('FeatureFlags|Edit Feature Flag')
%h3.page-title
= s_('FeatureFlags|Edit %{feature_flag_name}') % { feature_flag_name: @feature_flag.name }
%hr.clearfix
%div
= form_for [@project, @feature_flag], url: project_feature_flag_path(@project, @feature_flag), html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
- page_title _('Feature Flags')
= render 'configure_feature_flags_modal'
- if @feature_flags.empty?
= render 'empty_state'
- else
%h3.page-title.with-button
= _('Feature Flags')
.pull-right
= render 'configure_feature_flags_button'
= render 'new_feature_flag_button'
= render 'table'
- @breadcrumb_link = new_project_feature_flag_path(@project)
- add_to_breadcrumbs "Feature Flags", project_feature_flags_path(@project)
- breadcrumb_title s_('FeatureFlags|New')
- page_title s_('FeatureFlags|New Feature Flag')
%h3.page-title
= s_('FeatureFlags|New Feature Flag')
%hr.clearfix
%div
= form_for [@project, @feature_flag], url: project_feature_flags_path(@project), html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
- promotion_feature = 'promote_epics_sidebar_dismissed'
- if !@project.feature_available?(:epics) && show_promotions? && show_callout?(promotion_feature)
- if promote_feature?(:epic)
.block.js-epics-sidebar-callout.promotion-issue-sidebar{ data: { uid: promotion_feature } }
.sidebar-collapsed-icon{ data: { toggle: "dropdown", target: ".js-epics-sidebar-callout" } }
%span{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: _('Epic') }
......
---
title: Update DB model for security reports
merge_request:
author:
type: performance
---
title: Support short reference to epics from project entities
merge_request: 7475
author:
type: changed
---
title: Support autocomplete for commands in epics
merge_request: 7588
author:
type: added
---
title: Show promotion for epics on issues
merge_request: 7602
author:
type: fixed
---
title: Add Feature Flags MVC
merge_request: 7433
author:
type: added
class AddFeatureFlagsToProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def change
create_table :operations_feature_flags, id: :bigserial do |t|
t.integer :project_id, null: false
t.boolean :active, null: false
t.datetime_with_timezone :created_at, null: false
t.datetime_with_timezone :updated_at, null: false
t.string :name, null: false
t.text :description
t.foreign_key :projects, column: :project_id, on_delete: :cascade
t.index [:project_id, :name], unique: true
end
create_table :operations_feature_flags_clients, id: :bigserial do |t|
t.integer :project_id, null: false
t.string :token, null: false
t.index [:project_id, :token], unique: true
t.foreign_key :projects, column: :project_id, on_delete: :cascade
end
end
end
# frozen_string_literal: true
class RecreateVulnerabilityOccurrencesAndVulnerabilityOccurrenceIdentifiers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
drop_table :vulnerability_occurrence_identifiers
drop_table :vulnerability_occurrences
create_table :vulnerability_occurrences, id: :bigserial do |t|
t.timestamps_with_timezone null: false
t.integer :severity, null: false, limit: 2
t.integer :confidence, null: false, limit: 2
t.integer :report_type, null: false, limit: 2
t.integer :pipeline_id, null: false
t.foreign_key :ci_pipelines, column: :pipeline_id, on_delete: :cascade
t.references :project, null: false, foreign_key: { on_delete: :cascade }
t.bigint :scanner_id, null: false
t.foreign_key :vulnerability_scanners, column: :scanner_id, on_delete: :cascade
t.binary :project_fingerprint, null: false, limit: 20
t.binary :location_fingerprint, null: false, limit: 20
t.binary :primary_identifier_fingerprint, null: false, limit: 20
t.string :uuid, null: false, limit: 36
t.string :ref, null: false
t.string :name, null: false
t.string :metadata_version, null: false
t.text :raw_metadata, null: false
t.index :pipeline_id
t.index :scanner_id
t.index :uuid, unique: true
t.index [:project_id, :ref, :primary_identifier_fingerprint, :location_fingerprint, :pipeline_id, :scanner_id],
unique: true,
name: 'index_vulnerability_occurrences_on_unique_keys',
length: { location_fingerprint: 20, primary_identifier_fingerprint: 20 }
end
create_table :vulnerability_occurrence_identifiers, id: :bigserial do |t|
t.timestamps_with_timezone null: false
t.bigint :occurrence_id, null: false
t.foreign_key :vulnerability_occurrences, column: :occurrence_id, on_delete: :cascade
t.bigint :identifier_id, null: false
t.foreign_key :vulnerability_identifiers, column: :identifier_id, on_delete: :cascade
t.index :identifier_id
t.index [:occurrence_id, :identifier_id],
unique: true,
name: 'index_vulnerability_occurrence_identifiers_on_unique_keys'
end
end
def down
drop_table :vulnerability_occurrence_identifiers
drop_table :vulnerability_occurrences
create_table :vulnerability_occurrences, id: :bigserial do |t|
t.timestamps_with_timezone null: false
t.integer :severity, null: false, limit: 2
t.integer :confidence, null: false, limit: 2
t.integer :report_type, null: false, limit: 2
t.integer :pipeline_id, null: false
t.foreign_key :ci_pipelines, column: :pipeline_id, on_delete: :cascade
t.references :project, null: false, foreign_key: { on_delete: :cascade }
t.bigint :scanner_id, null: false
t.foreign_key :vulnerability_scanners, column: :scanner_id, on_delete: :cascade
t.binary :first_seen_in_commit_sha, null: false, limit: 20
t.binary :project_fingerprint, null: false, limit: 20
t.binary :location_fingerprint, null: false, limit: 20
t.binary :primary_identifier_fingerprint, null: false, limit: 20
t.string :ref, null: false
t.string :name, null: false
t.string :metadata_version, null: false
t.text :raw_metadata, null: false
t.index :pipeline_id
t.index :scanner_id
t.index [:project_id, :ref, :scanner_id, :primary_identifier_fingerprint, :location_fingerprint],
unique: true,
name: 'index_vulnerability_occurrences_on_unique_keys',
length: { location_fingerprint: 20, primary_identifier_fingerprint: 20 }
end
create_table :vulnerability_occurrence_identifiers, id: :bigserial do |t|
t.timestamps_with_timezone null: false
t.bigint :occurrence_id, null: false
t.foreign_key :vulnerability_occurrences, column: :occurrence_id, on_delete: :cascade
t.bigint :identifier_id, null: false
t.foreign_key :vulnerability_identifiers, column: :identifier_id, on_delete: :cascade
t.boolean :primary, null: false, default: false
t.index :identifier_id
t.index [:occurrence_id, :identifier_id],
unique: true,
name: 'index_vulnerability_occurrence_identifiers_on_unique_keys'
end
end
end
module API
class Unleash < Grape::API
include PaginationParams
namespace :feature_flags do
resource :unleash, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
params do
requires :project_id, type: String, desc: 'The ID of a project'
optional :instance_id, type: String, desc: 'The Instance ID of Unleash Client'
end
route_param :project_id do
before do
authorize_by_unleash_instance_id!
authorize_feature_flags_feature!
end
get do
# not supported yet
status :ok
end
get 'features' do
present project, with: ::EE::API::Entities::UnleashFeatures
end
post 'client/register' do
# not supported yet
status :ok
end
post 'client/metrics' do
# not supported yet
status :ok
end
end
end
end
helpers do
def project
@project ||= find_project(params[:project_id])
end
def unleash_instance_id
params[:instance_id] || env['HTTP_UNLEASH_INSTANCEID']
end
def authorize_by_unleash_instance_id!
unauthorized! unless Operations::FeatureFlagsClient
.find_for_project_and_token(project, unleash_instance_id)
end
def authorize_feature_flags_feature!
forbidden! unless project.feature_available?(:feature_flags)
end
end
end
end
......@@ -427,6 +427,27 @@ module EE
object.geo_node.missing_oauth_application?
end
end
class UnleashFeature < Grape::Entity
expose :name
expose :description, unless: ->(feature) { feature.description.nil? }
expose :active, as: :enabled
end
class UnleashFeatures < Grape::Entity
expose :version
expose :features, with: UnleashFeature
private
def version
1
end
def features
object.operations_feature_flags.ordered
end
end
end
end
end
......@@ -28,6 +28,15 @@ module EE
def maven_app_group_regex
maven_app_name_regex
end
def feature_flag_regex
/\A[a-z]([-_a-z0-9]*[a-z0-9])?\z/
end
def feature_flag_regex_message
"can contain only lowercase letters, digits, '_' and '-'. " \
"Must start with a letter, and cannot end with '-' or '_'"
end
end
end
end
# frozen_string_literal: tru
require 'spec_helper'
describe Groups::AutocompleteSourcesController do
let(:user) { create(:user) }
let(:group) { create(:group) }
let!(:epic) { create(:epic, group: group) }
before do
group.add_developer(user)
stub_licensed_features(epics: true)
sign_in(user)
end
context '#epics' do
it 'returns 200 status' do
get :epics, group_id: group
expect(response).to have_gitlab_http_status(200)
end
it 'returns the correct response' do
get :epics, group_id: group
expect(json_response).to be_an(Array)
expect(json_response.first).to include(
'id' => epic.id, 'iid' => epic.iid, 'title' => epic.title
)
end
end
context '#commands' do
it 'returns 200 status' do
get :commands, group_id: group, type: 'Epic', type_id: epic.iid
expect(response).to have_gitlab_http_status(200)
end
it 'returns the correct response' do
get :commands, group_id: group, type: 'Epic', type_id: epic.iid
expect(json_response).to be_an(Array)
expect(json_response).to include(
{ 'name' => 'close', 'aliases' => [], 'description' => 'Close this epic', 'params' => [] }
)
end
end
end
require 'spec_helper'
describe Projects::FeatureFlagsController do
include Gitlab::Routing
set(:user) { create(:user) }
set(:project) { create(:project) }
let(:feature_enabled) { true }
before do
project.add_developer(user)
sign_in(user)
stub_licensed_features(feature_flags: feature_enabled)
end
describe 'GET index' do
render_views
subject { get(:index, view_params) }
context 'when there is no feature flags' do
before do
subject
end
it 'shows an empty state with buttons' do
expect(response).to be_ok
expect(response).to render_template('_empty_state')
expect(response).to render_template('_configure_feature_flags_button')
expect(response).to render_template('_new_feature_flag_button')
end
end
context 'for a list of feature flags' do
let!(:feature_flags) { create_list(:operations_feature_flag, 50, project: project) }
before do
subject
end
it 'shows an list of feature flags with buttons' do
expect(response).to be_ok
expect(response).to render_template('_table')
expect(response).to render_template('_configure_feature_flags_button')
expect(response).to render_template('_new_feature_flag_button')
end
end
context 'when feature is not available' do
let(:feature_enabled) { false }
before do
subject
end
it 'shows not found' do
expect(subject).to have_gitlab_http_status(404)
end
end
end
describe 'GET new' do
render_views
subject { get(:new, view_params) }
it 'renders the form' do
subject
expect(response).to be_ok
expect(response).to render_template('new')
expect(response).to render_template('_form')
end
end
describe 'POST create' do
render_views
subject { post(:create, params) }
context 'when creating a new feature flag' do
let(:params) do
view_params.merge(operations_feature_flag: { name: 'my_feature_flag', active: true })
end
it 'creates and redirects to list' do
subject
expect(response).to redirect_to(project_feature_flags_path(project))
end
end
context 'when a feature flag already exists' do
let!(:feature_flag) { create(:operations_feature_flag, project: project, name: 'my_feature_flag') }
let(:params) do
view_params.merge(operations_feature_flag: { name: 'my_feature_flag', active: true })
end
it 'shows an error' do
subject
expect(response).to render_template('new')
expect(response).to render_template('_errors')
end
end
end
describe 'PUT update' do
let!(:feature_flag) { create(:operations_feature_flag, project: project, name: 'my_feature_flag') }
render_views
subject { post(:create, params) }
context 'when updating an existing feature flag' do
let(:params) do
view_params.merge(
id: feature_flag.id,
operations_feature_flag: { name: 'my_feature_flag_v2', active: true }
)
end
it 'updates and redirects to list' do
subject
expect(response).to redirect_to(project_feature_flags_path(project))
end
end
context 'when using existing name of the feature flag' do
let!(:other_feature_flag) { create(:operations_feature_flag, project: project, name: 'other_feature_flag') }
let(:params) do
view_params.merge(operations_feature_flag: { name: 'other_feature_flag', active: true })
end
it 'shows an error' do
subject
expect(response).to render_template('new')
expect(response).to render_template('_errors')
end
end
end
private
def view_params
{ namespace_id: project.namespace, project_id: project }
end
end
FactoryBot.define do
factory :operations_feature_flag, class: Operations::FeatureFlag do
sequence(:name) { |n| "feature_flag_#{n}" }
project
active true
end
end
FactoryBot.define do
factory :operations_feature_flags_client, class: Operations::FeatureFlagsClient do
project
end
end
......@@ -6,7 +6,7 @@ FactoryBot.define do
project
pipeline factory: :ci_pipeline
ref 'master'
first_seen_in_commit_sha '52d084cede3db8fafcd6b8ae382ddf1970da3b7f'
uuid 'a7342ca9-494e-457f-88e7-e65e145cc392'
project_fingerprint '4e5b6966dd100170b4b1ad599c7058cce91b57b4'
primary_identifier_fingerprint '4e5b6966dd100170b4b1ad599c7058cce91b57b4'
location_fingerprint '4e5b6966dd100170b4b1ad599c7058cce91b57b4'
......
......@@ -6,31 +6,50 @@ describe 'Referencing Epics', :js do
let(:epic) { create(:epic, group: group) }
let(:project) { create(:project, :public) }
let(:reference) { epic.to_reference(full: true) }
let(:full_reference) { epic.to_reference(full: true) }
context 'reference on an issue' do
let(:issue) { create(:issue, project: project, description: "Check #{reference}") }
before do
stub_licensed_features(epics: true)
sign_in(user)
end
context 'when referencing epics from the direct parent' do
let(:epic2) { create(:epic, group: group) }
let(:short_reference) { epic2.to_reference }
let(:text) { "Check #{full_reference} #{short_reference}" }
let(:child_project) { create(:project, :public, group: group) }
let(:issue) { create(:issue, project: child_project, description: text) }
it 'displays link to the reference' do
visit project_issue_path(child_project, issue)
page.within('.issuable-details .description') do
expect(page).to have_link(epic.to_reference, href: group_epic_path(group, epic))
expect(page).to have_link(short_reference, href: group_epic_path(group, epic2))
end
end
end
context 'when referencing an epic from another group' do
let(:text) { "Check #{full_reference}" }
let(:issue) { create(:issue, project: project, description: text) }
context 'when non group member displays the issue' do
context 'when referenced epic is in a public group' do
it 'displays link to the reference' do
visit project_issue_path(project, issue)
page.within('.issuable-details .description') do
expect(page).to have_link(reference, href: group_epic_path(group, epic))
expect(page).to have_link(full_reference, href: group_epic_path(group, epic))
end
end
end
context 'when referenced epic is in a private group' do
before do
group.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
group.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
it 'does not display link to the reference' do
......@@ -47,14 +66,15 @@ describe 'Referencing Epics', :js do
context 'when referenced epic is in a private group' do
before do
group.add_developer(user)
group.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
group.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
it 'displays link to the reference' do
visit project_issue_path(project, issue)
page.within('.issuable-details .description') do
expect(page).to have_link(reference, href: group_epic_path(group, epic))
expect(page).to have_link(full_reference, href: group_epic_path(group, epic))
end
end
end
end
......
require 'spec_helper'
describe 'Feature Flags', :js do
using RSpec::Parameterized::TableSyntax
invalid_input_table = proc do
'with space' | '' | 'Name can contain only'
'<script>' | '' | 'Name can contain only'
'x' * 100 | '' | 'Name is too long'
'some-name' | 'y' * 1001 | 'Description is too long'
end
let(:user) {create(:user)}
let(:project) {create(:project, namespace: user.namespace)}
before do
stub_licensed_features(feature_flags: true)
sign_in(user)
end
it 'shows empty state' do
visit(project_feature_flags_path(project))
expect_empty_state
end
context 'when creating a new feature flag' do
context 'and input is valid' do
where(:name, :description, :status) do
'my-active-flag' | 'a new flag' | true
'my-inactive-flag' | '' | false
end
with_them do
it 'adds the feature flag to the table' do
add_feature_flag(name, description, status)
expect_feature_flag(name, description, status)
expect(page).to have_selector '.flash-container', text: 'successfully created'
end
end
end
context 'and input is invalid' do
where(:name, :description, :error_message, &invalid_input_table)
with_them do
it 'displays an error message' do
add_feature_flag(name, description, false)
expect(page).to have_selector '.alert-danger', text: error_message
end
end
end
end
context 'when editing a feature flag' do
before do
add_feature_flag('feature-flag-to-edit', 'with some description', false)
end
context 'and input is valid' do
it 'updates the feature flag' do
name = 'new-name'
description = 'new description'
edit_feature_flag('feature-flag-to-edit', name, description, true)
expect_feature_flag(name, description, true)
expect(page).to have_selector '.flash-container', text: 'successfully updated'
end
end
context 'and input is invalid' do
where(:name, :description, :error_message, &invalid_input_table)
with_them do
it 'displays an error message' do
edit_feature_flag('feature-flag-to-edit', name, description, false)
expect(page).to have_selector '.alert-danger', text: error_message
end
end
end
end
context 'when deleting a feature flag' do
before do
add_feature_flag('feature-flag-to-delete', 'with some description', false)
end
context 'and no feature flags are left' do
it 'shows empty state' do
visit(project_feature_flags_path(project))
delete_feature_flag('feature-flag-to-delete')
expect_empty_state
end
end
context 'and there is a feature flag left' do
before do
add_feature_flag('another-feature-flag', '', true)
end
it 'shows feature flag table without deleted feature flag' do
visit(project_feature_flags_path(project))
delete_feature_flag('feature-flag-to-delete')
expect_feature_flag('another-feature-flag', '', true)
end
end
it 'does not delete if modal is cancelled' do
visit(project_feature_flags_path(project))
delete_feature_flag('feature-flag-to-delete', false)
expect_feature_flag('feature-flag-to-delete', 'with some description', false)
end
end
private
def add_feature_flag(name, description, status)
visit(new_project_feature_flag_path(project))
fill_in 'Name', with: name
fill_in 'Description', with: description
if status
check('Active')
else
uncheck('Active')
end
click_button 'Create feature flag'
end
def delete_feature_flag(name, confirm = true)
delete_button = find('.gl-responsive-table-row', text: name).find('.btn-danger[title="Delete"]')
delete_button.click
within '.modal' do
if confirm
click_button 'Delete'
else
click_button 'Cancel'
end
end
end
def edit_feature_flag(old_name, new_name, new_description, new_status)
visit(project_feature_flags_path(project))
edit_button = find('.gl-responsive-table-row', text: old_name).find('.btn-default[title="Edit"]')
edit_button.click
fill_in 'Name', with: new_name
fill_in 'Description', with: new_description
if new_status
check('Active')
else
uncheck('Active')
end
click_button 'Save changes'
end
def expect_empty_state
expect(page).to have_text 'Get started with feature flags'
expect(page).to have_selector('.btn-success', text: 'New Feature Flag')
expect(page).to have_selector('.btn-primary.btn-inverted', text: 'Configure')
end
def expect_feature_flag(name, description, status)
expect(current_path).to eq project_feature_flags_path(project)
expect(page).to have_selector '.table-section .badge', text: status ? 'Active' : 'Inactive'
expect(page).to have_selector '.table-section', text: name
expect(page).to have_selector '.table-section', text: description
end
end
......@@ -236,19 +236,12 @@ describe 'Promotions', :js do
end
describe 'for epics in issues sidebar', :js do
before do
allow(License).to receive(:current).and_return(nil)
stub_application_setting(check_namespace_plan: false)
project.add_maintainer(user)
sign_in(user)
end
shared_examples 'Epics promotion' do
it 'should appear on the page' do
visit project_issue_path(project, issue)
wait_for_requests
find('.js-epics-sidebar-callout .btn-link').click
click_epic_link
expect(find('.promotion-issue-sidebar-message')).to have_content 'Epics let you manage your portfolio of projects more efficiently'
end
......@@ -257,28 +250,28 @@ describe 'Promotions', :js do
visit project_issue_path(project, issue)
wait_for_requests
find('.js-epics-sidebar-callout .btn-link').click
click_epic_link
find('.js-epics-sidebar-callout .js-close-callout').click
expect(page).not_to have_selector('.js-epics-sidebar-callout')
expect(page).not_to have_selector('.promotion-issue-sidebar-message')
end
it 'should not appear on page after dismissal and reload' do
visit project_issue_path(project, issue)
wait_for_requests
find('.js-epics-sidebar-callout .btn-link').click
click_epic_link
find('.js-epics-sidebar-callout .js-close-callout').click
visit project_issue_path(project, issue)
expect(page).not_to have_selector('.js-epics-sidebar-callout')
expect(page).not_to have_selector('.promotion-issue-sidebar-message')
end
it 'should close dialog when clicking on X, but not dismiss it' do
visit project_issue_path(project, issue)
wait_for_requests
find('.js-epics-sidebar-callout .btn-link').click
click_epic_link
find('.js-epics-sidebar-callout .dropdown-menu-close').click
expect(page).to have_selector('.js-epics-sidebar-callout')
......@@ -286,6 +279,31 @@ describe 'Promotions', :js do
end
end
context 'gitlab.com' do
before do
stub_application_setting(check_namespace_plan: true)
allow(Gitlab).to receive(:com?) { true }
project.add_maintainer(user)
sign_in(user)
end
it_behaves_like 'Epics promotion'
end
context 'self hosted' do
before do
allow(License).to receive(:current).and_return(nil)
stub_application_setting(check_namespace_plan: false)
project.add_maintainer(user)
sign_in(user)
end
it_behaves_like 'Epics promotion'
end
end
describe 'for issue weight', :js do
before do
allow(License).to receive(:current).and_return(nil)
......@@ -439,4 +457,8 @@ describe 'Promotions', :js do
expect(page).not_to have_selector('#promote_advanced_search')
end
end
def click_epic_link
find('.js-epics-sidebar-callout .btn-link').click
end
end
{
"additionalProperties": false,
"properties": {
"features": {
"items": {
"$ref": "unleash_feature.json"
},
"minItems": 0,
"type": "array"
},
"version": {
"type": "integer"
}
},
"required": [
"version",
"features"
],
"type": "object"
}
{
"type": "object",
"additionalProperties": false,
"required": [
"name",
"enabled"
],
"properties": {
"name": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"description": {
"type": "string"
}
}
}
......@@ -15,7 +15,7 @@ describe ApplicationHelper do
let(:noteable_type) { Epic }
it 'returns paths for autocomplete_sources_controller' do
expect_autocomplete_data_sources(object, noteable_type, [:members, :labels, :epics])
expect_autocomplete_data_sources(object, noteable_type, [:members, :labels, :epics, :commands])
end
end
......
require 'spec_helper'
describe FeatureFlagsHelper do
set(:project) { create(:project) }
context '#unleash_api_url' do
subject { helper.unleash_api_url(project) }
it { is_expected.to end_with("/api/v4/feature_flags/unleash/#{project.id}") }
end
context '#unleash_api_instance_id' do
subject { helper.unleash_api_instance_id(project) }
it { is_expected.not_to be_empty }
end
end
......@@ -56,6 +56,8 @@ project:
- vulnerability_feedback
- vulnerability_identifiers
- vulnerability_scanners
- operations_feature_flags
- operations_feature_flags_client
- prometheus_alerts
- prometheus_alert_events
- software_license_policies
......
......@@ -9,4 +9,14 @@ describe Gitlab::Regex do
it { is_expected.to match('foo*Z') }
it { is_expected.not_to match('!!()()') }
end
describe '.feature_flag_regex' do
subject { described_class.feature_flag_regex }
it { is_expected.to match('foo') }
it { is_expected.to match('f_feature_flag') }
it { is_expected.not_to match('MY_FEATURE_FLAG') }
it { is_expected.not_to match('my feature flag') }
it { is_expected.not_to match('!!()()') }
end
end
......@@ -470,6 +470,10 @@ describe Epic do
describe '#to_reference' do
let(:group) { create(:group, path: 'group-a') }
let(:subgroup) { create(:group) }
let(:group_project) { create(:project, group: group) }
let(:subgroup_project) { create(:project, group: subgroup) }
let(:other_project) { create(:project) }
let(:epic) { create(:epic, iid: 1, group: group) }
context 'when nil argument' do
......@@ -478,23 +482,42 @@ describe Epic do
end
end
context 'when group argument equals epic group' do
context 'when from argument equals epic group' do
it 'returns epic id' do
expect(epic.to_reference(epic.group)).to eq('&1')
end
end
context 'when group argument differs from epic group' do
context 'when from argument is a group different from epic group' do
it 'returns complete path to the epic' do
expect(epic.to_reference(create(:group))).to eq('group-a&1')
end
end
context 'when from argument is a project under the epic group' do
it 'returns epic id' do
expect(epic.to_reference(group_project)).to eq('&1')
end
end
context 'when from argument is a project under the epic subgroup' do
it 'returns complete path to the epic' do
expect(epic.to_reference(subgroup_project)).to eq('group-a&1')
end
end
context 'when from argument is a project in another group' do
it 'returns complete path to the epic' do
expect(epic.to_reference(other_project)).to eq('group-a&1')
end
end
context 'when full is true' do
it 'returns complete path to the epic' do
expect(epic.to_reference(full: true)).to eq('group-a&1')
expect(epic.to_reference(epic.group, full: true)).to eq('group-a&1')
expect(epic.to_reference(group, full: true)).to eq('group-a&1')
expect(epic.to_reference(group_project, full: true)).to eq('group-a&1')
end
end
end
......
require 'spec_helper'
describe Operations::FeatureFlag do
subject { create(:operations_feature_flag) }
describe 'associations' do
it { is_expected.to belong_to(:project) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
end
end
require 'spec_helper'
describe Operations::FeatureFlagsClient do
subject { create(:operations_feature_flags_client) }
describe 'associations' do
it { is_expected.to belong_to(:project) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
end
describe '#token' do
it "ensures that token is always set" do
expect(subject.token).not_to be_empty
end
end
end
......@@ -1767,4 +1767,24 @@ describe Project do
.and_return(ref)
end
end
describe '#feature_flags_client_token' do
let(:project) { create(:project) }
subject { project.feature_flags_client_token }
context 'when there is no access token' do
it "creates a new one" do
is_expected.not_to be_empty
end
end
context 'when there is access token' do
let!(:instance) { create(:operations_feature_flags_client, project: project, token: 'token') }
it "provides an existing one" do
is_expected.to eq('token')
end
end
end
end
......@@ -14,21 +14,5 @@ describe Vulnerabilities::OccurrenceIdentifier do
it { is_expected.to validate_presence_of(:occurrence) }
it { is_expected.to validate_presence_of(:identifier) }
it { is_expected.to validate_uniqueness_of(:identifier_id).scoped_to(:occurrence_id) }
context 'when primary' do
before do
allow_any_instance_of(described_class).to receive(:primary).and_return(true)
end
it { is_expected.to validate_uniqueness_of(:occurrence_id) }
end
context 'when not primary' do
before do
allow_any_instance_of(described_class).to receive(:primary).and_return(false)
end
it { is_expected.not_to validate_uniqueness_of(:occurrence_id) }
end
end
end
......@@ -20,7 +20,7 @@ describe Vulnerabilities::Occurrence do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:pipeline) }
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to validate_presence_of(:first_seen_in_commit_sha) }
it { is_expected.to validate_presence_of(:uuid) }
it { is_expected.to validate_presence_of(:project_fingerprint) }
it { is_expected.to validate_presence_of(:primary_identifier_fingerprint) }
it { is_expected.to validate_presence_of(:location_fingerprint) }
......@@ -32,13 +32,11 @@ describe Vulnerabilities::Occurrence do
it { is_expected.to validate_inclusion_of(:severity).in_array(described_class::LEVELS.keys) }
it { is_expected.to validate_presence_of(:confidence) }
it { is_expected.to validate_inclusion_of(:confidence).in_array(described_class::LEVELS.keys) }
# Uniqueness validation doesn't work with binary columns. See TODO in class file
# it { is_expected.to validate_uniqueness_of(:ref).scoped_to(:primary_identifier_fingerprint, :location_fingerprint, :scanner_id, :ref, :project_id) }
end
context 'database uniqueness' do
let(:occurrence) { create(:vulnerabilities_occurrence) }
let(:new_occurrence) { occurrence.dup }
let(:new_occurrence) { occurrence.dup.tap { |o| o.uuid = SecureRandom.uuid } }
it "when all index attributes are identical" do
expect { new_occurrence.save! }.to raise_error(ActiveRecord::RecordNotUnique)
......@@ -52,6 +50,7 @@ describe Vulnerabilities::Occurrence do
:primary_identifier_fingerprint | -> { '005b6966dd100170b4b1ad599c7058cce91b57b4' }
:ref | -> { 'another_ref' }
:scanner | -> { create(:vulnerabilities_scanner) }
:pipeline | -> { create(:ci_pipeline) }
:project | -> { create(:project) }
end
......
require 'spec_helper'
describe API::Unleash do
set(:project) { create(:project) }
let(:project_id) { project.id }
let(:feature_enabled) { true }
let(:params) { }
let(:headers) { }
before do
stub_licensed_features(feature_flags: feature_enabled)
end
shared_examples 'authenticated request' do
context 'when using instance id' do
let(:client) { create(:operations_feature_flags_client, project: project) }
let(:params) { { instance_id: client.token } }
it 'responds with OK' do
subject
expect(response).to have_gitlab_http_status(200)
end
context 'when feature is not available' do
let(:feature_enabled) { false }
it 'responds with forbidden' do
subject
expect(response).to have_gitlab_http_status(403)
end
end
end
context 'when using header' do
let(:client) { create(:operations_feature_flags_client, project: project) }
let(:headers) { { "UNLEASH-INSTANCEID" => client.token }}
it 'responds with OK' do
subject
expect(response).to have_gitlab_http_status(200)
end
end
context 'when using bogus instance id' do
let(:params) { { instance_id: 'token' } }
it 'responds with unauthorized' do
subject
expect(response).to have_gitlab_http_status(401)
end
end
context 'when using not existing project' do
let(:project_id) { -5000 }
let(:params) { { instance_id: 'token' } }
it 'responds with unauthorized' do
subject
expect(response).to have_gitlab_http_status(401)
end
end
end
describe 'GET /feature_flags/unleash/:project_id/features' do
subject { get api("/feature_flags/unleash/#{project_id}/features"), params, headers }
it_behaves_like 'authenticated request'
context 'with a list of feature flag' do
let(:client) { create(:operations_feature_flags_client, project: project) }
let(:headers) { { "UNLEASH-INSTANCEID" => client.token }}
let!(:enable_feature_flag) { create(:operations_feature_flag, project: project, name: 'feature1', active: true) }
let!(:disabled_feature_flag) { create(:operations_feature_flag, project: project, name: 'feature2', active: false) }
it 'responds with a list' do
subject
expect(response).to have_gitlab_http_status(200)
expect(json_response['version']).to eq(1)
expect(json_response['features']).not_to be_empty
expect(json_response['features'].first['name']).to eq('feature1')
end
it 'matches json schema' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('unleash/unleash', dir: 'ee')
end
end
end
describe 'POST /feature_flags/unleash/:project_id/client/register' do
subject { post api("/feature_flags/unleash/#{project_id}/client/register"), params, headers }
it_behaves_like 'authenticated request'
end
describe 'POST /feature_flags/unleash/:project_id/client/metrics' do
subject { post api("/feature_flags/unleash/#{project_id}/client/metrics"), params, headers }
it_behaves_like 'authenticated request'
end
end
......@@ -6,8 +6,10 @@ describe Groups::AutocompleteService do
let(:user) { create(:user) }
let!(:epic) { create(:epic, group: group, author: user) }
subject { described_class.new(group, user) }
before do
create(:group_member, group: group, user: user)
group.add_developer(user)
end
def expect_labels_to_equal(labels, expected_labels)
......@@ -22,8 +24,8 @@ describe Groups::AutocompleteService do
let!(:parent_group_label) { create(:group_label, group: group.parent, group_id: group.id) }
it 'returns labels from own group and ancestor groups' do
service = described_class.new(group, user)
results = service.labels_as_hash(nil)
results = subject.labels_as_hash(nil)
expected_labels = [label1, label2, parent_group_label]
expect_labels_to_equal(results, expected_labels)
......@@ -35,8 +37,7 @@ describe Groups::AutocompleteService do
end
it 'marks already assigned as set' do
service = described_class.new(group, user)
results = service.labels_as_hash(epic)
results = subject.labels_as_hash(epic)
expected_labels = [label1, label2, parent_group_label]
expect_labels_to_equal(results, expected_labels)
......@@ -56,16 +57,29 @@ describe Groups::AutocompleteService do
describe '#epics' do
it 'returns nothing if not allowed' do
allow(Ability).to receive(:allowed?).with(user, :read_epic, group).and_return(false)
service = described_class.new(group, user)
expect(service.epics).to eq([])
expect(subject.epics).to eq([])
end
it 'returns epics from group' do
allow(Ability).to receive(:allowed?).with(user, :read_epic, group).and_return(true)
service = described_class.new(group, user)
expect(service.epics).to contain_exactly(epic)
expect(subject.epics).to contain_exactly(epic)
end
end
describe '#commands' do
context 'when target is an epic' do
before do
stub_licensed_features(epics: true)
end
it 'returns available commands' do
expect(subject.commands(epic).map { |c| c[:name] })
.to match_array(
[:todo, :unsubscribe, :award, :shrug, :tableflip, :cc, :title, :close]
)
end
end
end
end
......@@ -167,6 +167,7 @@ module API
mount ::EE::API::Boards
mount ::EE::API::GroupBoards
mount ::API::Unleash
mount ::API::EpicIssues
mount ::API::Epics
mount ::API::Geo
......
......@@ -11,6 +11,12 @@ module Banzai
def self.object_class
Epic
end
private
def group
context[:group] || context[:project]&.group
end
end
end
end
......@@ -2267,6 +2267,9 @@ msgstr ""
msgid "Copy incoming email address to clipboard"
msgstr ""
msgid "Copy name to clipboard"
msgstr ""
msgid "Copy reference to clipboard"
msgstr ""
......@@ -3193,6 +3196,81 @@ msgstr ""
msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
msgstr ""
msgid "Feature Flags"
msgstr ""
msgid "FeatureFlags|API URL"
msgstr ""
msgid "FeatureFlags|Active"
msgstr ""
msgid "FeatureFlags|Application name"
msgstr ""
msgid "FeatureFlags|Configure"
msgstr ""
msgid "FeatureFlags|Configure feature flags"
msgstr ""
msgid "FeatureFlags|Create feature flag"
msgstr ""
msgid "FeatureFlags|Delete %{feature_flag_name}?"
msgstr ""
msgid "FeatureFlags|Description"
msgstr ""
msgid "FeatureFlags|Edit %{feature_flag_name}"
msgstr ""
msgid "FeatureFlags|Edit Feature Flag"
msgstr ""
msgid "FeatureFlags|Feature Flag"
msgstr ""
msgid "FeatureFlags|Feature flag"
msgstr ""
msgid "FeatureFlags|Feature flag %{feature_flag_name} will be removed. Are you sure?"
msgstr ""
msgid "FeatureFlags|Feature flags allow you to configure your code into different flavors by dynamically toggling certain functionality."
msgstr ""
msgid "FeatureFlags|Get started with feature flags"
msgstr ""
msgid "FeatureFlags|Inactive"
msgstr ""
msgid "FeatureFlags|Install a %{docs_link_start}compatible client library%{docs_link_end} and specify the API URL, application name, and instance ID during the configuration setup."
msgstr ""
msgid "FeatureFlags|Instance ID"
msgstr ""
msgid "FeatureFlags|More information"
msgstr ""
msgid "FeatureFlags|Name"
msgstr ""
msgid "FeatureFlags|New"
msgstr ""
msgid "FeatureFlags|New Feature Flag"
msgstr ""
msgid "FeatureFlags|Save changes"
msgstr ""
msgid "FeatureFlags|Status"
msgstr ""
msgid "Feb"
msgstr ""
......
......@@ -331,11 +331,12 @@ describe CacheMarkdownField do
end
context 'with a project' do
let(:thing) { thing_subclass(:project).new(foo: markdown, foo_html: html, project: :project_value) }
let(:project) { create(:project, group: create(:group)) }
let(:thing) { thing_subclass(:project).new(foo: markdown, foo_html: html, project: project) }
it 'sets the project in the context' do
is_expected.to have_key(:project)
expect(context[:project]).to eq(:project_value)
expect(context[:project]).to eq(project)
end
it 'invalidates the cache when project changes' do
......
......@@ -641,6 +641,7 @@
lodash "^4.17.10"
to-fast-properties "^2.0.0"
<<<<<<< HEAD
"@babel/types@^7.1.2":
version "7.1.2"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.1.2.tgz#183e7952cf6691628afdc2e2b90d03240bac80c0"
......@@ -654,6 +655,15 @@
version "1.29.0"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.29.0.tgz#03b65b513f9099bbda6ecf94d673a2952f8c6c70"
integrity sha512-sCl6nP3ph36+8P3nrw9VanAR648rgOUEBlEoLPHkhKm79xB1dUkXGBtI0uaSJVgbJx40M1/Ts8HSdMv+PF3EIg==
=======
"@gitlab-org/gitlab-svgs@^1.29.0":
version "1.30.0"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.30.0.tgz#72dca2fb67bafb3c975a322dc6406aaa29aed86c"
"@gitlab-org/gitlab-svgs@^1.30.0":
version "1.31.0"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.31.0.tgz#495b074669f93af40e34f9978ce887773dea470a"
>>>>>>> origin/master
"@gitlab-org/gitlab-ui@^1.8.0":
version "1.8.0"
......
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