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,9 +211,14 @@ 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
current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
project &&
current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
available_labels.any?
end
command :label do |labels_param|
......@@ -287,7 +292,8 @@ module QuickActions
end
params '#issue | !merge_request'
condition do
current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
[MergeRequest, Issue].include?(issuable.class) &&
current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
end
parse_params do |issuable_param|
extract_references(issuable_param, :issue).first ||
......@@ -444,7 +450,8 @@ module QuickActions
end
params '<time(1h30m | -1h30m)> <date(YYYY-MM-DD)>'
condition do
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
issuable.is_a?(TimeTrackable) &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end
parse_params do |raw_time_date|
Gitlab::QuickActions::SpendTimeAndDateSeparator.new(raw_time_date).execute
......@@ -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
# GitLab quick actions
Quick actions are textual shortcuts for common actions on issues, epics, merge requests,
Quick actions are textual shortcuts for common actions on issues, epics, merge requests,
and commits that are usually done by clicking buttons or dropdowns in GitLab's UI.
You can enter these commands while creating a new issue or merge request, or
in comments of issues, epics, merge requests, and commits. Each command should be
in comments of issues, epics, merge requests, and commits. Each command should be
on a separate line in order to be properly detected and executed. Once executed,
the commands are removed from the text body and not visible to anyone else.
......@@ -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 | ✓ |
......@@ -70,7 +70,7 @@ The following quick actions are applicable for epics threads and description:
|:---------------------------|:----------------------------------------|
| `/tableflip <Comment>` | Append the comment with `(╯°□°)╯︵ ┻━┻` |
| `/shrug <Comment>` | Append the comment with `¯\_(ツ)_/¯` |
| `/todo` | Add a todo |
| `/todo` | Add a todo |
| `/done` | Mark todo as done |
| `/subscribe` | Subscribe |
| `/unsubscribe` | Unsubscribe |
......@@ -80,4 +80,4 @@ The following quick actions are applicable for epics threads and description:
| `/award :emoji:` | Toggle emoji award |
| `/label ~label1 ~label2` | Add label(s) |
| `/unlabel ~label1 ~label2` | Remove all or specific label(s) |
| `/relabel ~label1 ~label2` | Replace label |
\ No newline at end of file
| `/relabel ~label1 ~label2` | Replace label |
......@@ -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,55 +6,75 @@ 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 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)
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) }
page.within('.issuable-details .description') do
expect(page).to have_link(reference, href: group_epic_path(group, epic))
end
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 referenced epic is in a private group' do
before do
group.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
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(full_reference, href: group_epic_path(group, epic))
end
end
end
it 'does not display link to the reference' do
visit project_issue_path(project, issue)
context 'when referenced epic is in a private group' do
before do
group.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
it 'does not display link to the reference' do
visit project_issue_path(project, issue)
page.within('.issuable-details .description') do
expect(page).not_to have_link
page.within('.issuable-details .description') do
expect(page).not_to have_link
end
end
end
end
end
context 'when a group member displays the issue' 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)
end
context 'when a group member displays the issue' do
context 'when referenced epic is in a private group' do
before do
group.add_developer(user)
group.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
it 'displays link to the reference' do
visit project_issue_path(project, issue)
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))
page.within('.issuable-details .description') do
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,53 +236,71 @@ 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)
shared_examples 'Epics promotion' do
it 'should appear on the page' do
visit project_issue_path(project, issue)
wait_for_requests
project.add_maintainer(user)
sign_in(user)
end
click_epic_link
it 'should appear on the page' do
visit project_issue_path(project, issue)
wait_for_requests
expect(find('.promotion-issue-sidebar-message')).to have_content 'Epics let you manage your portfolio of projects more efficiently'
end
find('.js-epics-sidebar-callout .btn-link').click
it 'should be removed after dismissal' do
visit project_issue_path(project, issue)
wait_for_requests
expect(find('.promotion-issue-sidebar-message')).to have_content 'Epics let you manage your portfolio of projects more efficiently'
end
click_epic_link
find('.js-epics-sidebar-callout .js-close-callout').click
it 'should be removed after dismissal' do
visit project_issue_path(project, issue)
wait_for_requests
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
find('.js-epics-sidebar-callout .js-close-callout').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
click_epic_link
find('.js-epics-sidebar-callout .dropdown-menu-close').click
expect(page).to have_selector('.js-epics-sidebar-callout')
expect(page).to have_selector('.promotion-issue-sidebar-message', visible: false)
end
end
it 'should not appear on page after dismissal and reload' do
visit project_issue_path(project, issue)
wait_for_requests
context 'gitlab.com' do
before do
stub_application_setting(check_namespace_plan: true)
allow(Gitlab).to receive(:com?) { true }
find('.js-epics-sidebar-callout .btn-link').click
find('.js-epics-sidebar-callout .js-close-callout').click
visit project_issue_path(project, issue)
project.add_maintainer(user)
sign_in(user)
end
expect(page).not_to have_selector('.js-epics-sidebar-callout')
it_behaves_like 'Epics promotion'
end
it 'should close dialog when clicking on X, but not dismiss it' do
visit project_issue_path(project, issue)
wait_for_requests
context 'self hosted' do
before do
allow(License).to receive(:current).and_return(nil)
stub_application_setting(check_namespace_plan: false)
find('.js-epics-sidebar-callout .btn-link').click
find('.js-epics-sidebar-callout .dropdown-menu-close').click
project.add_maintainer(user)
sign_in(user)
end
expect(page).to have_selector('.js-epics-sidebar-callout')
expect(page).to have_selector('.promotion-issue-sidebar-message', visible: false)
it_behaves_like 'Epics promotion'
end
end
......@@ -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