Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
3d969a96
Commit
3d969a96
authored
Jan 09, 2019
by
Scott Hampton
Committed by
Filipa Lacerda
Jan 09, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Resolve "Feature Flags - Table View filter tabs"
parent
2b249629
Changes
17
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
877 additions
and
35 deletions
+877
-35
ee/app/assets/javascripts/feature_flags/components/delete_feature_flag.vue
...ascripts/feature_flags/components/delete_feature_flag.vue
+88
-0
ee/app/assets/javascripts/feature_flags/components/feature_flags.vue
...ts/javascripts/feature_flags/components/feature_flags.vue
+179
-0
ee/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
...ascripts/feature_flags/components/feature_flags_table.vue
+90
-0
ee/app/assets/javascripts/feature_flags/store/actions.js
ee/app/assets/javascripts/feature_flags/store/actions.js
+28
-0
ee/app/assets/javascripts/feature_flags/store/index.js
ee/app/assets/javascripts/feature_flags/store/index.js
+16
-0
ee/app/assets/javascripts/feature_flags/store/mutation_types.js
.../assets/javascripts/feature_flags/store/mutation_types.js
+6
-0
ee/app/assets/javascripts/feature_flags/store/mutations.js
ee/app/assets/javascripts/feature_flags/store/mutations.js
+32
-0
ee/app/assets/javascripts/feature_flags/store/state.js
ee/app/assets/javascripts/feature_flags/store/state.js
+9
-0
ee/app/assets/javascripts/pages/projects/feature_flags/index.js
.../assets/javascripts/pages/projects/feature_flags/index.js
+29
-0
ee/app/views/projects/feature_flags/index.html.haml
ee/app/views/projects/feature_flags/index.html.haml
+17
-4
ee/changelogs/unreleased/7731-feature-flags-table-view-filter-tabs.yml
.../unreleased/7731-feature-flags-table-view-filter-tabs.yml
+4
-0
ee/spec/controllers/projects/feature_flags_controller_spec.rb
...pec/controllers/projects/feature_flags_controller_spec.rb
+0
-2
ee/spec/features/projects/feature_flags_spec.rb
ee/spec/features/projects/feature_flags_spec.rb
+124
-29
ee/spec/javascripts/projects/feature_flags/feature_flags_spec.js
.../javascripts/projects/feature_flags/feature_flags_spec.js
+160
-0
ee/spec/javascripts/projects/feature_flags/feature_flags_table_spec.js
...cripts/projects/feature_flags/feature_flags_table_spec.js
+54
-0
ee/spec/javascripts/projects/feature_flags/mock_data.js
ee/spec/javascripts/projects/feature_flags/mock_data.js
+23
-0
locale/gitlab.pot
locale/gitlab.pot
+18
-0
No files found.
ee/app/assets/javascripts/feature_flags/components/delete_feature_flag.vue
0 → 100644
View file @
3d969a96
<
script
>
import
_
from
'
underscore
'
;
import
{
s__
,
sprintf
}
from
'
~/locale
'
;
import
{
GlButton
,
GlModal
,
GlModalDirective
,
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
export
default
{
components
:
{
GlButton
,
GlModal
,
Icon
,
},
directives
:
{
GlModal
:
GlModalDirective
,
GlTooltip
:
GlTooltipDirective
,
},
props
:
{
deleteFeatureFlagUrl
:
{
type
:
String
,
required
:
true
,
},
featureFlagName
:
{
type
:
String
,
required
:
true
,
},
modalId
:
{
type
:
String
,
required
:
true
,
},
csrfToken
:
{
type
:
String
,
required
:
true
,
},
},
computed
:
{
message
()
{
return
sprintf
(
s__
(
'
FeatureFlags|Feature flag %{name} will be removed. Are you sure?
'
),
{
name
:
_
.
escape
(
this
.
featureFlagName
),
},
false
,
);
},
title
()
{
return
sprintf
(
s__
(
'
FeatureFlags|Delete %{name}?
'
),
{
name
:
_
.
escape
(
this
.
featureFlagName
),
},
false
,
);
},
},
methods
:
{
onSubmit
()
{
this
.
$refs
.
form
.
submit
();
},
},
};
</
script
>
<
template
>
<div
class=
"d-inline-block"
>
<gl-button
v-gl-tooltip.hover.bottom=
"__('Delete')"
v-gl-modal=
"modalId"
class=
"js-feature-flag-delete-button"
variant=
"danger"
>
<icon
name=
"remove"
:size=
"16"
/>
</gl-button>
<gl-modal
:title=
"title"
:ok-title=
"s__('FeatureFlags|Delete feature flag')"
:modal-id=
"modalId"
title-tag=
"h4"
ok-variant=
"danger"
@
ok=
"onSubmit"
>
{{
message
}}
<form
ref=
"form"
:action=
"deleteFeatureFlagUrl"
method=
"post"
class=
"js-requires-input"
>
<input
ref=
"method"
type=
"hidden"
name=
"_method"
value=
"delete"
/>
<input
:value=
"csrfToken"
type=
"hidden"
name=
"authenticity_token"
/>
</form>
</gl-modal>
</div>
</
template
>
ee/app/assets/javascripts/feature_flags/components/feature_flags.vue
0 → 100644
View file @
3d969a96
<
script
>
import
{
GlEmptyState
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
FeatureFlagsTable
from
'
./feature_flags_table.vue
'
;
import
store
from
'
../store
'
;
import
{
__
}
from
'
~/locale
'
;
import
NavigationTabs
from
'
~/vue_shared/components/navigation_tabs.vue
'
;
import
TablePagination
from
'
~/vue_shared/components/table_pagination.vue
'
;
import
{
getParameterByName
,
historyPushState
,
buildUrlWithCurrentLocation
,
}
from
'
~/lib/utils/common_utils
'
;
export
default
{
store
,
components
:
{
FeatureFlagsTable
,
NavigationTabs
,
TablePagination
,
GlEmptyState
,
GlLoadingIcon
,
},
props
:
{
endpoint
:
{
type
:
String
,
required
:
true
,
},
csrfToken
:
{
type
:
String
,
required
:
true
,
},
errorStateSvgPath
:
{
type
:
String
,
required
:
true
,
},
featureFlagsHelpPagePath
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
scope
:
getParameterByName
(
'
scope
'
)
||
this
.
$options
.
scopes
.
all
,
page
:
getParameterByName
(
'
page
'
)
||
'
1
'
,
};
},
scopes
:
{
all
:
'
all
'
,
enabled
:
'
enabled
'
,
disabled
:
'
disabled
'
,
},
computed
:
{
...
mapState
([
'
featureFlags
'
,
'
count
'
,
'
pageInfo
'
,
'
isLoading
'
,
'
hasError
'
,
'
options
'
]),
shouldRenderTabs
()
{
/* Do not show tabs until after the first request to get the count */
return
this
.
count
.
all
!==
undefined
;
},
shouldRenderPagination
()
{
return
(
!
this
.
isLoading
&&
!
this
.
hasError
&&
this
.
featureFlags
.
length
&&
this
.
pageInfo
.
total
>
this
.
pageInfo
.
perPage
);
},
shouldShowEmptyState
()
{
return
!
this
.
isLoading
&&
!
this
.
hasError
&&
this
.
featureFlags
.
length
===
0
;
},
shouldRenderTable
()
{
return
!
this
.
isLoading
&&
this
.
featureFlags
.
length
>
0
&&
!
this
.
hasError
;
},
shouldRenderErrorState
()
{
return
this
.
hasError
&&
!
this
.
isLoading
;
},
tabs
()
{
const
{
scopes
}
=
this
.
$options
;
return
[
{
name
:
__
(
'
All
'
),
scope
:
scopes
.
all
,
count
:
this
.
count
.
all
,
isActive
:
this
.
scope
===
scopes
.
all
,
},
{
name
:
__
(
'
Enabled
'
),
scope
:
scopes
.
enabled
,
count
:
this
.
count
.
enabled
,
isActive
:
this
.
scope
===
scopes
.
enabled
,
},
{
name
:
__
(
'
Disabled
'
),
scope
:
scopes
.
disabled
,
count
:
this
.
count
.
disabled
,
isActive
:
this
.
scope
===
scopes
.
disabled
,
},
];
},
},
created
()
{
this
.
setFeatureFlagsEndpoint
(
this
.
endpoint
);
this
.
setFeatureFlagsOptions
({
scope
:
this
.
scope
,
page
:
this
.
page
});
this
.
fetchFeatureFlags
();
},
methods
:
{
...
mapActions
([
'
setFeatureFlagsEndpoint
'
,
'
setFeatureFlagsOptions
'
,
'
fetchFeatureFlags
'
]),
onChangeTab
(
scope
)
{
this
.
scope
=
scope
;
this
.
updateFeatureFlagOptions
({
scope
,
page
:
'
1
'
,
});
},
onChangePage
(
page
)
{
this
.
updateFeatureFlagOptions
({
scope
:
this
.
scope
,
/* URLS parameters are strings, we need to parse to match types */
page
:
Number
(
page
).
toString
(),
});
},
updateFeatureFlagOptions
(
parameters
)
{
const
queryString
=
Object
.
keys
(
parameters
)
.
map
(
parameter
=>
{
const
value
=
parameters
[
parameter
];
return
`
${
parameter
}
=
${
encodeURIComponent
(
value
)}
`
;
})
.
join
(
'
&
'
);
historyPushState
(
buildUrlWithCurrentLocation
(
`?
${
queryString
}
`
));
this
.
setFeatureFlagsOptions
(
parameters
);
this
.
fetchFeatureFlags
();
},
},
};
</
script
>
<
template
>
<div>
<div
v-if=
"shouldRenderTabs"
class=
"top-area scrolling-tabs-container inner-page-scroll-tabs"
>
<navigation-tabs
:tabs=
"tabs"
scope=
"featureflags"
@
onChangeTab=
"onChangeTab"
/>
</div>
<gl-loading-icon
v-if=
"isLoading"
:label=
"s__('Pipelines|Loading Pipelines')"
:size=
"3"
class=
"prepend-top-20"
/>
<template
v-else-if=
"shouldRenderErrorState"
>
<gl-empty-state
:title=
"s__(`FeatureFlags|There was an error fetching the feature flags.`)"
:description=
"s__(`FeatureFlags|Try again in a few moments or contact your support team.`)"
:svg-path=
"errorStateSvgPath"
/>
</
template
>
<
template
v-else-if=
"shouldShowEmptyState"
>
<gl-empty-state
class=
"js-feature-flags-empty-state"
:title=
"s__(`FeatureFlags|Get started with feature flags`)"
:description=
"
s__(
`FeatureFlags|Feature flags allow you to configure your code into different flavors by dynamically toggling certain functionality.`,
)
"
:svg-path=
"errorStateSvgPath"
:primary-button-link=
"featureFlagsHelpPagePath"
:primary-button-text=
"s__(`FeatureFlags|More Information`)"
/>
</
template
>
<
template
v-else-if=
"shouldRenderTable"
>
<feature-flags-table
:csrf-token=
"csrfToken"
:feature-flags=
"featureFlags"
/>
</
template
>
<table-pagination
v-if=
"shouldRenderPagination"
:change=
"onChangePage"
:page-info=
"pageInfo"
/>
</div>
</template>
ee/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
0 → 100644
View file @
3d969a96
<
script
>
import
{
GlButton
,
GlLink
,
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
DeleteFeatureFlag
from
'
./delete_feature_flag.vue
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
export
default
{
components
:
{
DeleteFeatureFlag
,
GlButton
,
GlLink
,
Icon
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
props
:
{
csrfToken
:
{
type
:
String
,
required
:
true
,
},
featureFlags
:
{
type
:
Array
,
required
:
true
,
},
},
};
</
script
>
<
template
>
<div
class=
"table-holder js-feature-flag-table"
>
<div
class=
"gl-responsive-table-row table-row-header"
role=
"row"
>
<div
class=
"table-section section-10"
role=
"columnheader"
>
{{
s__
(
'
FeatureFlags|Status
'
)
}}
</div>
<div
class=
"table-section section-50"
role=
"columnheader"
>
{{
s__
(
'
FeatureFlags|Feature flag
'
)
}}
</div>
</div>
<template
v-for=
"featureFlag in featureFlags"
>
<div
:key=
"featureFlag.id"
class=
"gl-responsive-table-row"
role=
"row"
>
<div
class=
"table-section section-10"
role=
"gridcell"
>
<div
class=
"table-mobile-header"
role=
"rowheader"
>
{{
s__
(
'
FeatureFlags|Status
'
)
}}
</div>
<div
class=
"table-mobile-content js-feature-flag-status"
>
<template
v-if=
"featureFlag.active"
>
<span
class=
"badge badge-success"
>
{{
s__
(
'
FeatureFlags|Active
'
)
}}
</span>
</
template
>
<
template
v-else
>
<span
class=
"badge badge-danger"
>
{{
s__
(
'
FeatureFlags|Inactive
'
)
}}
</span>
</
template
>
</div>
</div>
<div
class=
"table-section section-50"
role=
"gridcell"
>
<div
class=
"table-mobile-header"
role=
"rowheader"
>
{{ s__('FeatureFlags|Feature Flag') }}
</div>
<div
class=
"table-mobile-content d-flex flex-column js-feature-flag-title"
>
<div
class=
"feature-flag-name text-monospace text-truncate"
>
{{ featureFlag.name }}
</div>
<div
class=
"feature-flag-description text-secondary text-truncate"
>
{{ featureFlag.description }}
</div>
</div>
</div>
<div
class=
"table-section section-40 table-button-footer"
role=
"gridcell"
>
<div
class=
"table-action-buttons btn-group"
>
<
template
v-if=
"featureFlag.edit_path"
>
<gl-button
v-gl-tooltip.hover.bottom=
"__('Edit')"
class=
"js-feature-flag-edit-button"
:href=
"featureFlag.edit_path"
variant=
"outline-primary"
>
<icon
name=
"pencil"
:size=
"16"
/>
</gl-button>
</
template
>
<
template
v-if=
"featureFlag.destroy_path"
>
<delete-feature-flag
:delete-feature-flag-url=
"featureFlag.destroy_path"
:feature-flag-name=
"featureFlag.name"
:modal-id=
"`delete-feature-flag-$
{featureFlag.id}`"
:csrf-token="csrfToken"
/>
</
template
>
</div>
</div>
</div>
</template>
</div>
</template>
ee/app/assets/javascripts/feature_flags/store/actions.js
0 → 100644
View file @
3d969a96
import
*
as
types
from
'
./mutation_types
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
export
const
requestFeatureFlags
=
({
commit
})
=>
commit
(
types
.
REQUEST_FEATURE_FLAGS
);
export
const
receiveFeatureFlagsSuccess
=
({
commit
},
response
)
=>
commit
(
types
.
RECEIVE_FEATURE_FLAGS_SUCCESS
,
response
);
export
const
receiveFeatureFlagsError
=
({
commit
},
error
)
=>
commit
(
types
.
RECEIVE_FEATURE_FLAGS_ERROR
,
error
);
export
const
fetchFeatureFlags
=
({
state
,
dispatch
})
=>
{
dispatch
(
'
requestFeatureFlags
'
);
axios
.
get
(
state
.
endpoint
,
{
params
:
state
.
options
,
})
.
then
(
response
=>
dispatch
(
'
receiveFeatureFlagsSuccess
'
,
response
))
.
catch
(
error
=>
dispatch
(
'
receiveFeatureFlagsError
'
,
error
));
};
export
const
setFeatureFlagsEndpoint
=
({
commit
},
endpoint
)
=>
commit
(
types
.
SET_FEATURE_FLAGS_ENDPOINT
,
endpoint
);
export
const
setFeatureFlagsOptions
=
({
commit
},
options
)
=>
commit
(
types
.
SET_FEATURE_FLAGS_OPTIONS
,
options
);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export
default
()
=>
{};
ee/app/assets/javascripts/feature_flags/store/index.js
0 → 100644
View file @
3d969a96
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
state
from
'
./state
'
;
import
*
as
actions
from
'
./actions
'
;
import
mutations
from
'
./mutations
'
;
Vue
.
use
(
Vuex
);
export
const
createStore
=
()
=>
new
Vuex
.
Store
({
actions
,
mutations
,
state
,
});
export
default
createStore
();
ee/app/assets/javascripts/feature_flags/store/mutation_types.js
0 → 100644
View file @
3d969a96
export
const
SET_FEATURE_FLAGS_ENDPOINT
=
'
SET_FEATURE_FLAGS_ENDPOINT
'
;
export
const
SET_FEATURE_FLAGS_OPTIONS
=
'
SET_FEATURE_FLAGS_OPTIONS
'
;
export
const
REQUEST_FEATURE_FLAGS
=
'
REQUEST_FEATURE_FLAGS
'
;
export
const
RECEIVE_FEATURE_FLAGS_SUCCESS
=
'
RECEIVE_FEATURE_FLAGS_SUCCESS
'
;
export
const
RECEIVE_FEATURE_FLAGS_ERROR
=
'
RECEIVE_FEATURE_FLAGS_ERROR
'
;
ee/app/assets/javascripts/feature_flags/store/mutations.js
0 → 100644
View file @
3d969a96
import
*
as
types
from
'
./mutation_types
'
;
import
{
parseIntPagination
,
normalizeHeaders
}
from
'
~/lib/utils/common_utils
'
;
export
default
{
[
types
.
SET_FEATURE_FLAGS_ENDPOINT
](
state
,
endpoint
)
{
state
.
endpoint
=
endpoint
;
},
[
types
.
SET_FEATURE_FLAGS_OPTIONS
](
state
,
options
=
{})
{
state
.
options
=
options
;
},
[
types
.
REQUEST_FEATURE_FLAGS
](
state
)
{
state
.
isLoading
=
true
;
},
[
types
.
RECEIVE_FEATURE_FLAGS_SUCCESS
](
state
,
response
)
{
state
.
isLoading
=
false
;
state
.
featureFlags
=
response
.
data
.
feature_flags
;
state
.
count
=
response
.
data
.
count
;
let
paginationInfo
;
if
(
Object
.
keys
(
response
.
headers
).
length
)
{
const
normalizedHeaders
=
normalizeHeaders
(
response
.
headers
);
paginationInfo
=
parseIntPagination
(
normalizedHeaders
);
}
else
{
paginationInfo
=
response
.
headers
;
}
state
.
pageInfo
=
paginationInfo
;
},
[
types
.
RECEIVE_FEATURE_FLAGS_ERROR
](
state
)
{
state
.
isLoading
=
false
;
state
.
hasError
=
true
;
},
};
ee/app/assets/javascripts/feature_flags/store/state.js
0 → 100644
View file @
3d969a96
export
default
()
=>
({
featureFlags
:
[],
count
:
{},
pageInfo
:
{},
isLoading
:
true
,
hasError
:
false
,
endpoint
:
null
,
options
:
{},
});
ee/app/assets/javascripts/pages/projects/feature_flags/index.js
0 → 100644
View file @
3d969a96
import
Vue
from
'
vue
'
;
import
FeatureFlagsComponent
from
'
ee/feature_flags/components/feature_flags.vue
'
;
import
csrf
from
'
~/lib/utils/csrf
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
new
Vue
({
el
:
'
#feature-flags-vue
'
,
components
:
{
FeatureFlagsComponent
,
},
data
()
{
return
{
dataset
:
document
.
querySelector
(
this
.
$options
.
el
).
dataset
,
};
},
render
(
createElement
)
{
return
createElement
(
'
feature-flags-component
'
,
{
props
:
{
endpoint
:
this
.
dataset
.
endpoint
,
errorStateSvgPath
:
this
.
dataset
.
errorStateSvgPath
,
featureFlagsHelpPagePath
:
this
.
dataset
.
featureFlagsHelpPagePath
,
csrfToken
:
csrf
.
token
,
},
});
},
}),
);
ee/app/views/projects/feature_flags/index.html.haml
View file @
3d969a96
...
...
@@ -2,9 +2,7 @@
=
render
'configure_feature_flags_modal'
-
if
@feature_flags
.
empty?
=
render
'empty_state'
-
else
-
if
Feature
.
enabled?
(
:operations_feature_flag_index_tab
,
default_enabled:
true
)
%h3
.page-title.with-button
=
_
(
'Feature Flags'
)
...
...
@@ -12,4 +10,19 @@
=
render
'configure_feature_flags_button'
=
render
'new_feature_flag_button'
=
render
'table'
%div
#feature-flags-vue
{
data:
{
endpoint:
project_feature_flags_path
(
@project
,
format: :json
),
"error-state-svg-path"
=>
image_path
(
'illustrations/feature_flag.svg'
),
"feature-flags-help-page-path"
=>
help_page_path
(
"user/project/operations/feature_flags"
)
}
}
-
else
-
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'
ee/changelogs/unreleased/7731-feature-flags-table-view-filter-tabs.yml
0 → 100644
View file @
3d969a96
title
:
Allow to filter Feature Flags
merge_request
:
8821
author
:
type
:
added
ee/spec/controllers/projects/feature_flags_controller_spec.rb
View file @
3d969a96
...
...
@@ -26,7 +26,6 @@ describe Projects::FeatureFlagsController do
it
'shows an empty state with buttons'
do
expect
(
response
).
to
be_ok
expect
(
response
).
to
render_template
(
'_empty_state'
)
expect
(
response
).
to
render_template
(
'_configure_feature_flags_button'
)
expect
(
response
).
to
render_template
(
'_new_feature_flag_button'
)
end
...
...
@@ -41,7 +40,6 @@ describe Projects::FeatureFlagsController do
it
'shows an list of feature flags with buttons'
do
expect
(
response
).
to
be_ok
expect
(
response
).
to
render_template
(
'_table'
)
expect
(
response
).
to
render_template
(
'_configure_feature_flags_button'
)
expect
(
response
).
to
render_template
(
'_new_feature_flag_button'
)
end
...
...
ee/spec/features/projects/feature_flags_spec.rb
View file @
3d969a96
...
...
@@ -59,29 +59,41 @@ describe 'Feature Flags', :js do
add_feature_flag
(
'feature-flag-to-edit'
,
'with some description'
,
false
)
end
context
'and input is valid'
do
it
'updates the feature flag'
do
name
=
'new-name'
description
=
'new description'
shared_examples_for
'correct edit behavior'
do
context
'and input is valid'
do
it
'updates the feature flag'
do
name
=
'new-name'
description
=
'new description'
edit_feature_flag
(
'feature-flag-to-edit'
,
name
,
description
,
true
)
edit_feature_flag
(
'feature-flag-to-edit'
,
name
,
description
,
true
)
expect_feature_flag
(
name
,
description
,
true
)
expect
(
page
).
to
have_selector
'.flash-container'
,
text:
'successfully updated'
expect_feature_flag
(
name
,
description
,
true
)
expect
(
page
).
to
have_selector
'.flash-container'
,
text:
'successfully updated'
end
end
end
context
'and input is invalid'
do
where
(
:name
,
:description
,
:error_message
,
&
invalid_input_table
)
context
'and input is invalid'
do
where
(
:name
,
:description
,
:error_message
,
&
invalid_input_table
)
with_them
do
it
'displays an error message'
do
edit_feature_flag
(
'feature-flag-to-edit'
,
name
,
description
,
false
)
with_them
do
it
'displays an error message'
do
edit_feature_flag
(
'feature-flag-to-edit'
,
name
,
description
,
false
)
expect
(
page
).
to
have_selector
'.alert-danger'
,
text:
error_message
expect
(
page
).
to
have_selector
'.alert-danger'
,
text:
error_message
end
end
end
end
it_behaves_like
'correct edit behavior'
context
'when operations_feature_flag_index_tab feature flag is disabled'
do
before
do
stub_feature_flags
(
operations_feature_flag_index_tab:
false
)
end
it_behaves_like
'correct edit behavior'
end
end
context
'when deleting a feature flag'
do
...
...
@@ -89,36 +101,106 @@ describe 'Feature Flags', :js do
add_feature_flag
(
'feature-flag-to-delete'
,
'with some description'
,
false
)
end
context
'and no feature flags are left'
do
it
'shows empty state'
do
shared_examples_for
'correct delete behavior'
do
context
'and no feature flags are left'
do
it
'shows empty state'
do
visit
(
project_feature_flags_path
(
project
))
delete_feature_flag
(
'feature-flag-to-delete'
)
expect_empty_state
end
end
context
'and there is a feature flag left'
do
before
do
add_feature_flag
(
'another-feature-flag'
,
''
,
true
)
end
it
'shows feature flag table without deleted feature flag'
do
visit
(
project_feature_flags_path
(
project
))
delete_feature_flag
(
'feature-flag-to-delete'
)
expect_feature_flag
(
'another-feature-flag'
,
''
,
true
)
end
end
it
'does not delete if modal is cancelled'
do
visit
(
project_feature_flags_path
(
project
))
delete_feature_flag
(
'feature-flag-to-delete'
)
delete_feature_flag
(
'feature-flag-to-delete'
,
false
)
expect_
empty_state
expect_
feature_flag
(
'feature-flag-to-delete'
,
'with some description'
,
false
)
end
end
context
'and there is a feature flag left'
do
it_behaves_like
'correct delete behavior'
context
'when operations_feature_flag_index_tab feature flag is disabled'
do
before
do
add_feature_flag
(
'another-feature-flag'
,
''
,
tru
e
)
stub_feature_flags
(
operations_feature_flag_index_tab:
fals
e
)
end
it
'shows feature flag table without deleted feature flag'
do
visit
(
project_feature_flags_path
(
project
))
it_behaves_like
'correct delete behavior'
end
end
delete_feature_flag
(
'feature-flag-to-delete'
)
context
'when user sees empty index page'
do
before
do
visit
(
project_feature_flags_path
(
project
))
end
expect_feature_flag
(
'another-feature-flag'
,
''
,
true
)
shared_examples_for
'correct empty index behavior'
do
it
'shows empty state'
do
expect
(
page
).
to
have_content
(
'Get started with feature flags'
)
expect
(
page
).
to
have_link
(
'New Feature Flag'
)
expect
(
page
).
to
have_button
(
'Configure'
)
end
end
it
'does not delete if modal is cancelled'
do
it_behaves_like
'correct empty index behavior'
context
'when operations_feature_flag_index_tab feature flag is disabled'
do
before
do
stub_feature_flags
(
operations_feature_flag_index_tab:
false
)
end
it_behaves_like
'correct empty index behavior'
end
end
context
'when user sees index page'
do
let!
(
:feature_flag_enabled
)
{
create
(
:operations_feature_flag
,
project:
project
,
active:
true
)
}
let!
(
:feature_flag_disabled
)
{
create
(
:operations_feature_flag
,
project:
project
,
active:
false
)
}
before
do
visit
(
project_feature_flags_path
(
project
))
end
delete_feature_flag
(
'feature-flag-to-delete'
,
false
)
context
'when user sees all tab'
do
it
'shows all feature flags'
do
expect
(
page
).
to
have_content
(
feature_flag_enabled
.
name
)
expect
(
page
).
to
have_content
(
feature_flag_disabled
.
name
)
end
end
context
'when user sees enabled tab'
do
it
'shows only active feature flags'
do
find
(
'.js-featureflags-tab-enabled'
).
click
expect
(
page
).
to
have_content
(
feature_flag_enabled
.
name
)
expect
(
page
).
not_to
have_content
(
feature_flag_disabled
.
name
)
end
end
expect_feature_flag
(
'feature-flag-to-delete'
,
'with some description'
,
false
)
context
'when user sees disabled tab'
do
it
'shows only inactive feature flags'
do
find
(
'.js-featureflags-tab-disabled'
).
click
expect
(
page
).
not_to
have_content
(
feature_flag_enabled
.
name
)
expect
(
page
).
to
have_content
(
feature_flag_disabled
.
name
)
end
end
end
...
...
@@ -140,7 +222,13 @@ describe 'Feature Flags', :js do
end
def
delete_feature_flag
(
name
,
confirm
=
true
)
delete_button
=
find
(
'.gl-responsive-table-row'
,
text:
name
).
find
(
'.btn-danger[title="Delete"]'
)
delete_button
=
if
Feature
.
enabled?
(
:operations_feature_flag_index_tab
)
find
(
'.gl-responsive-table-row'
,
text:
name
).
find
(
'.js-feature-flag-delete-button'
)
else
find
(
'.gl-responsive-table-row'
,
text:
name
).
find
(
'.btn-danger[title="Delete"]'
)
end
delete_button
.
click
within
'.modal'
do
...
...
@@ -154,7 +242,14 @@ describe 'Feature Flags', :js do
def
edit_feature_flag
(
old_name
,
new_name
,
new_description
,
new_status
)
visit
(
project_feature_flags_path
(
project
))
edit_button
=
find
(
'.gl-responsive-table-row'
,
text:
old_name
).
find
(
'.btn-default[title="Edit"]'
)
edit_button
=
if
Feature
.
enabled?
(
:operations_feature_flag_index_tab
)
find
(
'.gl-responsive-table-row'
,
text:
old_name
).
find
(
'.js-feature-flag-edit-button'
)
else
find
(
'.gl-responsive-table-row'
,
text:
old_name
).
find
(
'.btn-default[title="Edit"]'
)
end
edit_button
.
click
fill_in
'Name'
,
with:
new_name
...
...
ee/spec/javascripts/projects/feature_flags/feature_flags_spec.js
0 → 100644
View file @
3d969a96
import
Vue
from
'
vue
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
featureFlagsComponent
from
'
ee/feature_flags/components/feature_flags.vue
'
;
import
{
createStore
}
from
'
ee/feature_flags/store
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
featureFlag
}
from
'
./mock_data
'
;
describe
(
'
Feature Flags
'
,
()
=>
{
const
mockData
=
{
endpoint
:
'
feature_flags.json
'
,
csrfToken
:
'
testToken
'
,
errorStateSvgPath
:
'
/assets/illustrations/feature_flag.svg
'
,
featureFlagsHelpPagePath
:
'
/help/feature-flags
'
,
};
let
store
;
let
FeatureFlagsComponent
;
let
component
;
let
mock
;
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
FeatureFlagsComponent
=
Vue
.
extend
(
featureFlagsComponent
);
});
afterEach
(()
=>
{
component
.
$destroy
();
mock
.
restore
();
});
describe
(
'
successful request
'
,
()
=>
{
describe
(
'
without feature flags
'
,
()
=>
{
beforeEach
(
done
=>
{
mock
.
onGet
(
mockData
.
endpoint
).
reply
(
200
,
{
feature_flags
:
[],
count
:
{
all
:
0
,
enabled
:
0
,
disabled
:
0
,
},
});
component
=
mountComponentWithStore
(
FeatureFlagsComponent
,
{
store
,
props
:
mockData
,
});
setTimeout
(()
=>
{
done
();
},
0
);
});
it
(
'
should render the empty state
'
,
()
=>
{
expect
(
component
.
$el
.
querySelectorAll
(
'
.js-feature-flags-empty-state
'
)).
not
.
toBeNull
();
});
});
describe
(
'
with paginated feature flags
'
,
()
=>
{
beforeEach
(
done
=>
{
mock
.
onGet
(
mockData
.
endpoint
).
reply
(
200
,
{
feature_flags
:
[
featureFlag
],
count
:
{
all
:
37
,
enabled
:
5
,
disabled
:
32
,
},
},
{
'
X-nExt-pAge
'
:
'
2
'
,
'
x-page
'
:
'
1
'
,
'
X-Per-Page
'
:
'
1
'
,
'
X-Prev-Page
'
:
''
,
'
X-TOTAL
'
:
'
37
'
,
'
X-Total-Pages
'
:
'
2
'
,
},
);
store
=
createStore
();
component
=
mountComponentWithStore
(
FeatureFlagsComponent
,
{
store
,
props
:
mockData
,
});
setTimeout
(()
=>
{
done
();
},
0
);
});
it
(
'
should render a table with feature flags
'
,
()
=>
{
expect
(
component
.
$el
.
querySelectorAll
(
'
.js-feature-flag-table
'
)).
not
.
toBeNull
();
expect
(
component
.
$el
.
querySelector
(
'
.feature-flag-name
'
).
textContent
.
trim
()).
toEqual
(
featureFlag
.
name
,
);
expect
(
component
.
$el
.
querySelector
(
'
.feature-flag-description
'
).
textContent
.
trim
()).
toEqual
(
featureFlag
.
description
,
);
});
describe
(
'
pagination
'
,
()
=>
{
it
(
'
should render pagination
'
,
()
=>
{
expect
(
component
.
$el
.
querySelectorAll
(
'
.gl-pagination li
'
).
length
).
toEqual
(
5
);
});
it
(
'
should make an API request when page is clicked
'
,
done
=>
{
spyOn
(
component
,
'
updateFeatureFlagOptions
'
);
setTimeout
(()
=>
{
component
.
$el
.
querySelector
(
'
.gl-pagination li:nth-child(5) a
'
).
click
();
expect
(
component
.
updateFeatureFlagOptions
).
toHaveBeenCalledWith
({
scope
:
'
all
'
,
page
:
'
2
'
,
});
done
();
},
0
);
});
it
(
'
should make an API request when using tabs
'
,
done
=>
{
setTimeout
(()
=>
{
spyOn
(
component
,
'
updateFeatureFlagOptions
'
);
component
.
$el
.
querySelector
(
'
.js-featureflags-tab-enabled
'
).
click
();
expect
(
component
.
updateFeatureFlagOptions
).
toHaveBeenCalledWith
({
scope
:
'
enabled
'
,
page
:
'
1
'
,
});
done
();
},
0
);
});
});
});
});
describe
(
'
unsuccessful request
'
,
()
=>
{
beforeEach
(
done
=>
{
mock
.
onGet
(
mockData
.
endpoint
).
reply
(
500
,
{});
store
=
createStore
();
component
=
mountComponentWithStore
(
FeatureFlagsComponent
,
{
store
,
props
:
mockData
,
});
setTimeout
(()
=>
{
done
();
},
0
);
});
it
(
'
should render error state
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.empty-state
'
).
textContent
.
trim
()).
toContain
(
'
There was an error fetching the feature flags. Try again in a few moments or contact your support team.
'
,
);
});
});
});
ee/spec/javascripts/projects/feature_flags/feature_flags_table_spec.js
0 → 100644
View file @
3d969a96
import
Vue
from
'
vue
'
;
import
featureFlagsTableComponent
from
'
ee/feature_flags/components/feature_flags_table.vue
'
;
import
mountComponent
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
featureFlag
}
from
'
./mock_data
'
;
describe
(
'
Feature Flag table
'
,
()
=>
{
let
Component
;
let
vm
;
beforeEach
(()
=>
{
Component
=
Vue
.
extend
(
featureFlagsTableComponent
);
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
it
(
'
Should render a table
'
,
()
=>
{
vm
=
mountComponent
(
Component
,
{
featureFlags
:
[
featureFlag
],
csrfToken
:
'
fakeToken
'
,
});
expect
(
vm
.
$el
.
getAttribute
(
'
class
'
)).
toContain
(
'
table-holder
'
);
});
it
(
'
Should render rows
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.gl-responsive-table-row
'
)).
not
.
toBeNull
();
});
it
(
'
Should render a status column
'
,
()
=>
{
const
status
=
featureFlag
.
active
?
'
Active
'
:
'
Inactive
'
;
expect
(
vm
.
$el
.
querySelector
(
'
.js-feature-flag-status
'
)).
not
.
toBeNull
();
expect
(
vm
.
$el
.
querySelector
(
'
.js-feature-flag-status
'
).
textContent
).
toEqual
(
status
);
});
it
(
'
Should render a feature flag column
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-feature-flag-title
'
)).
not
.
toBeNull
();
expect
(
vm
.
$el
.
querySelector
(
'
.feature-flag-name
'
).
textContent
.
trim
()).
toEqual
(
featureFlag
.
name
);
expect
(
vm
.
$el
.
querySelector
(
'
.feature-flag-description
'
).
textContent
.
trim
()).
toEqual
(
featureFlag
.
description
,
);
});
it
(
'
Should render an actions column
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.table-action-buttons
'
)).
not
.
toBeNull
();
expect
(
vm
.
$el
.
querySelector
(
'
.js-feature-flag-delete-button
'
)).
not
.
toBeNull
();
expect
(
vm
.
$el
.
querySelector
(
'
.js-feature-flag-edit-button
'
)).
not
.
toBeNull
();
expect
(
vm
.
$el
.
querySelector
(
'
.js-feature-flag-edit-button
'
).
getAttribute
(
'
href
'
)).
toEqual
(
featureFlag
.
edit_path
,
);
});
});
ee/spec/javascripts/projects/feature_flags/mock_data.js
0 → 100644
View file @
3d969a96
export
const
featureFlagsList
=
[
{
id
:
1
,
active
:
true
,
created_at
:
'
2018-12-12T22:07:31.401Z
'
,
updated_at
:
'
2018-12-12T22:07:31.401Z
'
,
name
:
'
test flag
'
,
description
:
'
flag for tests
'
,
destroy_path
:
'
feature_flags/1
'
,
edit_path
:
'
feature_flags/1/edit
'
,
},
];
export
const
featureFlag
=
{
id
:
1
,
active
:
true
,
created_at
:
'
2018-12-12T22:07:31.401Z
'
,
updated_at
:
'
2018-12-12T22:07:31.401Z
'
,
name
:
'
test flag
'
,
description
:
'
flag for tests
'
,
destroy_path
:
'
feature_flags/1
'
,
edit_path
:
'
feature_flags/1/edit
'
,
};
locale/gitlab.pot
View file @
3d969a96
...
...
@@ -3695,6 +3695,12 @@ msgstr ""
msgid "FeatureFlags|Delete %{feature_flag_name}?"
msgstr ""
msgid "FeatureFlags|Delete %{name}?"
msgstr ""
msgid "FeatureFlags|Delete feature flag"
msgstr ""
msgid "FeatureFlags|Description"
msgstr ""
...
...
@@ -3713,6 +3719,9 @@ msgstr ""
msgid "FeatureFlags|Feature flag %{feature_flag_name} will be removed. Are you sure?"
msgstr ""
msgid "FeatureFlags|Feature flag %{name} will be removed. Are you sure?"
msgstr ""
msgid "FeatureFlags|Feature flags allow you to configure your code into different flavors by dynamically toggling certain functionality."
msgstr ""
...
...
@@ -3728,6 +3737,9 @@ msgstr ""
msgid "FeatureFlags|Instance ID"
msgstr ""
msgid "FeatureFlags|More Information"
msgstr ""
msgid "FeatureFlags|More information"
msgstr ""
...
...
@@ -3746,6 +3758,12 @@ msgstr ""
msgid "FeatureFlags|Status"
msgstr ""
msgid "FeatureFlags|There was an error fetching the feature flags."
msgstr ""
msgid "FeatureFlags|Try again in a few moments or contact your support team."
msgstr ""
msgid "Feb"
msgstr ""
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment