From 00eb49d0a6debc47e4336143bb8646b225a72ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Tue, 26 Jun 2018 19:23:47 +0200 Subject: [PATCH 01/57] Add simple data models --- app/models/project.rb | 3 ++ app/models/project_feature_flag.rb | 12 +++++++ .../project_feature_flag_access_token.rb | 10 ++++++ ...626171125_add_feature_flags_to_projects.rb | 31 +++++++++++++++++++ lib/gitlab/regex.rb | 9 ++++++ 5 files changed, 65 insertions(+) create mode 100644 app/models/project_feature_flag.rb create mode 100644 app/models/project_feature_flag_access_token.rb create mode 100644 db/migrate/20180626171125_add_feature_flags_to_projects.rb diff --git a/app/models/project.rb b/app/models/project.rb index 9eee28ad0f455d..9e5b6cbc226c50 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -266,6 +266,9 @@ class Project < ActiveRecord::Base has_many :project_deploy_tokens has_many :deploy_tokens, through: :project_deploy_tokens + has_many :project_feature_flags + has_many :project_feature_flags_access_tokens + has_one :auto_devops, class_name: 'ProjectAutoDevops' has_many :custom_attributes, class_name: 'ProjectCustomAttribute' diff --git a/app/models/project_feature_flag.rb b/app/models/project_feature_flag.rb new file mode 100644 index 00000000000000..11681f555de3f4 --- /dev/null +++ b/app/models/project_feature_flag.rb @@ -0,0 +1,12 @@ +class ProjectFeatureFlag < ActiveRecord::Base + 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 + } +end diff --git a/app/models/project_feature_flag_access_token.rb b/app/models/project_feature_flag_access_token.rb new file mode 100644 index 00000000000000..986685e9ee1c1a --- /dev/null +++ b/app/models/project_feature_flag_access_token.rb @@ -0,0 +1,10 @@ +class ProjectFeatureFlagAccessToken < ActiveRecord::Base + include TokenAuthenticatable + + belongs_to :project + + validates :project, presence: true + validates :token, presence: true + + add_authentication_token_field :token +end diff --git a/db/migrate/20180626171125_add_feature_flags_to_projects.rb b/db/migrate/20180626171125_add_feature_flags_to_projects.rb new file mode 100644 index 00000000000000..9fe3d8433e48a4 --- /dev/null +++ b/db/migrate/20180626171125_add_feature_flags_to_projects.rb @@ -0,0 +1,31 @@ +class AddFeatureFlagsToProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def change + create_table :project_feature_flags do |t| + t.integer :project_id, 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.boolean :active, null: false + + t.foreign_key :projects, column: :project_id, on_delete: :cascade + + t.index [:project_id, :name], unique: true + end + + create_table :project_feature_flag_access_tokens do |t| + t.integer :project_id, null: false + t.string :token, null: false + + t.index [:project_id, :token], unique: true, name: :project_feature_flag_access_token + + t.foreign_key :projects, column: :project_id, on_delete: :cascade + end + end +end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 2a1231f5821d2f..f5c81bbc20044f 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -71,6 +71,15 @@ def environment_slug_regex_message "Must start with a letter, and cannot end with '-'" 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 '-'" + end + def build_trace_section_regex @build_trace_section_regexp ||= /section_((?:start)|(?:end)):(\d+):([a-zA-Z0-9_.-]+)\r\033\[0K/.freeze end -- GitLab From 9aba3a901fffb56d15af725a993590af1b467210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Thu, 21 Jun 2018 18:42:41 +0200 Subject: [PATCH 02/57] Add Unleash API compatible interface --- lib/api/api.rb | 1 + lib/api/entities.rb | 34 ++++++++++++++++++++++++++++++++++ lib/api/unleash.rb | 25 +++++++++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 lib/api/unleash.rb diff --git a/lib/api/api.rb b/lib/api/api.rb index c2208df334b8b3..5ce0733b38b134 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -158,6 +158,7 @@ class API < Grape::API mount ::API::Templates mount ::API::Todos mount ::API::Triggers + mount ::API::Unleash mount ::API::Users mount ::API::Variables mount ::API::Version diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 3a68da2a2bb906..d2835d8e926780 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1515,6 +1515,40 @@ class ResourceLabelEvent < Grape::Entity end expose :label, using: Entities::LabelBasic expose :action + + class UnleashIssue < Grape::Entity + expose :name + expose :title, as: :description + expose :enabled + expose :strategies + + private + + def name + "\##{object.iid}" + end + + def enabled + true + end + + def strategies + [{ name: 'default' }] + end + end + + class UnleashFeatureResponse < Grape::Entity + expose :version + expose :features, with: UnleashIssue + + private + + def version + 1 + end + + def features + end end end end diff --git a/lib/api/unleash.rb b/lib/api/unleash.rb new file mode 100644 index 00000000000000..249dff336749bf --- /dev/null +++ b/lib/api/unleash.rb @@ -0,0 +1,25 @@ +module API + class Unleash < Grape::API + include PaginationParams + + #before { authenticate! } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + get ':id/unleash/features' do + issues = IssuesFinder.new(current_user, project_id: user_project.id, label_name: 'rollout') + present issues, with: Entities::UnleashFeatureResponse + end + + post ':id/unleash/client/register' do + status :ok + end + + post ':id/unleash/client/metrics' do + status :ok + end + end + end +end -- GitLab From 23e06634c3604ac484b486c73037fcd9fb0eda3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Tue, 26 Jun 2018 20:53:47 +0200 Subject: [PATCH 03/57] Add feature flags backend controls --- .../projects/feature_flags_controller.rb | 57 +++++++++++++++++++ app/helpers/projects_helper.rb | 3 +- ... => project_feature_flags_access_token.rb} | 4 +- app/policies/project_policy.rb | 6 ++ .../layouts/nav/sidebar/_project.html.haml | 14 +++-- .../projects/feature_flags/_form.html.haml | 25 ++++++++ .../projects/feature_flags/_list.html.haml | 17 ++++++ .../projects/feature_flags/_use.html.haml | 23 ++++++++ .../projects/feature_flags/edit.html.haml | 11 ++++ .../projects/feature_flags/index.html.haml | 16 ++++++ .../projects/feature_flags/new.html.haml | 12 ++++ config/routes/project.rb | 2 + ...626171125_add_feature_flags_to_projects.rb | 2 +- db/schema.rb | 20 +++++++ lib/api/entities.rb | 26 ++------- lib/api/unleash.rb | 38 +++++++++---- lib/gitlab/regex.rb | 4 +- 17 files changed, 240 insertions(+), 40 deletions(-) create mode 100644 app/controllers/projects/feature_flags_controller.rb rename app/models/{project_feature_flag_access_token.rb => project_feature_flags_access_token.rb} (64%) create mode 100644 app/views/projects/feature_flags/_form.html.haml create mode 100644 app/views/projects/feature_flags/_list.html.haml create mode 100644 app/views/projects/feature_flags/_use.html.haml create mode 100644 app/views/projects/feature_flags/edit.html.haml create mode 100644 app/views/projects/feature_flags/index.html.haml create mode 100644 app/views/projects/feature_flags/new.html.haml diff --git a/app/controllers/projects/feature_flags_controller.rb b/app/controllers/projects/feature_flags_controller.rb new file mode 100644 index 00000000000000..60d0c4668a6c09 --- /dev/null +++ b/app/controllers/projects/feature_flags_controller.rb @@ -0,0 +1,57 @@ +class Projects::FeatureFlagsController < Projects::ApplicationController + respond_to :html + + before_action :authorize_read_feature_flags! + before_action :authorize_update_feature_flags!, only: [:edit, :update] + before_action :authorize_admin_feature_flags!, only: [:destroy] + + before_action :feature_flag, only: [:edit, :update, :destroy] + + def index + @feature_flags = project.project_feature_flags + @unleash_instanceid = project.project_feature_flags_access_tokens.first&.token || project.project_feature_flags_access_tokens.create!.token + end + + def new + @feature_flag = project.project_feature_flags.new + end + + def create + @feature_flag = project.project_feature_flags.create(create_params) + + if @feature_flag.persisted? + flash[:notice] = 'Feature flag was successfully created.' + redirect_to project_feature_flags_path(@project) + else + render :new + end + end + + def edit + end + + def update + if feature_flag.update(update_params) + flash[:notice] = 'Feature flag was successfully updated.' + redirect_to project_feature_flags_path(@project) + else + render :edit + end + end + + protected + + def feature_flag + @feature_flag ||= project.project_feature_flags.find(params[:id]) + end + + def create_params + params.require(:project_feature_flag) + .permit(:name, :description, :active) + end + + def update_params + params.require(:project_feature_flag) + .permit(:description, :active) + end +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 4562540f3b1698..4076b5c29f0cbe 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -284,7 +284,7 @@ def get_project_nav_tabs(project, current_user) nav_tabs << :pipelines end - if can?(current_user, :read_environment, project) || can?(current_user, :read_cluster, project) + if can?(current_user, :read_environment, project) || can?(current_user, :read_cluster, project) || can?(current_user, :read_feature_flags, project) nav_tabs << :operations end @@ -304,6 +304,7 @@ def get_project_nav_tabs(project, current_user) def tab_ability_map { environments: :read_environment, + feature_flags: :read_feature_flags, milestones: :read_milestone, snippets: :read_project_snippet, settings: :admin_project, diff --git a/app/models/project_feature_flag_access_token.rb b/app/models/project_feature_flags_access_token.rb similarity index 64% rename from app/models/project_feature_flag_access_token.rb rename to app/models/project_feature_flags_access_token.rb index 986685e9ee1c1a..d31c5713c3122c 100644 --- a/app/models/project_feature_flag_access_token.rb +++ b/app/models/project_feature_flags_access_token.rb @@ -1,4 +1,4 @@ -class ProjectFeatureFlagAccessToken < ActiveRecord::Base +class ProjectFeatureFlagsAccessToken < ActiveRecord::Base include TokenAuthenticatable belongs_to :project @@ -7,4 +7,6 @@ class ProjectFeatureFlagAccessToken < ActiveRecord::Base validates :token, presence: true add_authentication_token_field :token + + before_validation :ensure_token! end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 7c1450c20cdb4f..eb7c40d1338a21 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -15,6 +15,7 @@ class ProjectPolicy < BasePolicy note pipeline pipeline_schedule + feature_flags build trigger environment @@ -234,6 +235,7 @@ class ProjectPolicy < BasePolicy enable :update_container_image enable :create_environment enable :create_deployment + enable :read_feature_flags end rule { can?(:maintainer_access) }.policy do @@ -258,6 +260,9 @@ class ProjectPolicy < BasePolicy enable :read_cluster enable :create_cluster enable :create_environment_terminal + enable :create_feature_flags + enable :update_feature_flags + enable :admin_feature_flags end rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror @@ -306,6 +311,7 @@ class ProjectPolicy < BasePolicy prevent(*create_read_update_admin_destroy(:build)) prevent(*create_read_update_admin_destroy(:pipeline_schedule)) prevent(*create_read_update_admin_destroy(:environment)) + prevent(*create_read_update_admin_destroy(:feature_flags)) prevent(*create_read_update_admin_destroy(:cluster)) prevent(*create_read_update_admin_destroy(:deployment)) end diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 23095d2cb10ab8..9c712faa9b879f 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -195,16 +195,16 @@ = _('Charts') - if project_nav_tab? :operations - = nav_link(controller: [:environments, :clusters, :user, :gcp]) do - = link_to metrics_project_environments_path(@project), class: 'shortcuts-operations' do + = nav_link(controller: [:environments, :clusters, :user, :gcp, :feature_flags]) do + = link_to project_environments_path(@project), class: 'shortcuts-operations' do .nav-icon-container = sprite_icon('cloud-gear') %span.nav-item-name = _('Operations') %ul.sidebar-sub-level-items - = nav_link(controller: [:environments, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do - = link_to metrics_project_environments_path(@project) do + = nav_link(controller: [:environments, :clusters, :user, :gcp, :feature_flags], html_options: { class: "fly-out-top-item" } ) do + = link_to project_environments_path(@project) do %strong.fly-out-top-item-name = _('Operations') %li.divider.fly-out-top-item @@ -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 diff --git a/app/views/projects/feature_flags/_form.html.haml b/app/views/projects/feature_flags/_form.html.haml new file mode 100644 index 00000000000000..38f35a959858d3 --- /dev/null +++ b/app/views/projects/feature_flags/_form.html.haml @@ -0,0 +1,25 @@ +- if @feature_flag.errors.any? + #error_explanation + .alert.alert-danger + - @feature_flag.errors.full_messages.each do |msg| + %p= msg + +- unless @feature_flag.persisted? + .form-group.row + = f.label :name, class: 'col-form-label col-sm-2' do + Name + .col-sm-10 + = f.text_field :name, required: true, autocomplete: 'off', class: 'form-control', disabled: @feature_flag.persisted? + +.form-group.row + = f.label :active, class: 'col-form-label col-sm-2' do + Active + .col-sm-10 + = f.check_box :active + +.form-group.row + = f.label :description, class: 'col-form-label col-sm-2' do + Description + .col-sm-10 + = f.text_area :description, rows: 5, class: 'form-control' + %span.help-inline Write a description of your feature flag diff --git a/app/views/projects/feature_flags/_list.html.haml b/app/views/projects/feature_flags/_list.html.haml new file mode 100644 index 00000000000000..5ebc16589b1fa2 --- /dev/null +++ b/app/views/projects/feature_flags/_list.html.haml @@ -0,0 +1,17 @@ +.card + .card-header + Feature flags (#{@feature_flags.count}) + %ul.content-list.pages-domain-list + - @feature_flags.each do |feature_flag| + %li.pages-domain-list-item.unstyled + = feature_flag.name + + %div.controls.d-none.d-md-block + = link_to 'Edit', edit_project_feature_flag_path(@project, feature_flag), class: "btn btn-sm btn-grouped" + = link_to 'Remove', project_feature_flag_path(@project, feature_flag), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" + + %p + - if feature_flag.active? + %span.badge.badge-success Enabled + - else + %span.badge.badge-danger Disabled \ No newline at end of file diff --git a/app/views/projects/feature_flags/_use.html.haml b/app/views/projects/feature_flags/_use.html.haml new file mode 100644 index 00000000000000..a77244747a7673 --- /dev/null +++ b/app/views/projects/feature_flags/_use.html.haml @@ -0,0 +1,23 @@ +.card.bg-info + .card-header + Configure feature flags + .card-body + %p + Learn how to enable feature flags for your application. + + %ol + %li + = _("Install a compatible with client library") + = (_("(checkout the %{link} for information on how to install it).") % { link: "here" }).html_safe + %li + = _("Specify the following URL during for the library configuration setup:") + %code#coordinator_address= "#{root_url(only_path: false)}api/v4/unleash" + %li + = _("Use the following application name:") + %code#registration_token= @project.id + %li + = _("Use the following application name:") + %code#registration_token= @unleash_instanceid + %li + = _("You can also see all features online:") + %code#registration_token= "#{root_url(only_path: false)}api/v4/unleash/features?appname=#{@project.id}&instanceid=#{@unleash_instanceid}" \ No newline at end of file diff --git a/app/views/projects/feature_flags/edit.html.haml b/app/views/projects/feature_flags/edit.html.haml new file mode 100644 index 00000000000000..10a7b0e505a68f --- /dev/null +++ b/app/views/projects/feature_flags/edit.html.haml @@ -0,0 +1,11 @@ +- add_to_breadcrumbs "Feature Flags", project_feature_flags_path(@project) +- breadcrumb_title @feature_flag.name +- page_title @feature_flag.name +%h3.page-title + = @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 } + .form-actions + = f.submit 'Save Changes', class: "btn btn-save" diff --git a/app/views/projects/feature_flags/index.html.haml b/app/views/projects/feature_flags/index.html.haml new file mode 100644 index 00000000000000..20b5d680e01ff1 --- /dev/null +++ b/app/views/projects/feature_flags/index.html.haml @@ -0,0 +1,16 @@ +- page_title 'Feature Flags' + +%h3.page-title.with-button + Feature Flags + + - if can?(current_user, :create_feature_flags, @project) + = link_to new_project_feature_flag_path(@project), class: 'btn btn-new float-right', title: 'New Feature Flag' do + New Feature Flag + +%p.light + With GitLab Feature Flags + +%hr.clearfix + += render 'use' += render 'list' diff --git a/app/views/projects/feature_flags/new.html.haml b/app/views/projects/feature_flags/new.html.haml new file mode 100644 index 00000000000000..dac8fc54231252 --- /dev/null +++ b/app/views/projects/feature_flags/new.html.haml @@ -0,0 +1,12 @@ +- add_to_breadcrumbs "Feature Flags", project_feature_flags_path(@project) +- page_title 'New Feature Flags' +%h3.page-title + New Feature Flags +%hr.clearfix +%div + = form_for [@project, @feature_flag], url: project_feature_flags_path(@project), html: { class: 'fieldset-form' } do |f| + = render 'form', { f: f } + .form-actions + = f.submit 'Create New Feature Flag', class: "btn btn-save" + .float-right + = link_to _('Cancel'), project_feature_flags_path(@project), class: 'btn btn-cancel' diff --git a/config/routes/project.rb b/config/routes/project.rb index c0c1775f8d37d5..d858bf0dc66ffe 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -351,6 +351,8 @@ namespace :ci do resource :lint, only: [:show, :create] end + + resources :feature_flags end draw :legacy_builds diff --git a/db/migrate/20180626171125_add_feature_flags_to_projects.rb b/db/migrate/20180626171125_add_feature_flags_to_projects.rb index 9fe3d8433e48a4..9ca2d3a25803d0 100644 --- a/db/migrate/20180626171125_add_feature_flags_to_projects.rb +++ b/db/migrate/20180626171125_add_feature_flags_to_projects.rb @@ -19,7 +19,7 @@ def change t.index [:project_id, :name], unique: true end - create_table :project_feature_flag_access_tokens do |t| + create_table :project_feature_flags_access_tokens do |t| t.integer :project_id, null: false t.string :token, null: false diff --git a/db/schema.rb b/db/schema.rb index 3516fcf53c5d5a..846b212b114191 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -2090,6 +2090,24 @@ add_index "project_deploy_tokens", ["project_id", "deploy_token_id"], name: "index_project_deploy_tokens_on_project_id_and_deploy_token_id", unique: true, using: :btree + create_table "project_feature_flags", force: :cascade do |t| + t.integer "project_id", 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.boolean "active", null: false + end + + add_index "project_feature_flags", ["project_id", "name"], name: "index_project_feature_flags_on_project_id_and_name", unique: true, using: :btree + + create_table "project_feature_flags_access_tokens", force: :cascade do |t| + t.integer "project_id", null: false + t.string "token", null: false + end + + add_index "project_feature_flags_access_tokens", ["project_id", "token"], name: "project_feature_flag_access_token", unique: true, using: :btree + create_table "project_features", force: :cascade do |t| t.integer "project_id", null: false t.integer "merge_requests_access_level" @@ -3247,6 +3265,8 @@ add_foreign_key "project_custom_attributes", "projects", on_delete: :cascade add_foreign_key "project_deploy_tokens", "deploy_tokens", on_delete: :cascade add_foreign_key "project_deploy_tokens", "projects", on_delete: :cascade + add_foreign_key "project_feature_flags", "projects", on_delete: :cascade + add_foreign_key "project_feature_flags_access_tokens", "projects", on_delete: :cascade add_foreign_key "project_features", "projects", name: "fk_18513d9b92", on_delete: :cascade add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade diff --git a/lib/api/entities.rb b/lib/api/entities.rb index d2835d8e926780..8d6b1225b8956d 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1515,31 +1515,17 @@ class ResourceLabelEvent < Grape::Entity end expose :label, using: Entities::LabelBasic expose :action + end - class UnleashIssue < Grape::Entity + class UnleashFeature < Grape::Entity expose :name - expose :title, as: :description - expose :enabled - expose :strategies - - private - - def name - "\##{object.iid}" - end - - def enabled - true - end - - def strategies - [{ name: 'default' }] - end + expose :description, unless: ->(feature) { feature.description.nil? } + expose :active, as: :enabled end - class UnleashFeatureResponse < Grape::Entity + class UnleashFeatures < Grape::Entity expose :version - expose :features, with: UnleashIssue + expose :project_feature_flags, as: :features, with: UnleashFeature private diff --git a/lib/api/unleash.rb b/lib/api/unleash.rb index 249dff336749bf..e4f9d00d3ffd88 100644 --- a/lib/api/unleash.rb +++ b/lib/api/unleash.rb @@ -2,23 +2,39 @@ module API class Unleash < Grape::API include PaginationParams - #before { authenticate! } + before do + unauthorized! unless access_token + end + + get ':unleash/features' do + present @project, with: Entities::UnleashFeatures + end + + post 'unleash/client/register' do + status :ok + end - params do - requires :id, type: String, desc: 'The ID of a project' + post 'unleash/client/metrics' do + status :ok end - resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do - get ':id/unleash/features' do - issues = IssuesFinder.new(current_user, project_id: user_project.id, label_name: 'rollout') - present issues, with: Entities::UnleashFeatureResponse + + private + + helpers do + def project + @project ||= find_project(unleash_appname) + end + + def access_token + @access_token ||= ProjectFeatureFlagsAccessToken.find_by(token: unleash_instanceid, project: project) end - post ':id/unleash/client/register' do - status :ok + def unleash_appname + params[:appname] || env[:HTTP_UNLEASH_APPNAME] end - post ':id/unleash/client/metrics' do - status :ok + def unleash_instanceid + params[:instanceid] || env[:HTTP_UNLEASH_INSTANCEID] end end end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index f5c81bbc20044f..0f47381dc4c15d 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -72,12 +72,12 @@ def environment_slug_regex_message end def feature_flag_regex - /\A[a-z]([-a-z0-9]*[a-z0-9])?\z/ + /\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 '-'" + "Must start with a letter, and cannot end with '-' or '_'" end def build_trace_section_regex -- GitLab From c15bfd70afb4880f6fc97f1cf88cb6024855281d Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 17 Sep 2018 14:03:03 +0200 Subject: [PATCH 04/57] Use badge for feature flag count in list header --- app/views/projects/feature_flags/_list.html.haml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/projects/feature_flags/_list.html.haml b/app/views/projects/feature_flags/_list.html.haml index 5ebc16589b1fa2..1e01e668ac8a87 100644 --- a/app/views/projects/feature_flags/_list.html.haml +++ b/app/views/projects/feature_flags/_list.html.haml @@ -1,6 +1,8 @@ .card .card-header - Feature flags (#{@feature_flags.count}) + Feature flags + .badge.badge-pill + = @feature_flags.count %ul.content-list.pages-domain-list - @feature_flags.each do |feature_flag| %li.pages-domain-list-item.unstyled -- GitLab From 2ba2656dc336b20ff996e76759bb443c1da80bcc Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 17 Sep 2018 14:09:01 +0200 Subject: [PATCH 05/57] Show emtpy state if now feature flags exist --- .../projects/feature_flags/_list.html.haml | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/app/views/projects/feature_flags/_list.html.haml b/app/views/projects/feature_flags/_list.html.haml index 1e01e668ac8a87..29055fc85ed5c2 100644 --- a/app/views/projects/feature_flags/_list.html.haml +++ b/app/views/projects/feature_flags/_list.html.haml @@ -4,16 +4,21 @@ .badge.badge-pill = @feature_flags.count %ul.content-list.pages-domain-list - - @feature_flags.each do |feature_flag| - %li.pages-domain-list-item.unstyled - = feature_flag.name + - if @feature_flags.empty? + %li + .nothing-here-block + = s_('FeatureFlags|No feature flags found.') + - else + - @feature_flags.each do |feature_flag| + %li.pages-domain-list-item.unstyled + = feature_flag.name - %div.controls.d-none.d-md-block - = link_to 'Edit', edit_project_feature_flag_path(@project, feature_flag), class: "btn btn-sm btn-grouped" - = link_to 'Remove', project_feature_flag_path(@project, feature_flag), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" + %div.controls.d-none.d-md-block + = link_to 'Edit', edit_project_feature_flag_path(@project, feature_flag), class: "btn btn-sm btn-grouped" + = link_to 'Remove', project_feature_flag_path(@project, feature_flag), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" - %p - - if feature_flag.active? - %span.badge.badge-success Enabled - - else - %span.badge.badge-danger Disabled \ No newline at end of file + %p + - if feature_flag.active? + %span.badge.badge-success Enabled + - else + %span.badge.badge-danger Disabled \ No newline at end of file -- GitLab From 8fa1d249fa8e23d49cfd8e7d5fa8fbc3cf38dedf Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 17 Sep 2018 14:15:14 +0200 Subject: [PATCH 06/57] Remove two-column layout from create and edit feature flag forms --- .../projects/feature_flags/_form.html.haml | 42 ++++++++++--------- .../projects/feature_flags/edit.html.haml | 6 +-- .../projects/feature_flags/new.html.haml | 9 ++-- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/app/views/projects/feature_flags/_form.html.haml b/app/views/projects/feature_flags/_form.html.haml index 38f35a959858d3..c817f51551b811 100644 --- a/app/views/projects/feature_flags/_form.html.haml +++ b/app/views/projects/feature_flags/_form.html.haml @@ -4,22 +4,26 @@ - @feature_flag.errors.full_messages.each do |msg| %p= msg -- unless @feature_flag.persisted? - .form-group.row - = f.label :name, class: 'col-form-label col-sm-2' do - Name - .col-sm-10 - = f.text_field :name, required: true, autocomplete: 'off', class: 'form-control', disabled: @feature_flag.persisted? - -.form-group.row - = f.label :active, class: 'col-form-label col-sm-2' do - Active - .col-sm-10 - = f.check_box :active - -.form-group.row - = f.label :description, class: 'col-form-label col-sm-2' do - Description - .col-sm-10 - = f.text_area :description, rows: 5, class: 'form-control' - %span.help-inline Write a description of your feature flag +%fieldset + .row + .form-group.col-md-4 + = f.label :name, class: 'label-light', 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-light', 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-light', for: 'feature_flag_status' do + = s_('FeatureFlags|Status') + = f.check_box :active, class: "form-control", id: "feature_flag_status", rows: 4 +.form-actions + - if @feature_flag.persisted? + = f.submit 'Save changes', class: "btn btn-save" + - else + = f.submit 'Create feature flag', class: "btn btn-save" + .float-right + = link_to _('Cancel'), project_feature_flags_path(@project), class: 'btn btn-cancel' diff --git a/app/views/projects/feature_flags/edit.html.haml b/app/views/projects/feature_flags/edit.html.haml index 10a7b0e505a68f..166f59bb92010c 100644 --- a/app/views/projects/feature_flags/edit.html.haml +++ b/app/views/projects/feature_flags/edit.html.haml @@ -1,11 +1,9 @@ - add_to_breadcrumbs "Feature Flags", project_feature_flags_path(@project) - breadcrumb_title @feature_flag.name -- page_title @feature_flag.name +- page_title s_('FeatureFlags|Edit Feature Flag') %h3.page-title - = @feature_flag.name + = 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 } - .form-actions - = f.submit 'Save Changes', class: "btn btn-save" diff --git a/app/views/projects/feature_flags/new.html.haml b/app/views/projects/feature_flags/new.html.haml index dac8fc54231252..d75d8585c61a2f 100644 --- a/app/views/projects/feature_flags/new.html.haml +++ b/app/views/projects/feature_flags/new.html.haml @@ -1,12 +1,9 @@ - add_to_breadcrumbs "Feature Flags", project_feature_flags_path(@project) -- page_title 'New Feature Flags' +- breadcrumb_title s_('FeatureFlags|New') +- page_title s_('FeatureFlags|New Feature Flag') %h3.page-title - New Feature Flags + = 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 } - .form-actions - = f.submit 'Create New Feature Flag', class: "btn btn-save" - .float-right - = link_to _('Cancel'), project_feature_flags_path(@project), class: 'btn btn-cancel' -- GitLab From 1160a4e39e5a59fdbb5271c3773348b90593456a Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 17 Sep 2018 15:27:46 +0200 Subject: [PATCH 07/57] Add modal for deleting feature flags --- .../_delete_feature_flag_modal.html.haml | 21 +++++++++++++++++++ .../projects/feature_flags/_list.html.haml | 4 +++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 app/views/projects/feature_flags/_delete_feature_flag_modal.html.haml diff --git a/app/views/projects/feature_flags/_delete_feature_flag_modal.html.haml b/app/views/projects/feature_flags/_delete_feature_flag_modal.html.haml new file mode 100644 index 00000000000000..7367e6795260d6 --- /dev/null +++ b/app/views/projects/feature_flags/_delete_feature_flag_modal.html.haml @@ -0,0 +1,21 @@ +.modal{ id: "delete-feature-flag-modal-#{feature_flag.id}", + tabindex: -1, + role: 'dialog' } + .modal-dialog{ role: 'document' } + .modal-content + .modal-header + %h5.modal-title + = s_('FeatureFlags|Delete feature flag?') + %button.close{ type: 'button', data: { dismiss: 'modal' }, aria: { label: _('Close') } } + %span{ "aria-hidden": true } × + .modal-body + %p + = s_('FeatureFlags|Feature flag %{feature_flag_name} will be removed. Are you sure?') % { feature_flag_name: 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' diff --git a/app/views/projects/feature_flags/_list.html.haml b/app/views/projects/feature_flags/_list.html.haml index 29055fc85ed5c2..c64e84042159a5 100644 --- a/app/views/projects/feature_flags/_list.html.haml +++ b/app/views/projects/feature_flags/_list.html.haml @@ -14,8 +14,10 @@ = feature_flag.name %div.controls.d-none.d-md-block + = render 'delete_feature_flag_modal', { feature_flag: feature_flag } = link_to 'Edit', edit_project_feature_flag_path(@project, feature_flag), class: "btn btn-sm btn-grouped" - = link_to 'Remove', project_feature_flag_path(@project, feature_flag), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" + %button.btn.btn-remove.btn-sm.btn-grouped{ type: 'button', data: { toggle: 'modal', target: "#delete-feature-flag-modal-#{feature_flag.id}" } } + Remove %p - if feature_flag.active? -- GitLab From 8cf49158cf806e3281613acf071b317bf10b195f Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Tue, 18 Sep 2018 12:26:03 +0200 Subject: [PATCH 08/57] Use modal for feature flag configuration information --- .../_configure_feature_flags_modal.html.haml | 56 +++++++++++++++++++ .../projects/feature_flags/_use.html.haml | 23 -------- .../projects/feature_flags/index.html.haml | 12 ++-- 3 files changed, 64 insertions(+), 27 deletions(-) create mode 100644 app/views/projects/feature_flags/_configure_feature_flags_modal.html.haml delete mode 100644 app/views/projects/feature_flags/_use.html.haml diff --git a/app/views/projects/feature_flags/_configure_feature_flags_modal.html.haml b/app/views/projects/feature_flags/_configure_feature_flags_modal.html.haml new file mode 100644 index 00000000000000..2dae91771e0b7f --- /dev/null +++ b/app/views/projects/feature_flags/_configure_feature_flags_modal.html.haml @@ -0,0 +1,56 @@ +#configure-feature-flags-modal.modal{ tabindex: -1, + role: 'dialog' } + .modal-dialog{ role: 'document' } + .modal-content + .modal-header + %h5.modal-title + = s_('FeatureFlags|Configure feature flags') + %button.close{ type: 'button', data: { dismiss: 'modal' }, aria: { label: _('Close') } } + %span{ "aria-hidden": true } × + .modal-body + %p + = s_('FeatureFlags|Install a compatible client library and specify the API URL, application name, and instance ID during the configuration setup.') + = link_to s_('FeatureFlags|More information'), '#' + + .form-group + = label_tag :api_url, s_('FeatureFlags|API URL'), class: 'label-light' + .input-group + - unleash_api_url = "#{root_url(only_path: false)}api/v4/unleash" + = text_field_tag :api_url, + unleash_api_url, + 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 :application_name, s_('FeatureFlags|Application name'), class: 'label-light' + .input-group + = text_field_tag :application_name, + @project.id, + 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") + + .form-group + = label_tag :instance_id, s_('FeatureFlags|Instance ID'), class: 'label-light' + .input-group + = text_field_tag :instance_id, + @unleash_instanceid, + 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") diff --git a/app/views/projects/feature_flags/_use.html.haml b/app/views/projects/feature_flags/_use.html.haml deleted file mode 100644 index a77244747a7673..00000000000000 --- a/app/views/projects/feature_flags/_use.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -.card.bg-info - .card-header - Configure feature flags - .card-body - %p - Learn how to enable feature flags for your application. - - %ol - %li - = _("Install a compatible with client library") - = (_("(checkout the %{link} for information on how to install it).") % { link: "here" }).html_safe - %li - = _("Specify the following URL during for the library configuration setup:") - %code#coordinator_address= "#{root_url(only_path: false)}api/v4/unleash" - %li - = _("Use the following application name:") - %code#registration_token= @project.id - %li - = _("Use the following application name:") - %code#registration_token= @unleash_instanceid - %li - = _("You can also see all features online:") - %code#registration_token= "#{root_url(only_path: false)}api/v4/unleash/features?appname=#{@project.id}&instanceid=#{@unleash_instanceid}" \ No newline at end of file diff --git a/app/views/projects/feature_flags/index.html.haml b/app/views/projects/feature_flags/index.html.haml index 20b5d680e01ff1..778682b066ac18 100644 --- a/app/views/projects/feature_flags/index.html.haml +++ b/app/views/projects/feature_flags/index.html.haml @@ -3,14 +3,18 @@ %h3.page-title.with-button Feature Flags - - if can?(current_user, :create_feature_flags, @project) - = link_to new_project_feature_flag_path(@project), class: 'btn btn-new float-right', title: 'New Feature Flag' do - New Feature Flag + .pull-right + %button.btn.btn-default{ type: 'button', data: { toggle: 'modal', target: '#configure-feature-flags-modal' } } + = s_('FeatureFlags|Configure') + + - if can?(current_user, :create_feature_flags, @project) + = link_to new_project_feature_flag_path(@project), class: 'btn btn-new' do + = s_('FeatureFlags|New Feature Flag') %p.light With GitLab Feature Flags %hr.clearfix -= render 'use' += render 'configure_feature_flags_modal' = render 'list' -- GitLab From 8227cdeac9ae6d9bf1a56660c3a97743964f15e0 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Tue, 18 Sep 2018 12:49:23 +0200 Subject: [PATCH 09/57] Add label to feature flag status in form --- app/views/projects/feature_flags/_form.html.haml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/views/projects/feature_flags/_form.html.haml b/app/views/projects/feature_flags/_form.html.haml index c817f51551b811..6aafc782f5b307 100644 --- a/app/views/projects/feature_flags/_form.html.haml +++ b/app/views/projects/feature_flags/_form.html.haml @@ -17,9 +17,12 @@ = f.text_area :description, class: "form-control", id: "feature_flag_description", rows: 4 .row .form-group.col-md-1 - = f.label :active, class: 'label-light', for: 'feature_flag_status' do + = f.label :active, class: 'label-light' do = s_('FeatureFlags|Status') - = f.check_box :active, class: "form-control", id: "feature_flag_status", rows: 4 + .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 'Save changes', class: "btn btn-save" -- GitLab From f1bbb2d9e832bf3a27cb014e9a84b8a89e65f4cf Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Tue, 18 Sep 2018 13:34:05 +0200 Subject: [PATCH 10/57] Use icons for feature flag action buttons --- .../projects/feature_flags/_list.html.haml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/views/projects/feature_flags/_list.html.haml b/app/views/projects/feature_flags/_list.html.haml index c64e84042159a5..bc91756d14fa64 100644 --- a/app/views/projects/feature_flags/_list.html.haml +++ b/app/views/projects/feature_flags/_list.html.haml @@ -13,11 +13,20 @@ %li.pages-domain-list-item.unstyled = feature_flag.name - %div.controls.d-none.d-md-block + %div.controls.d-flex = render 'delete_feature_flag_modal', { feature_flag: feature_flag } - = link_to 'Edit', edit_project_feature_flag_path(@project, feature_flag), class: "btn btn-sm btn-grouped" - %button.btn.btn-remove.btn-sm.btn-grouped{ type: 'button', data: { toggle: 'modal', target: "#delete-feature-flag-modal-#{feature_flag.id}" } } - Remove + + = button_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) + + %button.btn.btn-danger.has-tooltip.m-0.ml-2{ type: 'button', + data: { toggle: 'modal', + target: "#delete-feature-flag-modal-#{feature_flag.id}" }, + title: _('Delete') } + = sprite_icon('remove', size: 16) %p - if feature_flag.active? -- GitLab From 55e4c7ca57bf40c49af5a344cf1cc645e9badc22 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Tue, 18 Sep 2018 14:06:05 +0200 Subject: [PATCH 11/57] Convert feature flag list to table --- .../projects/feature_flags/_list.html.haml | 35 ------------------- .../projects/feature_flags/_table.html.haml | 33 +++++++++++++++++ .../projects/feature_flags/index.html.haml | 7 +--- 3 files changed, 34 insertions(+), 41 deletions(-) delete mode 100644 app/views/projects/feature_flags/_list.html.haml create mode 100644 app/views/projects/feature_flags/_table.html.haml diff --git a/app/views/projects/feature_flags/_list.html.haml b/app/views/projects/feature_flags/_list.html.haml deleted file mode 100644 index bc91756d14fa64..00000000000000 --- a/app/views/projects/feature_flags/_list.html.haml +++ /dev/null @@ -1,35 +0,0 @@ -.card - .card-header - Feature flags - .badge.badge-pill - = @feature_flags.count - %ul.content-list.pages-domain-list - - if @feature_flags.empty? - %li - .nothing-here-block - = s_('FeatureFlags|No feature flags found.') - - else - - @feature_flags.each do |feature_flag| - %li.pages-domain-list-item.unstyled - = feature_flag.name - - %div.controls.d-flex - = render 'delete_feature_flag_modal', { feature_flag: feature_flag } - - = button_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) - - %button.btn.btn-danger.has-tooltip.m-0.ml-2{ type: 'button', - data: { toggle: 'modal', - target: "#delete-feature-flag-modal-#{feature_flag.id}" }, - title: _('Delete') } - = sprite_icon('remove', size: 16) - - %p - - if feature_flag.active? - %span.badge.badge-success Enabled - - else - %span.badge.badge-danger Disabled \ No newline at end of file diff --git a/app/views/projects/feature_flags/_table.html.haml b/app/views/projects/feature_flags/_table.html.haml new file mode 100644 index 00000000000000..2a022fb4b645ac --- /dev/null +++ b/app/views/projects/feature_flags/_table.html.haml @@ -0,0 +1,33 @@ + +.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') + - if @feature_flags.empty? + .nothing-here-block + = s_('FeatureFlags|No feature flags found.') + - else + - @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' } + = check_box_tag 'feature_flag_status', nil, feature_flag.active?, disabled: true + + .table-section.section-50.d-flex.flex-column{ role: 'gridcell' } + .text-monospace= feature_flag.name + .text-secondary= feature_flag.description + + .table-section.section-40.table-button-footer{ role: 'gridcell' } + .table-action-buttons + = 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) + + %button.btn.btn-danger.has-tooltip.ml-2{ type: 'button', + data: { toggle: 'modal', + target: "#delete-feature-flag-modal-#{feature_flag.id}" }, + title: _('Delete') } + = sprite_icon('remove', size: 16) diff --git a/app/views/projects/feature_flags/index.html.haml b/app/views/projects/feature_flags/index.html.haml index 778682b066ac18..8b446064548001 100644 --- a/app/views/projects/feature_flags/index.html.haml +++ b/app/views/projects/feature_flags/index.html.haml @@ -11,10 +11,5 @@ = link_to new_project_feature_flag_path(@project), class: 'btn btn-new' do = s_('FeatureFlags|New Feature Flag') -%p.light - With GitLab Feature Flags - -%hr.clearfix - = render 'configure_feature_flags_modal' -= render 'list' += render 'table' -- GitLab From 58bf62bc97012d6ff1259a4a7668a32484452a13 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Tue, 18 Sep 2018 14:26:52 +0200 Subject: [PATCH 12/57] Restyle empty state for feature flags --- .../feature_flags/_empty_state.html.haml | 11 +++++ .../_new_feature_flag_button.html.haml | 3 ++ .../projects/feature_flags/_table.html.haml | 45 +++++++++---------- .../projects/feature_flags/index.html.haml | 10 +++-- 4 files changed, 40 insertions(+), 29 deletions(-) create mode 100644 app/views/projects/feature_flags/_empty_state.html.haml create mode 100644 app/views/projects/feature_flags/_new_feature_flag_button.html.haml diff --git a/app/views/projects/feature_flags/_empty_state.html.haml b/app/views/projects/feature_flags/_empty_state.html.haml new file mode 100644 index 00000000000000..3cc8873abe2a86 --- /dev/null +++ b/app/views/projects/feature_flags/_empty_state.html.haml @@ -0,0 +1,11 @@ +.border-top + .row.empty-state + .col-12 + .text-content + %h4.text-center= s_('FeatureFlags|Get started with feature flags') + %p.text-center + = s_('FeatureFlags|Feature flags allow you to configure your code into different flavors by dynamically toggling certain functionality.') + = link_to 'More information', '#' + + .text-center + = render 'new_feature_flag_button' diff --git a/app/views/projects/feature_flags/_new_feature_flag_button.html.haml b/app/views/projects/feature_flags/_new_feature_flag_button.html.haml new file mode 100644 index 00000000000000..61bf64113d921c --- /dev/null +++ b/app/views/projects/feature_flags/_new_feature_flag_button.html.haml @@ -0,0 +1,3 @@ +- if can?(current_user, :create_feature_flags, @project) + = link_to new_project_feature_flag_path(@project), class: 'btn btn-new' do + = s_('FeatureFlags|New Feature Flag') diff --git a/app/views/projects/feature_flags/_table.html.haml b/app/views/projects/feature_flags/_table.html.haml index 2a022fb4b645ac..0da02da0a1ae5d 100644 --- a/app/views/projects/feature_flags/_table.html.haml +++ b/app/views/projects/feature_flags/_table.html.haml @@ -1,33 +1,28 @@ - .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') - - if @feature_flags.empty? - .nothing-here-block - = s_('FeatureFlags|No feature flags found.') - - else - - @feature_flags.each do |feature_flag| - = render 'delete_feature_flag_modal', { feature_flag: 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' } - = check_box_tag 'feature_flag_status', nil, feature_flag.active?, disabled: true + .gl-responsive-table-row{ role: 'row' } + .table-section.section-10{ role: 'gridcell' } + = check_box_tag 'feature_flag_status', nil, feature_flag.active?, disabled: true - .table-section.section-50.d-flex.flex-column{ role: 'gridcell' } - .text-monospace= feature_flag.name - .text-secondary= feature_flag.description + .table-section.section-50.d-flex.flex-column{ role: 'gridcell' } + .text-monospace= feature_flag.name + .text-secondary= feature_flag.description - .table-section.section-40.table-button-footer{ role: 'gridcell' } - .table-action-buttons - = 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) + .table-section.section-40.table-button-footer{ role: 'gridcell' } + .table-action-buttons + = 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) - %button.btn.btn-danger.has-tooltip.ml-2{ type: 'button', - data: { toggle: 'modal', - target: "#delete-feature-flag-modal-#{feature_flag.id}" }, - title: _('Delete') } - = sprite_icon('remove', size: 16) + %button.btn.btn-danger.has-tooltip.ml-2{ type: 'button', + data: { toggle: 'modal', + target: "#delete-feature-flag-modal-#{feature_flag.id}" }, + title: _('Delete') } + = sprite_icon('remove', size: 16) diff --git a/app/views/projects/feature_flags/index.html.haml b/app/views/projects/feature_flags/index.html.haml index 8b446064548001..f04291952d0149 100644 --- a/app/views/projects/feature_flags/index.html.haml +++ b/app/views/projects/feature_flags/index.html.haml @@ -7,9 +7,11 @@ %button.btn.btn-default{ type: 'button', data: { toggle: 'modal', target: '#configure-feature-flags-modal' } } = s_('FeatureFlags|Configure') - - if can?(current_user, :create_feature_flags, @project) - = link_to new_project_feature_flag_path(@project), class: 'btn btn-new' do - = s_('FeatureFlags|New Feature Flag') + = render 'new_feature_flag_button' = render 'configure_feature_flags_modal' -= render 'table' + +- if @feature_flags.empty? + = render 'empty_state' +- else + = render 'table' -- GitLab From af4a2fb819814bf55a3d3f62e17379f8328f4cb4 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Wed, 19 Sep 2018 15:00:58 +0200 Subject: [PATCH 13/57] Change margin of configure button to 8px --- app/views/projects/feature_flags/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/feature_flags/index.html.haml b/app/views/projects/feature_flags/index.html.haml index f04291952d0149..0f13c2b0be17d5 100644 --- a/app/views/projects/feature_flags/index.html.haml +++ b/app/views/projects/feature_flags/index.html.haml @@ -4,7 +4,7 @@ Feature Flags .pull-right - %button.btn.btn-default{ type: 'button', data: { toggle: 'modal', target: '#configure-feature-flags-modal' } } + %button.btn.btn-default.append-right-8{ type: 'button', data: { toggle: 'modal', target: '#configure-feature-flags-modal' } }> = s_('FeatureFlags|Configure') = render 'new_feature_flag_button' -- GitLab From 71678a43b89bc77372f7b8c6f395f64277edaccc Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Wed, 19 Sep 2018 15:02:01 +0200 Subject: [PATCH 14/57] Make feature flag table actions a button group --- app/views/projects/feature_flags/_table.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/feature_flags/_table.html.haml b/app/views/projects/feature_flags/_table.html.haml index 0da02da0a1ae5d..8dd2e7dced1562 100644 --- a/app/views/projects/feature_flags/_table.html.haml +++ b/app/views/projects/feature_flags/_table.html.haml @@ -14,14 +14,14 @@ .text-secondary= feature_flag.description .table-section.section-40.table-button-footer{ role: 'gridcell' } - .table-action-buttons + .table-action-buttons.btn-group = 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) - %button.btn.btn-danger.has-tooltip.ml-2{ type: 'button', + %button.btn.btn-danger.has-tooltip{ type: 'button', data: { toggle: 'modal', target: "#delete-feature-flag-modal-#{feature_flag.id}" }, title: _('Delete') } -- GitLab From afbd72938f307792170895690765353362055766 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Wed, 19 Sep 2018 15:08:20 +0200 Subject: [PATCH 15/57] Make configure button blue --- app/views/projects/feature_flags/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/feature_flags/index.html.haml b/app/views/projects/feature_flags/index.html.haml index 0f13c2b0be17d5..72ab7e9a0cfa6e 100644 --- a/app/views/projects/feature_flags/index.html.haml +++ b/app/views/projects/feature_flags/index.html.haml @@ -4,7 +4,7 @@ Feature Flags .pull-right - %button.btn.btn-default.append-right-8{ type: 'button', data: { toggle: 'modal', target: '#configure-feature-flags-modal' } }> + %button.btn.btn-primary.btn-inverted.append-right-8{ type: 'button', data: { toggle: 'modal', target: '#configure-feature-flags-modal' } }> = s_('FeatureFlags|Configure') = render 'new_feature_flag_button' -- GitLab From def89c5ed20ddfca2a11a2334f2284981e27b60e Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Wed, 19 Sep 2018 15:13:02 +0200 Subject: [PATCH 16/57] Replace checkbox for status in feature flag list with badge --- app/views/projects/feature_flags/_table.html.haml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/views/projects/feature_flags/_table.html.haml b/app/views/projects/feature_flags/_table.html.haml index 8dd2e7dced1562..21efebb4b1acf2 100644 --- a/app/views/projects/feature_flags/_table.html.haml +++ b/app/views/projects/feature_flags/_table.html.haml @@ -7,7 +7,12 @@ .gl-responsive-table-row{ role: 'row' } .table-section.section-10{ role: 'gridcell' } - = check_box_tag 'feature_flag_status', nil, feature_flag.active?, disabled: true + - if feature_flag.active? + %span.badge.badge-success + = s_('FeatureFlags|Active') + - else + %span.badge.badge-danger + = s_('FeatureFlags|Inactive') .table-section.section-50.d-flex.flex-column{ role: 'gridcell' } .text-monospace= feature_flag.name -- GitLab From 685b68a02e5425cbec2f84dc3398ba99b632f538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Wed, 19 Sep 2018 15:37:26 +0200 Subject: [PATCH 17/57] Add `destroy` action --- app/controllers/projects/feature_flags_controller.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/controllers/projects/feature_flags_controller.rb b/app/controllers/projects/feature_flags_controller.rb index 60d0c4668a6c09..3c6157c33e774d 100644 --- a/app/controllers/projects/feature_flags_controller.rb +++ b/app/controllers/projects/feature_flags_controller.rb @@ -39,6 +39,14 @@ def update end end + def destroy + if feature_flag.destroy + flash[:notice] = 'Feature flag was successfully removed.' + end + + redirect_to project_feature_flags_path(@project) + end + protected def feature_flag -- GitLab From 542763ffb02dff56e2911bd6d52b45fcf715863a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Thu, 20 Sep 2018 13:05:37 +0200 Subject: [PATCH 18/57] Rename FF schema to use `Operations::` --- .../feature_flag.rb} | 0 .../feature_flag_access_token.rb} | 0 ...626171125_add_feature_flags_to_projects.rb | 4 +- db/schema.rb | 40 +++++++++---------- 4 files changed, 22 insertions(+), 22 deletions(-) rename app/models/{project_feature_flag.rb => operations/feature_flag.rb} (100%) rename app/models/{project_feature_flags_access_token.rb => operations/feature_flag_access_token.rb} (100%) diff --git a/app/models/project_feature_flag.rb b/app/models/operations/feature_flag.rb similarity index 100% rename from app/models/project_feature_flag.rb rename to app/models/operations/feature_flag.rb diff --git a/app/models/project_feature_flags_access_token.rb b/app/models/operations/feature_flag_access_token.rb similarity index 100% rename from app/models/project_feature_flags_access_token.rb rename to app/models/operations/feature_flag_access_token.rb diff --git a/db/migrate/20180626171125_add_feature_flags_to_projects.rb b/db/migrate/20180626171125_add_feature_flags_to_projects.rb index 9ca2d3a25803d0..d3232463f61c99 100644 --- a/db/migrate/20180626171125_add_feature_flags_to_projects.rb +++ b/db/migrate/20180626171125_add_feature_flags_to_projects.rb @@ -5,7 +5,7 @@ class AddFeatureFlagsToProjects < ActiveRecord::Migration DOWNTIME = false def change - create_table :project_feature_flags do |t| + create_table :operations_feature_flags do |t| t.integer :project_id, null: false t.datetime_with_timezone :created_at, null: false t.datetime_with_timezone :updated_at, null: false @@ -19,7 +19,7 @@ def change t.index [:project_id, :name], unique: true end - create_table :project_feature_flags_access_tokens do |t| + create_table :operations_feature_flags_access_tokens do |t| t.integer :project_id, null: false t.string :token, null: false diff --git a/db/schema.rb b/db/schema.rb index 846b212b114191..975da6fcae95d2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1943,6 +1943,24 @@ t.string "nonce", null: false end + create_table "operations_feature_flags", force: :cascade do |t| + t.integer "project_id", 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.boolean "active", null: false + 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_access_tokens", force: :cascade do |t| + t.integer "project_id", null: false + t.string "token", null: false + end + + add_index "operations_feature_flags_access_tokens", ["project_id", "token"], name: "project_feature_flag_access_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 @@ -2090,24 +2108,6 @@ add_index "project_deploy_tokens", ["project_id", "deploy_token_id"], name: "index_project_deploy_tokens_on_project_id_and_deploy_token_id", unique: true, using: :btree - create_table "project_feature_flags", force: :cascade do |t| - t.integer "project_id", 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.boolean "active", null: false - end - - add_index "project_feature_flags", ["project_id", "name"], name: "index_project_feature_flags_on_project_id_and_name", unique: true, using: :btree - - create_table "project_feature_flags_access_tokens", force: :cascade do |t| - t.integer "project_id", null: false - t.string "token", null: false - end - - add_index "project_feature_flags_access_tokens", ["project_id", "token"], name: "project_feature_flag_access_token", unique: true, using: :btree - create_table "project_features", force: :cascade do |t| t.integer "project_id", null: false t.integer "merge_requests_access_level" @@ -3251,6 +3251,8 @@ 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_access_tokens", "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 @@ -3265,8 +3267,6 @@ add_foreign_key "project_custom_attributes", "projects", on_delete: :cascade add_foreign_key "project_deploy_tokens", "deploy_tokens", on_delete: :cascade add_foreign_key "project_deploy_tokens", "projects", on_delete: :cascade - add_foreign_key "project_feature_flags", "projects", on_delete: :cascade - add_foreign_key "project_feature_flags_access_tokens", "projects", on_delete: :cascade add_foreign_key "project_features", "projects", name: "fk_18513d9b92", on_delete: :cascade add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade -- GitLab From d36a45047ea204da7724d7cac194dd2e95fc11a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Thu, 20 Sep 2018 13:16:32 +0200 Subject: [PATCH 19/57] Fix code structure --- .../projects/feature_flags_controller.rb | 14 +++++------ app/models/operations/feature_flag.rb | 24 +++++++++++-------- .../operations/feature_flag_access_token.rb | 12 ---------- .../operations/feature_flags_access_token.rb | 16 +++++++++++++ app/models/project.rb | 8 +++++-- lib/api/entities.rb | 2 +- 6 files changed, 44 insertions(+), 32 deletions(-) delete mode 100644 app/models/operations/feature_flag_access_token.rb create mode 100644 app/models/operations/feature_flags_access_token.rb diff --git a/app/controllers/projects/feature_flags_controller.rb b/app/controllers/projects/feature_flags_controller.rb index 3c6157c33e774d..19e1751db168f4 100644 --- a/app/controllers/projects/feature_flags_controller.rb +++ b/app/controllers/projects/feature_flags_controller.rb @@ -8,16 +8,16 @@ class Projects::FeatureFlagsController < Projects::ApplicationController before_action :feature_flag, only: [:edit, :update, :destroy] def index - @feature_flags = project.project_feature_flags - @unleash_instanceid = project.project_feature_flags_access_tokens.first&.token || project.project_feature_flags_access_tokens.create!.token + @feature_flags = project.operations_feature_flags + @unleash_instanceid = project.feature_flag_access_token end def new - @feature_flag = project.project_feature_flags.new + @feature_flag = project.operations_feature_flags.new end def create - @feature_flag = project.project_feature_flags.create(create_params) + @feature_flag = project.operations_feature_flags.create(create_params) if @feature_flag.persisted? flash[:notice] = 'Feature flag was successfully created.' @@ -50,16 +50,16 @@ def destroy protected def feature_flag - @feature_flag ||= project.project_feature_flags.find(params[:id]) + @feature_flag ||= project.operations_feature_flags.find(params[:id]) end def create_params - params.require(:project_feature_flag) + params.require(:operations_feature_flag) .permit(:name, :description, :active) end def update_params - params.require(:project_feature_flag) + params.require(:operations_feature_flag) .permit(:description, :active) end end diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb index 11681f555de3f4..4ae91fa5890bc9 100644 --- a/app/models/operations/feature_flag.rb +++ b/app/models/operations/feature_flag.rb @@ -1,12 +1,16 @@ -class ProjectFeatureFlag < ActiveRecord::Base - belongs_to :project +module Operations + class FeatureFlag < ActiveRecord::Base + self.table_name = 'operations_feature_flags' - 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 - } + 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 + } + end end diff --git a/app/models/operations/feature_flag_access_token.rb b/app/models/operations/feature_flag_access_token.rb deleted file mode 100644 index d31c5713c3122c..00000000000000 --- a/app/models/operations/feature_flag_access_token.rb +++ /dev/null @@ -1,12 +0,0 @@ -class ProjectFeatureFlagsAccessToken < ActiveRecord::Base - include TokenAuthenticatable - - belongs_to :project - - validates :project, presence: true - validates :token, presence: true - - add_authentication_token_field :token - - before_validation :ensure_token! -end diff --git a/app/models/operations/feature_flags_access_token.rb b/app/models/operations/feature_flags_access_token.rb new file mode 100644 index 00000000000000..3f2daf332f4206 --- /dev/null +++ b/app/models/operations/feature_flags_access_token.rb @@ -0,0 +1,16 @@ +module Operations + class FeatureFlagsAccessToken < ActiveRecord::Base + include TokenAuthenticatable + + self.table_name = 'operations_feature_flags_access_tokens' + + belongs_to :project + + validates :project, presence: true + validates :token, presence: true + + add_authentication_token_field :token + + before_validation :ensure_token! + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 9e5b6cbc226c50..2d294fc0c5ee50 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -266,8 +266,8 @@ class Project < ActiveRecord::Base has_many :project_deploy_tokens has_many :deploy_tokens, through: :project_deploy_tokens - has_many :project_feature_flags - has_many :project_feature_flags_access_tokens + has_many :operations_feature_flags, class_name: 'Operations::FeatureFlag' + has_one :operations_feature_flags_access_token, class_name: 'Operations::FeatureFlagsAccessToken' has_one :auto_devops, class_name: 'ProjectAutoDevops' has_many :custom_attributes, class_name: 'ProjectCustomAttribute' @@ -2105,6 +2105,10 @@ def auto_cancel_pending_pipelines? auto_cancel_pending_pipelines == 'enabled' end + def feature_flag_access_token + (operations_feature_flags_access_token || create_operations_feature_flags_access_token!).token + end + private # rubocop: disable CodeReuse/ServiceClass diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 8d6b1225b8956d..157a67737314ef 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1525,7 +1525,7 @@ class UnleashFeature < Grape::Entity class UnleashFeatures < Grape::Entity expose :version - expose :project_feature_flags, as: :features, with: UnleashFeature + expose :operations_feature_flags, as: :features, with: UnleashFeature private -- GitLab From 7827d187e47555e2014aba6a9d1a3dfc4d8e8285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Thu, 20 Sep 2018 13:30:14 +0200 Subject: [PATCH 20/57] Refactor API interface --- .../projects/feature_flags_controller.rb | 1 - app/helpers/feature_flags_helper.rb | 11 ++++ .../_configure_feature_flags_modal.html.haml | 23 ++++----- lib/api/api.rb | 2 +- lib/api/feature_flags.rb | 50 +++++++++++++++++++ lib/api/unleash.rb | 41 --------------- 6 files changed, 73 insertions(+), 55 deletions(-) create mode 100644 app/helpers/feature_flags_helper.rb create mode 100644 lib/api/feature_flags.rb delete mode 100644 lib/api/unleash.rb diff --git a/app/controllers/projects/feature_flags_controller.rb b/app/controllers/projects/feature_flags_controller.rb index 19e1751db168f4..41389e47c4f159 100644 --- a/app/controllers/projects/feature_flags_controller.rb +++ b/app/controllers/projects/feature_flags_controller.rb @@ -9,7 +9,6 @@ class Projects::FeatureFlagsController < Projects::ApplicationController def index @feature_flags = project.operations_feature_flags - @unleash_instanceid = project.feature_flag_access_token end def new diff --git a/app/helpers/feature_flags_helper.rb b/app/helpers/feature_flags_helper.rb new file mode 100644 index 00000000000000..7986b4144dac0d --- /dev/null +++ b/app/helpers/feature_flags_helper.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module FeatureFlagsHelper + def unleash_api_url(project) + "#{root_url(only_path: false)}api/v4/feature_flags/projects/#{project.id}/unleash" + end + + def unleash_api_instanceid(project) + project.feature_flag_access_token + end +end diff --git a/app/views/projects/feature_flags/_configure_feature_flags_modal.html.haml b/app/views/projects/feature_flags/_configure_feature_flags_modal.html.haml index 2dae91771e0b7f..81816dd637fdb2 100644 --- a/app/views/projects/feature_flags/_configure_feature_flags_modal.html.haml +++ b/app/views/projects/feature_flags/_configure_feature_flags_modal.html.haml @@ -15,9 +15,8 @@ .form-group = label_tag :api_url, s_('FeatureFlags|API URL'), class: 'label-light' .input-group - - unleash_api_url = "#{root_url(only_path: false)}api/v4/unleash" = text_field_tag :api_url, - unleash_api_url, + unleash_api_url(@project), readonly: true, class: "form-control js-select-on-focus" %span.input-group-append @@ -28,29 +27,29 @@ class: "input-group-text btn btn-default") .form-group - = label_tag :application_name, s_('FeatureFlags|Application name'), class: 'label-light' + = label_tag :instance_id, s_('FeatureFlags|Instance ID'), class: 'label-light' .input-group - = text_field_tag :application_name, - @project.id, + = text_field_tag :instance_id, + unleash_api_instanceid(@project), readonly: true, class: "form-control js-select-on-focus" %span.input-group-append - = clipboard_button(target: '#application_name', - title: _("Copy name to clipboard"), + = 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 :instance_id, s_('FeatureFlags|Instance ID'), class: 'label-light' + = label_tag :application_name, s_('FeatureFlags|Application name'), class: 'label-light' .input-group - = text_field_tag :instance_id, - @unleash_instanceid, + = text_field_tag :application_name, + "production", readonly: true, class: "form-control js-select-on-focus" %span.input-group-append - = clipboard_button(target: '#instance_id', - title: _("Copy ID to clipboard"), + = clipboard_button(target: '#application_name', + title: _("Copy name to clipboard"), placement: "left", container: '#configure-feature-flags-modal', class: "input-group-text btn btn-default") diff --git a/lib/api/api.rb b/lib/api/api.rb index 5ce0733b38b134..37d2f91ee93f7e 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -108,6 +108,7 @@ class API < Grape::API mount ::API::Environments mount ::API::Events mount ::API::Features + mount ::API::FeatureFlags mount ::API::Files mount ::API::GroupBoards mount ::API::GroupMilestones @@ -158,7 +159,6 @@ class API < Grape::API mount ::API::Templates mount ::API::Todos mount ::API::Triggers - mount ::API::Unleash mount ::API::Users mount ::API::Variables mount ::API::Version diff --git a/lib/api/feature_flags.rb b/lib/api/feature_flags.rb new file mode 100644 index 00000000000000..dff43544a75a07 --- /dev/null +++ b/lib/api/feature_flags.rb @@ -0,0 +1,50 @@ +module API + class FeatureFlags < Grape::API + include PaginationParams + + resource :feature_flags do + resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do + params do + requires :id, type: String, desc: 'The ID of a project' + end + route_param :id do + resource :unleash do + before do + authenticate_by_unleash_access_token! + end + + get 'features' do + present project, with: 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[:id]) + end + + def unleash_instanceid + params[:instanceid] || env[:HTTP_UNLEASH_INSTANCEID] + end + + def authenticate_by_unleash_access_token! + unless Operations::FeatureFlagsAccessToken.find_by(token: unleash_instanceid, project: project) + unauthorized! + end + end + end + end + end +end diff --git a/lib/api/unleash.rb b/lib/api/unleash.rb deleted file mode 100644 index e4f9d00d3ffd88..00000000000000 --- a/lib/api/unleash.rb +++ /dev/null @@ -1,41 +0,0 @@ -module API - class Unleash < Grape::API - include PaginationParams - - before do - unauthorized! unless access_token - end - - get ':unleash/features' do - present @project, with: Entities::UnleashFeatures - end - - post 'unleash/client/register' do - status :ok - end - - post 'unleash/client/metrics' do - status :ok - end - - private - - helpers do - def project - @project ||= find_project(unleash_appname) - end - - def access_token - @access_token ||= ProjectFeatureFlagsAccessToken.find_by(token: unleash_instanceid, project: project) - end - - def unleash_appname - params[:appname] || env[:HTTP_UNLEASH_APPNAME] - end - - def unleash_instanceid - params[:instanceid] || env[:HTTP_UNLEASH_INSTANCEID] - end - end - end -end -- GitLab From 90a0e58158402e6ec948c7ef5d49e817978b7733 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 24 Sep 2018 11:01:26 +0200 Subject: [PATCH 21/57] Remove top button row from feature flag empty state --- .../_configure_feature_flags_button.html.haml | 2 ++ .../feature_flags/_empty_state.html.haml | 5 ++++- .../_new_feature_flag_button.html.haml | 2 +- .../projects/feature_flags/index.html.haml | 18 ++++++++---------- 4 files changed, 15 insertions(+), 12 deletions(-) create mode 100644 app/views/projects/feature_flags/_configure_feature_flags_button.html.haml diff --git a/app/views/projects/feature_flags/_configure_feature_flags_button.html.haml b/app/views/projects/feature_flags/_configure_feature_flags_button.html.haml new file mode 100644 index 00000000000000..b1336b02967423 --- /dev/null +++ b/app/views/projects/feature_flags/_configure_feature_flags_button.html.haml @@ -0,0 +1,2 @@ +%button.btn.btn-primary.btn-inverted.append-right-8{ type: 'button', data: { toggle: 'modal', target: '#configure-feature-flags-modal' } }> + = s_('FeatureFlags|Configure') diff --git a/app/views/projects/feature_flags/_empty_state.html.haml b/app/views/projects/feature_flags/_empty_state.html.haml index 3cc8873abe2a86..9b8c83328cfbfa 100644 --- a/app/views/projects/feature_flags/_empty_state.html.haml +++ b/app/views/projects/feature_flags/_empty_state.html.haml @@ -7,5 +7,8 @@ = s_('FeatureFlags|Feature flags allow you to configure your code into different flavors by dynamically toggling certain functionality.') = link_to 'More information', '#' - .text-center + .text-center.append-bottom-8 = render 'new_feature_flag_button' + + .text-center + = render 'configure_feature_flags_button' diff --git a/app/views/projects/feature_flags/_new_feature_flag_button.html.haml b/app/views/projects/feature_flags/_new_feature_flag_button.html.haml index 61bf64113d921c..c2901f41a6aa05 100644 --- a/app/views/projects/feature_flags/_new_feature_flag_button.html.haml +++ b/app/views/projects/feature_flags/_new_feature_flag_button.html.haml @@ -1,3 +1,3 @@ - if can?(current_user, :create_feature_flags, @project) - = link_to new_project_feature_flag_path(@project), class: 'btn btn-new' do + = link_to new_project_feature_flag_path(@project), class: 'btn btn-success' do = s_('FeatureFlags|New Feature Flag') diff --git a/app/views/projects/feature_flags/index.html.haml b/app/views/projects/feature_flags/index.html.haml index 72ab7e9a0cfa6e..bcdc6ac57fa6cf 100644 --- a/app/views/projects/feature_flags/index.html.haml +++ b/app/views/projects/feature_flags/index.html.haml @@ -1,17 +1,15 @@ -- page_title 'Feature Flags' - -%h3.page-title.with-button - Feature Flags - - .pull-right - %button.btn.btn-primary.btn-inverted.append-right-8{ type: 'button', data: { toggle: 'modal', target: '#configure-feature-flags-modal' } }> - = s_('FeatureFlags|Configure') - - = render 'new_feature_flag_button' +- 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' -- GitLab From ac554371fe2aefde6133eac31f67a71406fd39ad Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 24 Sep 2018 11:03:29 +0200 Subject: [PATCH 22/57] Replace deprecated CSS classes --- .../_configure_feature_flags_modal.html.haml | 6 +++--- app/views/projects/feature_flags/_form.html.haml | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/views/projects/feature_flags/_configure_feature_flags_modal.html.haml b/app/views/projects/feature_flags/_configure_feature_flags_modal.html.haml index 81816dd637fdb2..c660a7ae42b9e5 100644 --- a/app/views/projects/feature_flags/_configure_feature_flags_modal.html.haml +++ b/app/views/projects/feature_flags/_configure_feature_flags_modal.html.haml @@ -13,7 +13,7 @@ = link_to s_('FeatureFlags|More information'), '#' .form-group - = label_tag :api_url, s_('FeatureFlags|API URL'), class: 'label-light' + = label_tag :api_url, s_('FeatureFlags|API URL'), class: 'label-bold' .input-group = text_field_tag :api_url, unleash_api_url(@project), @@ -27,7 +27,7 @@ class: "input-group-text btn btn-default") .form-group - = label_tag :instance_id, s_('FeatureFlags|Instance ID'), class: 'label-light' + = label_tag :instance_id, s_('FeatureFlags|Instance ID'), class: 'label-bold' .input-group = text_field_tag :instance_id, unleash_api_instanceid(@project), @@ -41,7 +41,7 @@ class: "input-group-text btn btn-default") .form-group - = label_tag :application_name, s_('FeatureFlags|Application name'), class: 'label-light' + = label_tag :application_name, s_('FeatureFlags|Application name'), class: 'label-bold' .input-group = text_field_tag :application_name, "production", diff --git a/app/views/projects/feature_flags/_form.html.haml b/app/views/projects/feature_flags/_form.html.haml index 6aafc782f5b307..0fe5f93f80d33a 100644 --- a/app/views/projects/feature_flags/_form.html.haml +++ b/app/views/projects/feature_flags/_form.html.haml @@ -7,17 +7,17 @@ %fieldset .row .form-group.col-md-4 - = f.label :name, class: 'label-light', for: 'feature_flag_name' do + = 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-light', for: 'feature_flag_description' do + = 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-light' do + = f.label :active, class: 'label-bold' do = s_('FeatureFlags|Status') .form-check = f.check_box :active, id: 'feature_flag_status', class: 'form-check-input' @@ -25,8 +25,8 @@ = s_('FeatureFlags|Active') .form-actions - if @feature_flag.persisted? - = f.submit 'Save changes', class: "btn btn-save" + = f.submit 'Save changes', class: "btn btn-success" - else - = f.submit 'Create feature flag', class: "btn btn-save" + = f.submit 'Create feature flag', class: "btn btn-success" .float-right = link_to _('Cancel'), project_feature_flags_path(@project), class: 'btn btn-cancel' -- GitLab From eae3eb5c0a2f10ff340966109ae5fd0d95261386 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 24 Sep 2018 11:36:27 +0200 Subject: [PATCH 23/57] Add graphic to feature flag empty state --- .../feature_flags/_empty_state.html.haml | 24 +++++++++++-------- yarn.lock | 9 ++++--- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/app/views/projects/feature_flags/_empty_state.html.haml b/app/views/projects/feature_flags/_empty_state.html.haml index 9b8c83328cfbfa..6426bf481abbe9 100644 --- a/app/views/projects/feature_flags/_empty_state.html.haml +++ b/app/views/projects/feature_flags/_empty_state.html.haml @@ -1,14 +1,18 @@ .border-top .row.empty-state - .col-12 - .text-content - %h4.text-center= s_('FeatureFlags|Get started with feature flags') - %p.text-center - = s_('FeatureFlags|Feature flags allow you to configure your code into different flavors by dynamically toggling certain functionality.') - = link_to 'More information', '#' + .col-12 + .svg-content + = image_tag 'illustrations/feature_flag.svg' - .text-center.append-bottom-8 - = render 'new_feature_flag_button' + .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', '#' - .text-center - = render 'configure_feature_flags_button' + .text-center + = render 'new_feature_flag_button' + + .text-center + = render 'configure_feature_flags_button' diff --git a/yarn.lock b/yarn.lock index 0b8bd6c81c2ad3..daf00af00610f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -180,17 +180,16 @@ lodash "^4.17.10" to-fast-properties "^2.0.0" -"@gitlab-org/gitlab-svgs@^1.23.0", "@gitlab-org/gitlab-svgs@^1.29.0": - 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-ui@^1.7.1": version "1.7.1" resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-ui/-/gitlab-ui-1.7.1.tgz#e9cce86cb7e34311405e705c1de674276b453f17" integrity sha512-X12W39lFnWmfmYcHBokrauKvp6VLW9u0rFdgBXWlnrRL2hBShnyeBsBXKLMGUGnofMtgYv3iYO/rFW9IB79lFg== dependencies: - "@gitlab-org/gitlab-svgs" "^1.23.0" + "@gitlab-org/gitlab-svgs" "^1.30.0" bootstrap-vue "^2.0.0-rc.11" vue "^2.5.16" -- GitLab From 11b79321fd29dd25a17516ad42d946f29e57de38 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 24 Sep 2018 11:54:49 +0200 Subject: [PATCH 24/57] Add placeholder for feature flag documentation --- .../feature_flags/_configure_feature_flags_modal.html.haml | 6 ++++-- app/views/projects/feature_flags/_empty_state.html.haml | 2 +- doc/user/project/operations/feature_flags.md | 3 +++ 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 doc/user/project/operations/feature_flags.md diff --git a/app/views/projects/feature_flags/_configure_feature_flags_modal.html.haml b/app/views/projects/feature_flags/_configure_feature_flags_modal.html.haml index c660a7ae42b9e5..4968f898947114 100644 --- a/app/views/projects/feature_flags/_configure_feature_flags_modal.html.haml +++ b/app/views/projects/feature_flags/_configure_feature_flags_modal.html.haml @@ -9,8 +9,10 @@ %span{ "aria-hidden": true } × .modal-body %p - = s_('FeatureFlags|Install a compatible client library and specify the API URL, application name, and instance ID during the configuration setup.') - = link_to s_('FeatureFlags|More information'), '#' + - 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{}.html_safe, + docs_link_end: ''.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' diff --git a/app/views/projects/feature_flags/_empty_state.html.haml b/app/views/projects/feature_flags/_empty_state.html.haml index 6426bf481abbe9..667abd5f0a7e24 100644 --- a/app/views/projects/feature_flags/_empty_state.html.haml +++ b/app/views/projects/feature_flags/_empty_state.html.haml @@ -9,7 +9,7 @@ %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', '#' + = link_to 'More information', help_page_path("user/project/operations/feature_flags") .text-center = render 'new_feature_flag_button' diff --git a/doc/user/project/operations/feature_flags.md b/doc/user/project/operations/feature_flags.md new file mode 100644 index 00000000000000..1dbf485fea8af6 --- /dev/null +++ b/doc/user/project/operations/feature_flags.md @@ -0,0 +1,3 @@ +# Feature Flags + +## Client libraries -- GitLab From 9494a3741a578d2d4c70fd5e590403953eaea5f2 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 24 Sep 2018 12:36:05 +0200 Subject: [PATCH 25/57] Add feature flag name to delete modal header --- .../feature_flags/_delete_feature_flag_modal.html.haml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/views/projects/feature_flags/_delete_feature_flag_modal.html.haml b/app/views/projects/feature_flags/_delete_feature_flag_modal.html.haml index 7367e6795260d6..091c20871f5769 100644 --- a/app/views/projects/feature_flags/_delete_feature_flag_modal.html.haml +++ b/app/views/projects/feature_flags/_delete_feature_flag_modal.html.haml @@ -1,3 +1,5 @@ +- feature_flag_name = capture do + %span.text-monospace= feature_flag.name .modal{ id: "delete-feature-flag-modal-#{feature_flag.id}", tabindex: -1, role: 'dialog' } @@ -5,12 +7,12 @@ .modal-content .modal-header %h5.modal-title - = s_('FeatureFlags|Delete feature flag?') + = s_('FeatureFlags|Delete %{feature_flag_name}?').html_safe % { feature_flag_name: feature_flag_name } %button.close{ type: 'button', data: { dismiss: 'modal' }, aria: { label: _('Close') } } %span{ "aria-hidden": true } × .modal-body %p - = s_('FeatureFlags|Feature flag %{feature_flag_name} will be removed. Are you sure?') % { feature_flag_name: feature_flag.name } + = s_('FeatureFlags|Feature flag %{feature_flag_name} will be removed. Are you sure?').html_safe % { feature_flag_name: feature_flag_name } .modal-footer %button{ type: 'button', data: { dismiss: 'modal' }, class: 'btn btn-default' } Cancel -- GitLab From 37ebe5d81b332d014443897f34a8fe043e9dd06f Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 24 Sep 2018 12:42:28 +0200 Subject: [PATCH 26/57] Display column headers of feature flag table on small screen --- .../projects/feature_flags/_table.html.haml | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/app/views/projects/feature_flags/_table.html.haml b/app/views/projects/feature_flags/_table.html.haml index 21efebb4b1acf2..78b1125ba80ef9 100644 --- a/app/views/projects/feature_flags/_table.html.haml +++ b/app/views/projects/feature_flags/_table.html.haml @@ -7,16 +7,20 @@ .gl-responsive-table-row{ role: 'row' } .table-section.section-10{ role: 'gridcell' } - - if feature_flag.active? - %span.badge.badge-success - = s_('FeatureFlags|Active') - - else - %span.badge.badge-danger - = s_('FeatureFlags|Inactive') + .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.d-flex.flex-column{ role: 'gridcell' } - .text-monospace= feature_flag.name - .text-secondary= feature_flag.description + .table-section.section-50{ role: 'gridcell' } + .table-mobile-header{ role: "rowheader" }= s_('FeatureFlags|Feature Flag') + .table-mobile-content.d-flex.flex-column + .text-monospace= feature_flag.name + .text-secondary= feature_flag.description .table-section.section-40.table-button-footer{ role: 'gridcell' } .table-action-buttons.btn-group -- GitLab From f3ac8e7d514c899422e04207bf86e83a99b9cec9 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 24 Sep 2018 17:50:05 +0200 Subject: [PATCH 27/57] Put feature flags empty state buttons in same row --- app/views/projects/feature_flags/_empty_state.html.haml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/views/projects/feature_flags/_empty_state.html.haml b/app/views/projects/feature_flags/_empty_state.html.haml index 667abd5f0a7e24..ed8adf09cddba8 100644 --- a/app/views/projects/feature_flags/_empty_state.html.haml +++ b/app/views/projects/feature_flags/_empty_state.html.haml @@ -11,8 +11,5 @@ = 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") - .text-center - = render 'new_feature_flag_button' - - .text-center - = render 'configure_feature_flags_button' + = render 'new_feature_flag_button' + = render 'configure_feature_flags_button' -- GitLab From ec0d860cfa965423e86b60b5df1aef309cd76411 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Mon, 24 Sep 2018 19:08:20 +0200 Subject: [PATCH 28/57] Truncate long feature flag names and descriptions in table on small screens --- app/views/projects/feature_flags/_table.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/feature_flags/_table.html.haml b/app/views/projects/feature_flags/_table.html.haml index 78b1125ba80ef9..ca3420946d3c34 100644 --- a/app/views/projects/feature_flags/_table.html.haml +++ b/app/views/projects/feature_flags/_table.html.haml @@ -19,8 +19,8 @@ .table-section.section-50{ role: 'gridcell' } .table-mobile-header{ role: "rowheader" }= s_('FeatureFlags|Feature Flag') .table-mobile-content.d-flex.flex-column - .text-monospace= feature_flag.name - .text-secondary= feature_flag.description + .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 -- GitLab From dca61d1d5238a41813f66df381eadcceec90024a Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Tue, 25 Sep 2018 11:11:29 +0200 Subject: [PATCH 29/57] Truncate feature flag name in delete modal header --- .../_delete_feature_flag_modal.html.haml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/views/projects/feature_flags/_delete_feature_flag_modal.html.haml b/app/views/projects/feature_flags/_delete_feature_flag_modal.html.haml index 091c20871f5769..02269049b606d4 100644 --- a/app/views/projects/feature_flags/_delete_feature_flag_modal.html.haml +++ b/app/views/projects/feature_flags/_delete_feature_flag_modal.html.haml @@ -1,18 +1,20 @@ -- feature_flag_name = capture do - %span.text-monospace= feature_flag.name .modal{ id: "delete-feature-flag-modal-#{feature_flag.id}", tabindex: -1, role: 'dialog' } .modal-dialog{ role: 'document' } .modal-content .modal-header - %h5.modal-title - = s_('FeatureFlags|Delete %{feature_flag_name}?').html_safe % { feature_flag_name: feature_flag_name } + %h5.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 } × .modal-body %p - = s_('FeatureFlags|Feature flag %{feature_flag_name} will be removed. Are you sure?').html_safe % { feature_flag_name: feature_flag_name } + - 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 -- GitLab From d59bafb90b517a548c0a1c4d0fa2935614b36077 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Tue, 25 Sep 2018 11:11:52 +0200 Subject: [PATCH 30/57] Avoid close icon leaving the modal header --- app/assets/stylesheets/framework/modal.scss | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 7e30747963a592..b157f8c4568608 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -30,6 +30,17 @@ } } + // leave enough space for the close icon + .modal-title { + &.mw-100, + &.w-100 { + // after upgrading to Bootstrap 4.2 we can use $modal-header-padding-x here + // https://github.com/twbs/bootstrap/pull/26976 + margin-right: -2rem; + padding-right: 2rem; + } + } + .page-title { margin-top: 0; } -- GitLab From 0f9274b2be31fc553081087d7e7c85c21465d159 Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Wed, 26 Sep 2018 17:13:55 +0200 Subject: [PATCH 31/57] Limit feature flag description to 1000 characters --- app/models/operations/feature_flag.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb index 4ae91fa5890bc9..b78bae6ca3cfaf 100644 --- a/app/models/operations/feature_flag.rb +++ b/app/models/operations/feature_flag.rb @@ -12,5 +12,6 @@ class FeatureFlag < ActiveRecord::Base with: Gitlab::Regex.feature_flag_regex, message: Gitlab::Regex.feature_flag_regex_message } + validates :description, length: 0..1000 end end -- GitLab From 056d86db776ab7ce8f7d52783406af79a770591b Mon Sep 17 00:00:00 2001 From: Winnie Hellmann Date: Wed, 26 Sep 2018 17:15:09 +0200 Subject: [PATCH 32/57] Add feature tests for feature flags --- .../features/projects/feature_flags_spec.rb | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 ee/spec/features/projects/feature_flags_spec.rb diff --git a/ee/spec/features/projects/feature_flags_spec.rb b/ee/spec/features/projects/feature_flags_spec.rb new file mode 100644 index 00000000000000..ad66a4ecf8360a --- /dev/null +++ b/ee/spec/features/projects/feature_flags_spec.rb @@ -0,0 +1,187 @@ +require 'spec_helper' + +describe 'Feature Flags', :js do + using RSpec::Parameterized::TableSyntax + + invalid_input_table = proc do + 'with space' | '' | 'Name can contain only' + '