Commit 02d54ff3 authored by Jason Goodman's avatar Jason Goodman Committed by Shinya Maeda

Add Operations::FeatureFlags::UserList model

Add AtomicInternalId for model
parent ea0fa636
......@@ -3,7 +3,7 @@
module InternalIdEnums
def self.usage_resources
# when adding new resource, make sure it doesn't conflict with EE usage_resources
{ issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5, operations_feature_flags: 6 }
{ issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5, operations_feature_flags: 6, operations_user_lists: 7 }
end
end
......
---
title: Add public API for feature flag user lists
merge_request: 29415
author:
type: added
......@@ -31,6 +31,8 @@ The following API resources are available in the project context:
| [Environments](environments.md) | `/projects/:id/environments` |
| [Error Tracking](error_tracking.md) | `/projects/:id/error_tracking/settings` |
| [Events](events.md) | `/projects/:id/events` (also available for users and standalone) |
| [Feature Flags](feature_flags.md) | `/projects/:id/feature_flags` |
| [Feature Flag User Lists](feature_flag_user_lists.md) | `/projects/:id/feature_flags_user_lists` |
| [Issues](issues.md) | `/projects/:id/issues` (also available for groups and standalone) |
| [Issues Statistics](issues_statistics.md) | `/projects/:id/issues_statistics` (also available for groups and standalone) |
| [Issue boards](boards.md) | `/projects/:id/boards` |
......
# Feature Flag User Lists API **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/205409) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.10.
API for accessing GitLab Feature Flag User Lists.
Users with Developer or higher [permissions](../user/permissions.md) can access the Feature Flag User Lists API.
NOTE: **Note:**
`GET` requests return twenty results at a time because the API results
are [paginated](README.md#pagination). You can change this value.
## List all feature flag user lists for a project
Gets all feature flag user lists for the requested project.
```plaintext
GET /projects/:id/feature_flags_user_lists
```
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists
```
Example response:
```json
[
{
"name": "user_list",
"user_xids": "user1,user2",
"id": 1,
"iid": 1,
"project_id": 1,
"created_at": "2020-02-04T08:13:51.423Z",
"updated_at": "2020-02-04T08:13:51.423Z"
},
{
"name": "test_users",
"user_xids": "user3,user4,user5",
"id": 2,
"iid": 2,
"project_id": 1,
"created_at": "2020-02-04T08:13:10.507Z",
"updated_at": "2020-02-04T08:13:10.507Z"
}
]
```
## Create a feature flag user list
Creates a feature flag user list.
```plaintext
POST /projects/:id/feature_flags_user_lists
```
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| `name` | string | yes | The name of the feature flag. |
| `user_xids` | string | yes | A comma separated list of user ids. |
```shell
curl https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists \
--header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-type: application/json" \
--data @- << EOF
{
"name": "my_user_list",
"user_xids": "user1,user2,user3"
}
EOF
```
Example response:
```json
{
"name": "my_user_list",
"user_xids": "user1,user2,user3",
"id": 1,
"iid": 1,
"project_id": 1,
"created_at": "2020-02-04T08:32:27.288Z",
"updated_at": "2020-02-04T08:32:27.288Z"
}
```
## Get a feature flag user list
Gets a feature flag user list.
```plaintext
GET /projects/:id/feature_flags_user_lists/:iid
```
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| `iid` | integer/string | yes | The internal ID of the project's feature flag user list. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1
```
Example response:
```json
{
"name": "my_user_list",
"user_xids": "123,456",
"id": 1,
"iid": 1,
"project_id": 1,
"created_at": "2020-02-04T08:13:10.507Z",
"updated_at": "2020-02-04T08:13:10.507Z",
}
```
## Delete feature flag user list
Deletes a feature flag user list.
```plaintext
DELETE /projects/:id/feature_flags_user_lists/:iid
```
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| `iid` | integer/string | yes | The internal ID of the project's feature flag user list |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" --request DELETE https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1
```
......@@ -265,3 +265,4 @@ to control them in an automated flow:
- [Feature Flags API](../../../api/feature_flags.md)
- [Feature Flag Specs API](../../../api/feature_flag_specs.md)
- [Feature Flag User Lists API](../../../api/feature_flag_user_lists.md)
......@@ -85,6 +85,7 @@ module EE
has_many :operations_feature_flags, class_name: 'Operations::FeatureFlag'
has_one :operations_feature_flags_client, class_name: 'Operations::FeatureFlagsClient'
has_many :operations_feature_flags_user_lists, class_name: 'Operations::FeatureFlags::UserList'
has_many :project_aliases
......
# frozen_string_literal: true
module Operations
module FeatureFlags
class UserList < ApplicationRecord
include AtomicInternalId
USERXID_MAX_LENGTH = 256
self.table_name = 'operations_user_lists'
belongs_to :project
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.operations_feature_flags_user_lists&.maximum(:iid) }, presence: true
validates :project, presence: true
validates :name,
presence: true,
uniqueness: { scope: :project_id },
length: 1..255
validate :user_xids_validation
private
def user_xids_validation
unless user_xids.is_a?(String) && !user_xids.match(/[\n\r\t]|,,/) && valid_xids?(user_xids.split(","))
errors.add(:user_xids,
"user_xids must be a string of unique comma separated values each #{USERXID_MAX_LENGTH} characters or less")
end
end
def valid_xids?(user_xids)
user_xids.uniq.length == user_xids.length &&
user_xids.all? { |xid| valid_xid?(xid) }
end
def valid_xid?(user_xid)
user_xid.present? &&
user_xid.strip == user_xid &&
user_xid.length <= USERXID_MAX_LENGTH
end
end
end
end
......@@ -182,6 +182,7 @@ module EE
enable :update_feature_flag
enable :destroy_feature_flag
enable :admin_feature_flag
enable :admin_feature_flags_user_lists
enable :create_design
enable :destroy_design
end
......@@ -218,6 +219,7 @@ module EE
rule { feature_flags_disabled | repository_disabled }.policy do
prevent(*create_read_update_admin_destroy(:feature_flag))
prevent(:admin_feature_flags_user_lists)
end
rule { can?(:maintainer_access) }.policy do
......
# frozen_string_literal: true
module API
class FeatureFlagsUserLists < Grape::API
include PaginationParams
error_formatter :json, -> (message, _backtrace, _options, _env, _original_exception) {
message.is_a?(String) ? { message: message }.to_json : message.to_json
}
before do
authorize_admin_feature_flags_user_lists!
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource 'projects/:id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
resource :feature_flags_user_lists do
desc 'Get all feature flags user lists of a project' do
detail 'This feature was introduced in GitLab 12.10'
success EE::API::Entities::FeatureFlag::UserList
end
params do
use :pagination
end
get do
present paginate(user_project.operations_feature_flags_user_lists),
with: EE::API::Entities::FeatureFlag::UserList
end
desc 'Create a feature flags user list for a project' do
detail 'This feature was introduced in GitLab 12.10'
success EE::API::Entities::FeatureFlag::UserList
end
params do
requires :name, type: String, desc: 'The name of the list'
requires :user_xids, type: String, desc: 'A comma separated list of external user ids'
end
post do
list = user_project.operations_feature_flags_user_lists.create(declared_params)
if list.save
present list, with: EE::API::Entities::FeatureFlag::UserList
else
render_api_error!(list.errors.full_messages, :bad_request)
end
end
end
params do
requires :iid, type: String, desc: 'The internal id of the user list'
end
resource 'feature_flags_user_lists/:iid' do
desc 'Get a single feature flag user list belonging to a project' do
detail 'This feature was introduced in GitLab 12.10'
success EE::API::Entities::FeatureFlag::UserList
end
get do
present user_project.operations_feature_flags_user_lists.find_by_iid!(params[:iid]),
with: EE::API::Entities::FeatureFlag::UserList
end
desc 'Update a feature flag user list' do
detail 'This feature was introduced in GitLab 12.10'
success EE::API::Entities::FeatureFlag::UserList
end
params do
optional :name, type: String, desc: 'The name of the list'
optional :user_xids, type: String, desc: 'A comma separated list of external user ids'
end
put do
list = user_project.operations_feature_flags_user_lists.find_by_iid!(params[:iid])
if list.update(declared_params(include_missing: false))
present list, with: EE::API::Entities::FeatureFlag::UserList
else
render_api_error!(list.errors.full_messages, :bad_request)
end
end
desc 'Delete a feature flag user list' do
detail 'This feature was introduced in GitLab 12.10'
end
delete do
list = user_project.operations_feature_flags_user_lists.find_by_iid!(params[:iid])
list.destroy
end
end
end
helpers do
def authorize_admin_feature_flags_user_lists!
authorize! :admin_feature_flags_user_lists, user_project
end
end
end
end
......@@ -21,6 +21,7 @@ module EE
mount ::API::Epics
mount ::API::ElasticsearchIndexedNamespaces
mount ::API::FeatureFlags
mount ::API::FeatureFlagsUserLists
mount ::API::FeatureFlagScopes
mount ::API::Geo
mount ::API::GeoReplication
......
# frozen_string_literal: true
module EE
module API
module Entities
class FeatureFlag < Grape::Entity
class UserList < Grape::Entity
expose :id
expose :iid
expose :project_id
expose :created_at
expose :updated_at
expose :name
expose :user_xids
end
end
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :operations_feature_flag_user_list, class: 'Operations::FeatureFlags::UserList' do
association :project, factory: :project
name { 'My User List' }
user_xids { 'user1,user2,user3' }
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Operations::FeatureFlags::UserList do
subject { create(:operations_feature_flag_user_list) }
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
it { is_expected.to validate_length_of(:name).is_at_least(1).is_at_most(255) }
describe 'user_xids' do
where(:valid_value) do
["", "sam", "1", "a", "uuid-of-some-kind", "sam,fred,tom,jane,joe,mike",
"gitlab@example.com", "123,4", "UPPER,Case,charActeRS", "0",
"$valid$email#2345#$%..{}+=-)?\\/@example.com", "spaces allowed",
"a" * 256, "a,#{'b' * 256},ccc", "many spaces"]
end
with_them do
it 'is valid with a string of comma separated values' do
user_list = described_class.create(user_xids: valid_value)
expect(user_list.errors[:user_xids]).to be_empty
end
end
where(:typecast_value) do
[1, 2.5, {}, []]
end
with_them do
it 'automatically casts values of other types' do
user_list = described_class.create(user_xids: typecast_value)
expect(user_list.errors[:user_xids]).to be_empty
expect(user_list.user_xids).to eq(typecast_value.to_s)
end
end
where(:invalid_value) do
[nil, "123\n456", "1,2,3,12\t3", "\n", "\n\r",
"joe\r,sam", "1,2,2", "1,,2", "1,2,,,,", "b" * 257, "1, ,2", "tim, ,7", " ",
" ", " ,1", "1, ", " leading,1", "1,trailing ", "1, both ,2"]
end
with_them do
it 'is invalid' do
user_list = described_class.create(user_xids: invalid_value)
expect(user_list.errors[:user_xids]).to include(
'user_xids must be a string of unique comma separated values each 256 characters or less'
)
end
end
end
end
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
let(:instance) { build(:operations_feature_flag_user_list) }
let(:scope) { :project }
let(:scope_attrs) { { project: instance.project } }
let(:usage) { :operations_user_lists }
end
end
# frozen_string_literal: true
require 'spec_helper'
describe API::FeatureFlagsUserLists do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:reporter) { create(:user) }
before_all do
project.add_developer(developer)
project.add_reporter(reporter)
end
before do
stub_licensed_features(feature_flags: true)
end
def create_list(name: 'mylist', user_xids: 'user1')
create(:operations_feature_flag_user_list, project: project, name: name, user_xids: user_xids)
end
describe 'GET /projects/:id/feature_flags_user_lists' do
it 'forbids the request for a reporter' do
get api("/projects/#{project.id}/feature_flags_user_lists", reporter)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'returns forbidden if the feature is unavailable' do
stub_licensed_features(feature_flags: false)
get api("/projects/#{project.id}/feature_flags_user_lists", developer)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'returns all the user lists' do
create_list(name: 'list_a', user_xids: 'user1')
create_list(name: 'list_b', user_xids: 'user1,user2,user3')
get api("/projects/#{project.id}/feature_flags_user_lists", developer)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.map { |list| list['name'] }.sort).to eq(%w[list_a list_b])
end
it 'returns all the data for a user list' do
user_list = create_list(name: 'list_a', user_xids: 'user1')
get api("/projects/#{project.id}/feature_flags_user_lists", developer)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq([{
'id' => user_list.id,
'iid' => user_list.iid,
'project_id' => project.id,
'created_at' => user_list.created_at.as_json,
'updated_at' => user_list.updated_at.as_json,
'name' => 'list_a',
'user_xids' => 'user1'
}])
end
it 'paginates user lists' do
create_list(name: 'list_a', user_xids: 'user1')
create_list(name: 'list_b', user_xids: 'user1,user2,user3')
get api("/projects/#{project.id}/feature_flags_user_lists?page=2&per_page=1", developer)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.map { |list| list['name'] }).to eq(['list_b'])
end
it 'returns the user lists for only the specified project' do
create(:operations_feature_flag_user_list, project: project, name: 'list')
other_project = create(:project)
create(:operations_feature_flag_user_list, project: other_project, name: 'other_list')
get api("/projects/#{project.id}/feature_flags_user_lists", developer)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.map { |list| list['name'] }).to eq(['list'])
end
it 'returns an empty list' do
get api("/projects/#{project.id}/feature_flags_user_lists", developer)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq([])
end
end
describe 'GET /projects/:id/feature_flags_user_lists/:iid' do
it 'forbids the request for a reporter' do
list = create_list
get api("/projects/#{project.id}/feature_flags_user_lists/#{list.iid}", reporter)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'returns forbidden if the feature is unavailable' do
stub_licensed_features(feature_flags: false)
list = create_list
get api("/projects/#{project.id}/feature_flags_user_lists/#{list.iid}", developer)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'returns the feature flag' do
list = create_list(name: 'testers', user_xids: 'test1,test2')
get api("/projects/#{project.id}/feature_flags_user_lists/#{list.iid}", developer)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq({
'name' => 'testers',
'user_xids' => 'test1,test2',
'id' => list.id,
'iid' => list.iid,
'project_id' => project.id,
'created_at' => list.created_at.as_json,
'updated_at' => list.updated_at.as_json
})
end
it 'returns the correct feature flag identified by the iid' do
create_list(name: 'list_a', user_xids: 'test1')
list_b = create_list(name: 'list_b', user_xids: 'test2')
get api("/projects/#{project.id}/feature_flags_user_lists/#{list_b.iid}", developer)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq('list_b')
end
it 'scopes the iid search to the project' do
other_project = create(:project)
other_project.add_developer(developer)
create(:operations_feature_flag_user_list, project: other_project, name: 'other_list')
list = create(:operations_feature_flag_user_list, project: project, name: 'list')
get api("/projects/#{project.id}/feature_flags_user_lists/#{list.iid}", developer)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq('list')
end
it 'returns not found when the list does not exist' do
get api("/projects/#{project.id}/feature_flags_user_lists/1", developer)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response).to eq({ 'message' => '404 Not found' })
end
end
describe 'POST /projects/:id/feature_flags_user_lists' do
it 'forbids the request for a reporter' do
post api("/projects/#{project.id}/feature_flags_user_lists", reporter), params: {
name: 'mylist', user_xids: 'user1'
}
expect(response).to have_gitlab_http_status(:forbidden)
expect(project.operations_feature_flags_user_lists.count).to eq(0)
end
it 'returns forbidden if the feature is unavailable' do
stub_licensed_features(feature_flags: false)
post api("/projects/#{project.id}/feature_flags_user_lists", developer), params: {
name: 'mylist', user_xids: 'user1'
}
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'creates the flag' do
post api("/projects/#{project.id}/feature_flags_user_lists", developer), params: {
name: 'mylist', user_xids: 'user1'
}
expect(response).to have_gitlab_http_status(:created)
expect(json_response.slice('name', 'user_xids', 'project_id', 'iid')).to eq({
'name' => 'mylist',
'user_xids' => 'user1',
'project_id' => project.id,
'iid' => 1
})
expect(project.operations_feature_flags_user_lists.count).to eq(1)
expect(project.operations_feature_flags_user_lists.last.name).to eq('mylist')
end
it 'requires name' do
post api("/projects/#{project.id}/feature_flags_user_lists", developer), params: {
user_xids: 'user1'
}
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq({ 'message' => 'name is missing' })
expect(project.operations_feature_flags_user_lists.count).to eq(0)
end
it 'requires user_xids' do
post api("/projects/#{project.id}/feature_flags_user_lists", developer), params: {
name: 'empty_list'
}
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq({ 'message' => 'user_xids is missing' })
expect(project.operations_feature_flags_user_lists.count).to eq(0)
end
it 'returns an error when name is already taken' do
create_list(name: 'myname')
post api("/projects/#{project.id}/feature_flags_user_lists", developer), params: {
name: 'myname', user_xids: 'a'
}
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq({ 'message' => ['Name has already been taken'] })
expect(project.operations_feature_flags_user_lists.count).to eq(1)
end
it 'does not create a flag for a project of which the developer is not a member' do
other_project = create(:project)
post api("/projects/#{other_project.id}/feature_flags_user_lists", developer), params: {
name: 'mylist', user_xids: 'user1'
}
expect(response).to have_gitlab_http_status(:not_found)
expect(other_project.operations_feature_flags_user_lists.count).to eq(0)
expect(project.operations_feature_flags_user_lists.count).to eq(0)
end
end
describe 'PUT /projects/:id/feature_flags_user_lists/:iid' do
it 'forbids the request for a reporter' do
list = create_list(name: 'original_name')
put api("/projects/#{project.id}/feature_flags_user_lists/#{list.iid}", reporter), params: {
name: 'mylist'
}
expect(response).to have_gitlab_http_status(:forbidden)
expect(list.reload.name).to eq('original_name')
end
it 'returns forbidden if the feature is unavailable' do
list = create_list(name: 'original_name')
stub_licensed_features(feature_flags: false)
put api("/projects/#{project.id}/feature_flags_user_lists/#{list.iid}", developer), params: {
name: 'mylist', user_xids: '456,789'
}
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'updates the list' do
list = create_list(name: 'original_name', user_xids: '123')
put api("/projects/#{project.id}/feature_flags_user_lists/#{list.iid}", developer), params: {
name: 'mylist', user_xids: '456,789'
}
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.slice('name', 'user_xids')).to eq({
'name' => 'mylist',
'user_xids' => '456,789'
})
expect(list.reload.name).to eq('mylist')
end
it 'preserves attributes not listed in the request' do
list = create_list(name: 'original_name', user_xids: '123')
put api("/projects/#{project.id}/feature_flags_user_lists/#{list.iid}", developer), params: {}
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.slice('name', 'user_xids')).to eq({
'name' => 'original_name',
'user_xids' => '123'
})
expect(list.reload.name).to eq('original_name')
expect(list.reload.user_xids).to eq('123')
end
it 'returns an error when the update is invalid' do
create_list(name: 'taken', user_xids: '123')
list = create_list(name: 'original_name', user_xids: '123')
put api("/projects/#{project.id}/feature_flags_user_lists/#{list.iid}", developer), params: {
name: 'taken'
}
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq({ 'message' => ['Name has already been taken'] })
end
it 'returns not found when the list does not exist' do
list = create_list(name: 'original_name', user_xids: '123')
put api("/projects/#{project.id}/feature_flags_user_lists/#{list.iid + 1}", developer), params: {
name: 'new_name'
}
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response).to eq({ 'message' => '404 Not found' })
end
end
describe 'DELETE /projects/:id/feature_flags_user_lists/:iid' do
it 'forbids the request for a reporter' do
list = create_list
delete api("/projects/#{project.id}/feature_flags_user_lists/#{list.iid}", reporter)
expect(response).to have_gitlab_http_status(:forbidden)
expect(project.operations_feature_flags_user_lists.count).to eq(1)
end
it 'returns forbidden if the feature is unavailable' do
list = create_list
stub_licensed_features(feature_flags: false)
delete api("/projects/#{project.id}/feature_flags_user_lists/#{list.iid}", developer)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'returns not found when the list does not exist' do
delete api("/projects/#{project.id}/feature_flags_user_lists/1", developer)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response).to eq({ 'message' => '404 Not found' })
end
it 'deletes the list' do
list = create_list
delete api("/projects/#{project.id}/feature_flags_user_lists/#{list.iid}", developer)
expect(response).to have_gitlab_http_status(:ok)
expect(project.operations_feature_flags_user_lists.count).to eq(0)
end
end
end
......@@ -443,6 +443,7 @@ project:
- vulnerability_scanners
- operations_feature_flags
- operations_feature_flags_client
- operations_feature_flags_user_lists
- prometheus_alerts
- prometheus_alert_events
- self_managed_prometheus_alert_events
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment