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
58dae1a2
Commit
58dae1a2
authored
Nov 02, 2018
by
Adriel Santiago
Committed by
Sean McGivern
Nov 02, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Resolve "Operations Homepage MVC"
parent
7e1f9a5f
Changes
66
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
66 changed files
with
3622 additions
and
7 deletions
+3622
-7
app/controllers/root_controller.rb
app/controllers/root_controller.rb
+2
-0
app/finders/projects_finder.rb
app/finders/projects_finder.rb
+2
-0
app/helpers/preferences_helper.rb
app/helpers/preferences_helper.rb
+2
-0
app/policies/global_policy.rb
app/policies/global_policy.rb
+2
-0
db/schema.rb
db/schema.rb
+12
-0
ee/app/assets/javascripts/operations/components/dashboard/alerts.vue
...ts/javascripts/operations/components/dashboard/alerts.vue
+93
-0
ee/app/assets/javascripts/operations/components/dashboard/dashboard.vue
...javascripts/operations/components/dashboard/dashboard.vue
+109
-0
ee/app/assets/javascripts/operations/components/dashboard/project.vue
...s/javascripts/operations/components/dashboard/project.vue
+100
-0
ee/app/assets/javascripts/operations/components/dashboard/project_header.vue
...cripts/operations/components/dashboard/project_header.vue
+62
-0
ee/app/assets/javascripts/operations/components/dashboard/project_search.vue
...cripts/operations/components/dashboard/project_search.vue
+103
-0
ee/app/assets/javascripts/operations/components/tokenized_input/input.vue
...vascripts/operations/components/tokenized_input/input.vue
+69
-0
ee/app/assets/javascripts/operations/mixins.js
ee/app/assets/javascripts/operations/mixins.js
+17
-0
ee/app/assets/javascripts/operations/store/actions.js
ee/app/assets/javascripts/operations/store/actions.js
+127
-0
ee/app/assets/javascripts/operations/store/index.js
ee/app/assets/javascripts/operations/store/index.js
+13
-0
ee/app/assets/javascripts/operations/store/mutation_types.js
ee/app/assets/javascripts/operations/store/mutation_types.js
+14
-0
ee/app/assets/javascripts/operations/store/mutations.js
ee/app/assets/javascripts/operations/store/mutations.js
+40
-0
ee/app/assets/javascripts/operations/store/state.js
ee/app/assets/javascripts/operations/store/state.js
+12
-0
ee/app/assets/javascripts/pages/operations/index.js
ee/app/assets/javascripts/pages/operations/index.js
+24
-0
ee/app/assets/stylesheets/pages/operations.scss
ee/app/assets/stylesheets/pages/operations.scss
+67
-0
ee/app/controllers/ee/root_controller.rb
ee/app/controllers/ee/root_controller.rb
+19
-0
ee/app/controllers/operations_controller.rb
ee/app/controllers/operations_controller.rb
+62
-0
ee/app/finders/ee/projects_finder.rb
ee/app/finders/ee/projects_finder.rb
+31
-0
ee/app/helpers/ee/preferences_helper.rb
ee/app/helpers/ee/preferences_helper.rb
+14
-0
ee/app/helpers/operations_helper.rb
ee/app/helpers/operations_helper.rb
+11
-0
ee/app/models/ee/project.rb
ee/app/models/ee/project.rb
+1
-0
ee/app/models/ee/user.rb
ee/app/models/ee/user.rb
+7
-4
ee/app/models/license.rb
ee/app/models/license.rb
+1
-0
ee/app/models/prometheus_alert_event.rb
ee/app/models/prometheus_alert_event.rb
+12
-0
ee/app/models/users_ops_dashboard_project.rb
ee/app/models/users_ops_dashboard_project.rb
+10
-0
ee/app/policies/ee/global_policy.rb
ee/app/policies/ee/global_policy.rb
+15
-0
ee/app/serializers/dashboard_operations_project_entity.rb
ee/app/serializers/dashboard_operations_project_entity.rb
+42
-0
ee/app/serializers/dashboard_operations_serializer.rb
ee/app/serializers/dashboard_operations_serializer.rb
+5
-0
ee/app/services/dashboard/operations/list_service.rb
ee/app/services/dashboard/operations/list_service.rb
+77
-0
ee/app/services/dashboard/operations/projects_service.rb
ee/app/services/dashboard/operations/projects_service.rb
+36
-0
ee/app/services/ee/groups/update_service.rb
ee/app/services/ee/groups/update_service.rb
+1
-1
ee/app/services/users_ops_dashboard_projects/base_service.rb
ee/app/services/users_ops_dashboard_projects/base_service.rb
+11
-0
ee/app/services/users_ops_dashboard_projects/create_service.rb
...p/services/users_ops_dashboard_projects/create_service.rb
+39
-0
ee/app/services/users_ops_dashboard_projects/destroy_service.rb
.../services/users_ops_dashboard_projects/destroy_service.rb
+17
-0
ee/app/views/dashboard/operations/_nav_link.html.haml
ee/app/views/dashboard/operations/_nav_link.html.haml
+4
-0
ee/app/views/operations/index.html.haml
ee/app/views/operations/index.html.haml
+3
-0
ee/changelogs/unreleased/5781-operations-homepage-mvc-frontend.yml
...logs/unreleased/5781-operations-homepage-mvc-frontend.yml
+5
-0
ee/config/routes/operations.rb
ee/config/routes/operations.rb
+4
-2
ee/db/migrate/20181012151642_create_users_ops_dashboard_projects.rb
...ate/20181012151642_create_users_ops_dashboard_projects.rb
+17
-0
ee/spec/controllers/ee/root_controller_spec.rb
ee/spec/controllers/ee/root_controller_spec.rb
+39
-0
ee/spec/controllers/operations_controller_spec.rb
ee/spec/controllers/operations_controller_spec.rb
+272
-0
ee/spec/finders/ee/projects_finder_spec.rb
ee/spec/finders/ee/projects_finder_spec.rb
+56
-0
ee/spec/fixtures/api/schemas/dashboard/operations/add.json
ee/spec/fixtures/api/schemas/dashboard/operations/add.json
+28
-0
ee/spec/fixtures/api/schemas/dashboard/operations/list.json
ee/spec/fixtures/api/schemas/dashboard/operations/list.json
+58
-0
ee/spec/helpers/operations_helper_spec.rb
ee/spec/helpers/operations_helper_spec.rb
+17
-0
ee/spec/helpers/preferences_helper_spec.rb
ee/spec/helpers/preferences_helper_spec.rb
+33
-0
ee/spec/javascripts/operations/components/dashboard/alerts_spec.js
...avascripts/operations/components/dashboard/alerts_spec.js
+77
-0
ee/spec/javascripts/operations/components/dashboard/dashboard_spec.js
...scripts/operations/components/dashboard/dashboard_spec.js
+123
-0
ee/spec/javascripts/operations/components/dashboard/project_header_spec.js
...ts/operations/components/dashboard/project_header_spec.js
+66
-0
ee/spec/javascripts/operations/components/dashboard/project_search_spec.js
...ts/operations/components/dashboard/project_search_spec.js
+183
-0
ee/spec/javascripts/operations/components/dashboard/project_spec.js
...vascripts/operations/components/dashboard/project_spec.js
+112
-0
ee/spec/javascripts/operations/components/tokenized_input/input_spec.js
...ripts/operations/components/tokenized_input/input_spec.js
+89
-0
ee/spec/javascripts/operations/helpers.js
ee/spec/javascripts/operations/helpers.js
+15
-0
ee/spec/javascripts/operations/mock_data.js
ee/spec/javascripts/operations/mock_data.js
+67
-0
ee/spec/javascripts/operations/store/actions_spec.js
ee/spec/javascripts/operations/store/actions_spec.js
+509
-0
ee/spec/javascripts/operations/store/mutations_spec.js
ee/spec/javascripts/operations/store/mutations_spec.js
+86
-0
ee/spec/policies/global_policy_spec.rb
ee/spec/policies/global_policy_spec.rb
+28
-0
ee/spec/services/dashboard/operations/list_service_spec.rb
ee/spec/services/dashboard/operations/list_service_spec.rb
+175
-0
ee/spec/services/dashboard/operations/projects_service_spec.rb
...ec/services/dashboard/operations/projects_service_spec.rb
+79
-0
ee/spec/services/users_ops_dashboard_projects/create_service_spec.rb
...vices/users_ops_dashboard_projects/create_service_spec.rb
+113
-0
ee/spec/services/users_ops_dashboard_projects/destroy_service_spec.rb
...ices/users_ops_dashboard_projects/destroy_service_spec.rb
+37
-0
locale/gitlab.pot
locale/gitlab.pot
+46
-0
No files found.
app/controllers/root_controller.rb
View file @
58dae1a2
...
@@ -9,6 +9,8 @@
...
@@ -9,6 +9,8 @@
# For users who haven't customized the setting, we simply delegate to
# For users who haven't customized the setting, we simply delegate to
# `DashboardController#show`, which is the default.
# `DashboardController#show`, which is the default.
class
RootController
<
Dashboard
::
ProjectsController
class
RootController
<
Dashboard
::
ProjectsController
prepend
EE
::
RootController
skip_before_action
:authenticate_user!
,
only:
[
:index
]
skip_before_action
:authenticate_user!
,
only:
[
:index
]
before_action
:redirect_unlogged_user
,
if:
->
{
current_user
.
nil?
}
before_action
:redirect_unlogged_user
,
if:
->
{
current_user
.
nil?
}
...
...
app/finders/projects_finder.rb
View file @
58dae1a2
...
@@ -24,6 +24,8 @@
...
@@ -24,6 +24,8 @@
class
ProjectsFinder
<
UnionFinder
class
ProjectsFinder
<
UnionFinder
include
CustomAttributesFilter
include
CustomAttributesFilter
prepend
::
EE
::
ProjectsFinder
attr_accessor
:params
attr_accessor
:params
attr_reader
:current_user
,
:project_ids_relation
attr_reader
:current_user
,
:project_ids_relation
...
...
app/helpers/preferences_helper.rb
View file @
58dae1a2
...
@@ -2,6 +2,8 @@
...
@@ -2,6 +2,8 @@
# Helper methods for per-User preferences
# Helper methods for per-User preferences
module
PreferencesHelper
module
PreferencesHelper
prepend
EE
::
PreferencesHelper
def
layout_choices
def
layout_choices
[
[
[
'Fixed'
,
:fixed
],
[
'Fixed'
,
:fixed
],
...
...
app/policies/global_policy.rb
View file @
58dae1a2
# frozen_string_literal: true
# frozen_string_literal: true
class
GlobalPolicy
<
BasePolicy
class
GlobalPolicy
<
BasePolicy
prepend
EE
::
GlobalPolicy
desc
"User is blocked"
desc
"User is blocked"
with_options
scope: :user
,
score:
0
with_options
scope: :user
,
score:
0
condition
(
:blocked
)
{
@user
&
.
blocked?
}
condition
(
:blocked
)
{
@user
&
.
blocked?
}
...
...
db/schema.rb
View file @
58dae1a2
...
@@ -3011,6 +3011,16 @@ ActiveRecord::Schema.define(version: 20181031190559) do
...
@@ -3011,6 +3011,16 @@ ActiveRecord::Schema.define(version: 20181031190559) do
add_index
"users"
,
[
"username"
],
name:
"index_users_on_username"
,
using: :btree
add_index
"users"
,
[
"username"
],
name:
"index_users_on_username"
,
using: :btree
add_index
"users"
,
[
"username"
],
name:
"index_users_on_username_trigram"
,
using: :gin
,
opclasses:
{
"username"
=>
"gin_trgm_ops"
}
add_index
"users"
,
[
"username"
],
name:
"index_users_on_username_trigram"
,
using: :gin
,
opclasses:
{
"username"
=>
"gin_trgm_ops"
}
create_table
"users_ops_dashboard_projects"
,
id: :bigserial
,
force: :cascade
do
|
t
|
t
.
datetime_with_timezone
"created_at"
,
null:
false
t
.
datetime_with_timezone
"updated_at"
,
null:
false
t
.
integer
"user_id"
,
null:
false
t
.
integer
"project_id"
,
null:
false
end
add_index
"users_ops_dashboard_projects"
,
[
"project_id"
],
name:
"index_users_ops_dashboard_projects_on_project_id"
,
using: :btree
add_index
"users_ops_dashboard_projects"
,
[
"user_id"
,
"project_id"
],
name:
"index_users_ops_dashboard_projects_on_user_id_and_project_id"
,
unique:
true
,
using: :btree
create_table
"users_star_projects"
,
force: :cascade
do
|
t
|
create_table
"users_star_projects"
,
force: :cascade
do
|
t
|
t
.
integer
"project_id"
,
null:
false
t
.
integer
"project_id"
,
null:
false
t
.
integer
"user_id"
,
null:
false
t
.
integer
"user_id"
,
null:
false
...
@@ -3412,6 +3422,8 @@ ActiveRecord::Schema.define(version: 20181031190559) do
...
@@ -3412,6 +3422,8 @@ ActiveRecord::Schema.define(version: 20181031190559) do
add_foreign_key
"user_statuses"
,
"users"
,
on_delete: :cascade
add_foreign_key
"user_statuses"
,
"users"
,
on_delete: :cascade
add_foreign_key
"user_synced_attributes_metadata"
,
"users"
,
on_delete: :cascade
add_foreign_key
"user_synced_attributes_metadata"
,
"users"
,
on_delete: :cascade
add_foreign_key
"users"
,
"application_setting_terms"
,
column:
"accepted_term_id"
,
name:
"fk_789cd90b35"
,
on_delete: :cascade
add_foreign_key
"users"
,
"application_setting_terms"
,
column:
"accepted_term_id"
,
name:
"fk_789cd90b35"
,
on_delete: :cascade
add_foreign_key
"users_ops_dashboard_projects"
,
"projects"
,
on_delete: :cascade
add_foreign_key
"users_ops_dashboard_projects"
,
"users"
,
on_delete: :cascade
add_foreign_key
"users_star_projects"
,
"projects"
,
name:
"fk_22cd27ddfc"
,
on_delete: :cascade
add_foreign_key
"users_star_projects"
,
"projects"
,
name:
"fk_22cd27ddfc"
,
on_delete: :cascade
add_foreign_key
"vulnerability_feedback"
,
"ci_pipelines"
,
column:
"pipeline_id"
,
on_delete: :nullify
add_foreign_key
"vulnerability_feedback"
,
"ci_pipelines"
,
column:
"pipeline_id"
,
on_delete: :nullify
add_foreign_key
"vulnerability_feedback"
,
"issues"
,
on_delete: :nullify
add_foreign_key
"vulnerability_feedback"
,
"issues"
,
on_delete: :nullify
...
...
ee/app/assets/javascripts/operations/components/dashboard/alerts.vue
0 → 100644
View file @
58dae1a2
<
script
>
import
{
__
,
n__
,
sprintf
}
from
'
~/locale
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
export
default
{
components
:
{
Icon
,
},
props
:
{
count
:
{
type
:
Number
,
required
:
false
,
default
:
0
,
},
lastAlert
:
{
type
:
Object
,
required
:
false
,
default
:
null
,
},
alertPath
:
{
type
:
String
,
required
:
false
,
default
:
null
,
},
},
computed
:
{
alertClasses
()
{
return
{
'
text-success
'
:
this
.
count
<=
0
,
'
text-warning
'
:
this
.
count
>
0
,
};
},
alertCount
()
{
return
sprintf
(
__
(
'
%{count} %{alerts}
'
),
{
count
:
this
.
count
,
alerts
:
this
.
pluralizedAlerts
,
});
},
alertLinkTitle
()
{
return
sprintf
(
__
(
'
View %{alerts}
'
),
{
alerts
:
this
.
pluralizedAlerts
});
},
lastAlertText
()
{
if
(
this
.
count
===
0
||
this
.
lastAlert
===
null
)
{
return
__
(
'
None
'
);
}
const
ellipsis
=
this
.
count
>
1
?
'
\
u2026
'
:
''
;
return
`
${
this
.
lastAlert
.
operator
}
${
this
.
lastAlert
.
threshold
}${
ellipsis
}
`
;
},
pluralizedAlerts
()
{
return
n__
(
'
Alert
'
,
'
Alerts
'
,
this
.
count
);
},
},
};
</
script
>
<
template
>
<div
class=
"row"
>
<div
class=
"col-12 d-flex align-items-center"
>
<icon
:class=
"alertClasses"
name=
"warning"
/>
<span
class=
"js-alert-count text-secondary prepend-left-4"
>
{{
alertCount
}}
</span>
</div>
<div
class=
"js-last-alert col-12"
>
<a
v-if=
"alertPath"
:href=
"alertPath"
class=
"js-alert-link cgray"
>
<span
v-if=
"lastAlert"
class=
"str-truncated-60"
>
{{
lastAlert
.
title
}}
</span>
<span>
{{
lastAlertText
}}
</span>
</a>
<span
v-else
>
{{
lastAlertText
}}
</span>
</div>
</div>
</
template
>
ee/app/assets/javascripts/operations/components/dashboard/dashboard.vue
0 → 100644
View file @
58dae1a2
<
script
>
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
DashboardProject
from
'
./project.vue
'
;
import
ProjectSearch
from
'
./project_search.vue
'
;
export
default
{
components
:
{
DashboardProject
,
ProjectSearch
,
},
props
:
{
addPath
:
{
type
:
String
,
required
:
true
,
},
listPath
:
{
type
:
String
,
required
:
true
,
},
emptyDashboardSvgPath
:
{
type
:
String
,
required
:
true
,
},
},
computed
:
{
...
mapState
([
'
projects
'
,
'
projectTokens
'
,
'
isLoadingProjects
'
]),
addIsDisabled
()
{
return
!
this
.
projectTokens
.
length
;
},
},
created
()
{
this
.
setProjectEndpoints
({
list
:
this
.
listPath
,
add
:
this
.
addPath
,
});
this
.
fetchProjects
();
},
methods
:
{
...
mapActions
([
'
addProjectsToDashboard
'
,
'
fetchProjects
'
,
'
setProjectEndpoints
'
]),
addProjects
()
{
if
(
!
this
.
addIsDisabled
)
{
this
.
addProjectsToDashboard
();
}
},
},
};
</
script
>
<
template
>
<div
class=
"operations-dashboard"
>
<nav
class=
"breadcrumbs container-fluid container-limited"
>
<div
class=
"breadcrumbs-container"
>
<h2
class=
"js-dashboard-title breadcrumbs-sub-title"
>
{{
__
(
'
Operations Dashboard
'
)
}}
</h2>
</div>
</nav>
<div
class=
"container-fluid container-limited prepend-top-default"
>
<div
class=
"d-flex align-items-center"
>
<project-search
class=
"flex-grow-1"
/>
<button
:class=
"
{ disabled: addIsDisabled }"
type="button"
class="js-add-projects-button btn btn-success prepend-left-8"
@click="addProjects"
>
{{
__
(
'
Add projects
'
)
}}
</button>
</div>
<div
v-if=
"projects.length"
class=
"row m-0 prepend-top-default"
>
<div
v-for=
"project in projects"
:key=
"project.id"
class=
"col-12 col-md-6 odds-md-pad-right evens-md-pad-left"
>
<dashboard-project
:project=
"project"
/>
</div>
</div>
<div
v-else-if=
"!isLoadingProjects"
class=
"row prepend-top-20 text-center"
>
<div
class=
"col-12 d-flex justify-content-center svg-content"
>
<img
:src=
"emptyDashboardSvgPath"
class=
"js-empty-state-svg col-12 prepend-top-20"
/>
</div>
<h4
class=
"js-title col-12 prepend-top-20"
>
{{
s__
(
'
OperationsDashboard|Add a project to the dashboard
'
)
}}
</h4>
<div
class=
"col-12 d-flex justify-content-center"
>
<span
class=
"js-sub-title mw-460 text-tertiary"
>
{{
s__
(
`OperationsDashboard|The operations dashboard provides a summary of each project's
operational health, including pipeline and alert status.`
)
}}
</span>
</div>
</div>
<gl-loading-icon
v-else
:size=
"2"
class=
"prepend-top-20"
/>
</div>
</div>
</
template
>
ee/app/assets/javascripts/operations/components/dashboard/project.vue
0 → 100644
View file @
58dae1a2
<
script
>
import
{
mapActions
}
from
'
vuex
'
;
import
timeago
from
'
~/vue_shared/mixins/timeago
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
Commit
from
'
~/vue_shared/components/commit.vue
'
;
import
DashboardAlerts
from
'
./alerts.vue
'
;
import
ProjectHeader
from
'
./project_header.vue
'
;
export
default
{
components
:
{
Icon
,
Commit
,
DashboardAlerts
,
ProjectHeader
,
},
mixins
:
[
timeago
],
props
:
{
project
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
author
()
{
return
this
.
hasDeployment
&&
this
.
project
.
last_deployment
.
user
?
{
avatar_url
:
this
.
project
.
last_deployment
.
user
.
avatar_url
,
path
:
this
.
project
.
last_deployment
.
user
.
web_url
,
username
:
this
.
project
.
last_deployment
.
user
.
username
,
}
:
null
;
},
commitRef
()
{
return
this
.
hasDeployment
&&
this
.
project
.
last_deployment
.
ref
?
{
name
:
this
.
project
.
last_deployment
.
ref
.
name
,
ref_url
:
this
.
project
.
last_deployment
.
ref
.
ref_path
,
}
:
null
;
},
hasDeployment
()
{
return
this
.
project
.
last_deployment
!==
null
;
},
lastDeployed
()
{
return
this
.
hasDeployment
?
this
.
timeFormated
(
this
.
project
.
last_deployment
.
created_at
)
:
null
;
},
},
methods
:
{
...
mapActions
([
'
removeProject
'
]),
},
};
</
script
>
<
template
>
<div
class=
"card"
>
<project-header
:project=
"project"
class=
"card-header"
@
remove=
"removeProject"
/>
<div
class=
"card-body"
>
<div
class=
"row"
>
<div
class=
"col-6 col-sm-4 col-md-6 col-lg-4 pr-1"
>
<dashboard-alerts
:count=
"project.alert_count"
:last-alert=
"project.last_alert"
:alert-path=
"project.alert_path"
/>
</div>
<template
v-if=
"project.last_deployment"
>
<div
class=
"col-6 col-sm-4 col-md-6 col-lg-4 px-1"
>
<commit
:commit-ref=
"commitRef"
:short-sha=
"project.last_deployment.commit.short_id"
:commit-url=
"project.last_deployment.commit.web_url"
:title=
"project.last_deployment.commit.title"
:author=
"author"
:tag=
"project.last_deployment.commit.tag"
/>
</div>
<div
class=
"js-project-container col-12 col-sm-4 col-md-12 col-lg-4 pl-1 d-flex justify-content-end"
>
<div
class=
"d-flex align-items-end justify-content-end"
>
<div
class=
"prepend-top-default text-secondary d-flex align-items-center flex-wrap"
>
<icon
name=
"calendar"
class=
"append-right-4"
/>
{{
lastDeployed
}}
</div>
</div>
</div>
</
template
>
</div>
</div>
</div>
</template>
ee/app/assets/javascripts/operations/components/dashboard/project_header.vue
0 → 100644
View file @
58dae1a2
<
script
>
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
ProjectAvatar
from
'
~/vue_shared/components/project_avatar/default.vue
'
;
export
default
{
components
:
{
Icon
,
ProjectAvatar
,
},
props
:
{
project
:
{
type
:
Object
,
required
:
true
,
},
},
methods
:
{
onRemove
()
{
this
.
$emit
(
'
remove
'
,
this
.
project
.
remove_path
);
},
},
};
</
script
>
<
template
>
<div
class=
"project-header d-flex align-items-center"
>
<project-avatar
:project=
"project"
:size=
"20"
class=
"flex-shrink-0"
/>
<div
class=
"flex-grow-1"
>
<a
class=
"js-project-link cgray"
:href=
"project.web_url"
>
<span
class=
"js-name-with-namespace bold"
>
{{
project
.
name_with_namespace
}}
</span>
</a>
</div>
<div
class=
"dropdown"
>
<div
class=
"d-flex align-items-center ml-2"
data-toggle=
"dropdown"
>
<icon
name=
"ellipsis_v"
class=
"text-secondary"
/>
</div>
<div
class=
"dropdown-menu dropdown-menu-right"
>
<button
type=
"button"
class=
"js-remove-button dropdown-item btn-link text-danger prepend-left-default append-right-default outline-0"
@
click=
"onRemove"
>
{{
__
(
'
Remove
'
)
}}
</button>
</div>
</div>
</div>
</
template
>
ee/app/assets/javascripts/operations/components/dashboard/project_search.vue
0 → 100644
View file @
58dae1a2
<
script
>
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
_
from
'
underscore
'
;
import
{
__
,
sprintf
}
from
'
~/locale
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
ProjectAvatar
from
'
~/vue_shared/components/project_avatar/default.vue
'
;
import
TokenizedInput
from
'
../tokenized_input/input.vue
'
;
import
inputFocus
from
'
../../mixins
'
;
const
inputSearchDelay
=
300
;
export
default
{
components
:
{
Icon
,
ProjectAvatar
,
TokenizedInput
,
},
mixins
:
[
inputFocus
],
data
()
{
return
{
hasSearchedInput
:
false
,
};
},
computed
:
{
...
mapState
([
'
inputValue
'
,
'
projectTokens
'
,
'
projectSearchResults
'
,
'
searchCount
'
]),
isSearchingProjects
()
{
return
this
.
searchCount
>
0
;
},
searchDescription
()
{
return
sprintf
(
__
(
'
"%{query}" in projects
'
),
{
query
:
this
.
inputValue
});
},
shouldShowSearch
()
{
return
this
.
inputValue
.
length
&&
this
.
isInputFocused
;
},
foundNoResults
()
{
return
!
this
.
projectSearchResults
.
length
&&
this
.
hasSearchedInput
;
},
},
watch
:
{
inputValue
()
{
this
.
queryInputInProjects
();
},
},
methods
:
{
...
mapActions
([
'
addProjectToken
'
,
'
searchProjects
'
,
'
clearProjectSearchResults
'
]),
queryInputInProjects
:
_
.
debounce
(
function
search
()
{
this
.
searchProjects
(
this
.
inputValue
);
this
.
hasSearchedInput
=
true
;
},
inputSearchDelay
),
},
};
</
script
>
<
template
>
<div
:class=
"
{ show: shouldShowSearch }"
class="dropdown"
>
<tokenized-input
@
focus=
"onFocus"
@
blur=
"onBlur"
/>
<div
class=
"js-search-results dropdown-menu w-100 mw-100"
@
mousedown
.
prevent
>
<div
class=
"py-2 px-4 text-tertiary"
>
<icon
name=
"search"
/>
{{
searchDescription
}}
</div>
<div
class=
"dropdown-divider"
></div>
<gl-loading-icon
v-if=
"isSearchingProjects"
:size=
"2"
class=
"py-2 px-4"
/>
<div
v-else-if=
"foundNoResults"
class=
"py-2 px-4 text-tertiary"
>
{{
__
(
'
Sorry, no projects matched your search
'
)
}}
</div>
<button
v-for=
"project in projectSearchResults"
:key=
"project.id"
type=
"button"
class=
"js-search-result dropdown-item btn-link d-flex align-items-center cgray py-2 px-4"
@
mousedown=
"addProjectToken(project)"
>
<project-avatar
:project=
"project"
:size=
"20"
class=
"flex-shrink-0 mr-3"
/>
<div
class=
"flex-grow-1"
>
<div
class=
"js-name-with-namespace bold ws-initial"
>
{{
project
.
name_with_namespace
}}
</div>
</div>
</button>
</div>
</div>
</
template
>
ee/app/assets/javascripts/operations/components/tokenized_input/input.vue
0 → 100644
View file @
58dae1a2
<
script
>
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
inputFocus
from
'
../../mixins
'
;
export
default
{
components
:
{
Icon
,
},
mixins
:
[
inputFocus
],
computed
:
{
...
mapState
([
'
inputValue
'
,
'
projectTokens
'
]),
localInputValue
:
{
get
()
{
return
this
.
inputValue
;
},
set
(
newValue
)
{
this
.
setInputValue
(
newValue
);
},
},
},
methods
:
{
...
mapActions
([
'
setInputValue
'
,
'
removeProjectTokenAt
'
]),
focusInput
()
{
this
.
$refs
.
input
.
focus
();
},
},
};
</
script
>
<
template
>
<div
:class=
"
{ focus: isInputFocused }"
class="form-control tokenized-input-wrapper d-flex flex-wrap align-items-center"
@click="focusInput"
>
<div
v-for=
"(token, index) in projectTokens"
:key=
"token.id"
class=
"d-flex"
@
click
.
stop
>
<div
class=
"js-input-token input-token text-secondary py-0 pl-2 pr-1 rounded-left"
>
{{
token
.
name_with_namespace
}}
</div>
<div
class=
"js-token-remove tokenized-input-token-remove d-flex align-items-center text-secondary py-0 px-1 rounded-right"
@
click=
"removeProjectTokenAt(index)"
>
<icon
name=
"close"
/>
</div>
</div>
<div
class=
"d-flex align-items-center flex-grow-1"
>
<input
ref=
"input"
v-model=
"localInputValue"
:placeholder=
"__('Search your projects')"
class=
"tokenized-input flex-grow-1"
type=
"text"
@
focus=
"onFocus"
@
blur=
"onBlur"
/>
<icon
name=
"search"
class=
"text-secondary"
/>
</div>
</div>
</
template
>
ee/app/assets/javascripts/operations/mixins.js
0 → 100644
View file @
58dae1a2
export
default
{
data
()
{
return
{
isInputFocused
:
false
,
};
},
methods
:
{
onFocus
()
{
this
.
isInputFocused
=
true
;
this
.
$emit
(
'
focus
'
);
},
onBlur
()
{
this
.
isInputFocused
=
false
;
this
.
$emit
(
'
blur
'
);
},
},
};
ee/app/assets/javascripts/operations/store/actions.js
0 → 100644
View file @
58dae1a2
import
Api
from
'
~/api
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
createFlash
from
'
~/flash
'
;
import
{
__
,
s__
,
n__
,
sprintf
}
from
'
~/locale
'
;
import
*
as
types
from
'
./mutation_types
'
;
export
const
addProjectsToDashboard
=
({
state
,
dispatch
})
=>
{
axios
.
post
(
state
.
projectEndpoints
.
add
,
{
project_ids
:
state
.
projectTokens
.
map
(
project
=>
project
.
id
),
})
.
then
(
response
=>
dispatch
(
'
requestAddProjectsToDashboardSuccess
'
,
response
.
data
))
.
catch
(()
=>
dispatch
(
'
requestAddProjectsToDashboardError
'
));
};
export
const
clearInputValue
=
({
commit
})
=>
{
commit
(
types
.
SET_INPUT_VALUE
,
''
);
};
export
const
clearProjectTokens
=
({
commit
})
=>
{
commit
(
types
.
SET_PROJECT_TOKENS
,
[]);
};
export
const
filterProjectTokensById
=
({
commit
,
state
},
ids
)
=>
{
const
tokens
=
state
.
projectTokens
.
filter
(
token
=>
ids
.
includes
(
token
.
id
));
commit
(
types
.
SET_PROJECT_TOKENS
,
tokens
);
};
export
const
requestAddProjectsToDashboardSuccess
=
({
dispatch
},
data
)
=>
{
const
{
added
,
invalid
}
=
data
;
dispatch
(
'
clearInputValue
'
);
if
(
invalid
.
length
)
{
createFlash
(
s__
(
'
OperationsDashboard|Some projects could not be added to dashboard
'
));
dispatch
(
'
filterProjectTokensById
'
,
invalid
);
}
else
{
dispatch
(
'
clearProjectTokens
'
);
}
if
(
added
.
length
)
{
dispatch
(
'
fetchProjects
'
);
}
};
export
const
requestAddProjectsToDashboardError
=
({
state
})
=>
{
createFlash
(
sprintf
(
__
(
'
Something went wrong, unable to add %{project} to dashboard
'
),
{
project
:
n__
(
'
project
'
,
'
projects
'
,
state
.
projectTokens
.
length
),
}),
);
};
export
const
addProjectToken
=
({
commit
},
project
)
=>
{
commit
(
types
.
ADD_PROJECT_TOKEN
,
project
);
};
export
const
clearProjectSearchResults
=
({
commit
})
=>
{
commit
(
types
.
SET_PROJECT_SEARCH_RESULTS
,
[]);
};
export
const
fetchProjects
=
({
state
,
dispatch
})
=>
{
dispatch
(
'
requestProjects
'
);
axios
.
get
(
state
.
projectEndpoints
.
list
)
.
then
(
response
=>
dispatch
(
'
receiveProjectsSuccess
'
,
response
.
data
))
.
catch
(()
=>
dispatch
(
'
receiveProjectsError
'
))
.
then
(()
=>
dispatch
(
'
requestProjects
'
))
.
catch
(()
=>
{});
};
export
const
requestProjects
=
({
commit
})
=>
{
commit
(
types
.
TOGGLE_IS_LOADING_PROJECTS
);
};
export
const
receiveProjectsSuccess
=
({
commit
},
data
)
=>
{
commit
(
types
.
SET_PROJECTS
,
data
.
projects
);
};
export
const
receiveProjectsError
=
({
commit
})
=>
{
commit
(
types
.
SET_PROJECTS
,
null
);
createFlash
(
__
(
'
Something went wrong, unable to get operations projects
'
));
};
export
const
removeProject
=
({
dispatch
},
removePath
)
=>
{
axios
.
delete
(
removePath
)
.
then
(()
=>
dispatch
(
'
requestRemoveProjectSuccess
'
))
.
catch
(()
=>
dispatch
(
'
requestRemoveProjectError
'
));
};
export
const
requestRemoveProjectSuccess
=
({
dispatch
})
=>
{
dispatch
(
'
fetchProjects
'
);
};
export
const
requestRemoveProjectError
=
()
=>
{
createFlash
(
__
(
'
Something went wrong, unable to remove project
'
));
};
export
const
removeProjectTokenAt
=
({
commit
},
index
)
=>
{
commit
(
types
.
REMOVE_PROJECT_TOKEN_AT
,
index
);
};
export
const
searchProjects
=
({
commit
},
query
)
=>
{
commit
(
types
.
INCREMENT_PROJECT_SEARCH_COUNT
,
1
);
Api
.
projects
(
query
,
{})
.
then
(
data
=>
data
)
.
catch
(()
=>
[])
.
then
(
results
=>
{
commit
(
types
.
SET_PROJECT_SEARCH_RESULTS
,
results
);
commit
(
types
.
DECREMENT_PROJECT_SEARCH_COUNT
,
1
);
})
.
catch
(()
=>
{});
};
export
const
setInputValue
=
({
commit
},
value
)
=>
{
commit
(
types
.
SET_INPUT_VALUE
,
value
);
};
export
const
setProjectEndpoints
=
({
commit
},
endpoints
)
=>
{
commit
(
types
.
SET_PROJECT_ENDPOINT_LIST
,
endpoints
.
list
);
commit
(
types
.
SET_PROJECT_ENDPOINT_ADD
,
endpoints
.
add
);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export
default
()
=>
{};
ee/app/assets/javascripts/operations/store/index.js
0 → 100644
View file @
58dae1a2
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
state
from
'
./state
'
;
import
mutations
from
'
./mutations
'
;
import
*
as
actions
from
'
./actions
'
;
Vue
.
use
(
Vuex
);
export
default
new
Vuex
.
Store
({
state
,
mutations
,
actions
,
});
ee/app/assets/javascripts/operations/store/mutation_types.js
0 → 100644
View file @
58dae1a2
export
const
ADD_PROJECT_TOKEN
=
'
ADD_PROJECT_TOKEN
'
;
export
const
INCREMENT_PROJECT_SEARCH_COUNT
=
'
INCREMENT_PROJECT_SEARCH_COUNT
'
;
export
const
DECREMENT_PROJECT_SEARCH_COUNT
=
'
DECREMENT_PROJECT_SEARCH_COUNT
'
;
export
const
SET_INPUT_VALUE
=
'
SET_INPUT_VALUE
'
;
export
const
SET_PROJECT_ENDPOINT_LIST
=
'
SET_PROJECT_ENDPOINT_LIST
'
;
export
const
SET_PROJECT_ENDPOINT_ADD
=
'
SET_PROJECT_ENDPOINT_ADD
'
;
export
const
SET_PROJECT_SEARCH_RESULTS
=
'
SET_PROJECT_SEARCH_RESULTS
'
;
export
const
SET_PROJECTS
=
'
SET_PROJECTS
'
;
export
const
SET_PROJECT_TOKENS
=
'
SET_PROJECT_TOKENS
'
;
export
const
REMOVE_PROJECT_TOKEN_AT
=
'
REMOVE_PROJECT_TOKEN_AT
'
;
export
const
TOGGLE_IS_LOADING_PROJECTS
=
'
TOGGLE_IS_LOADING_PROJECTS
'
;
ee/app/assets/javascripts/operations/store/mutations.js
0 → 100644
View file @
58dae1a2
import
*
as
types
from
'
./mutation_types
'
;
export
default
{
[
types
.
ADD_PROJECT_TOKEN
](
state
,
project
)
{
const
projectsWithMatchingId
=
state
.
projectTokens
.
filter
(
token
=>
token
.
id
===
project
.
id
);
if
(
!
projectsWithMatchingId
.
length
)
{
state
.
projectTokens
.
push
(
project
);
}
},
[
types
.
DECREMENT_PROJECT_SEARCH_COUNT
](
state
,
value
)
{
state
.
searchCount
-=
value
;
},
[
types
.
INCREMENT_PROJECT_SEARCH_COUNT
](
state
,
value
)
{
state
.
searchCount
+=
value
;
},
[
types
.
SET_INPUT_VALUE
](
state
,
value
)
{
state
.
inputValue
=
value
;
},
[
types
.
SET_PROJECT_ENDPOINT_LIST
](
state
,
url
)
{
state
.
projectEndpoints
.
list
=
url
;
},
[
types
.
SET_PROJECT_ENDPOINT_ADD
](
state
,
url
)
{
state
.
projectEndpoints
.
add
=
url
;
},
[
types
.
SET_PROJECT_SEARCH_RESULTS
](
state
,
results
)
{
state
.
projectSearchResults
=
results
;
},
[
types
.
SET_PROJECTS
](
state
,
projects
)
{
state
.
projects
=
projects
||
[];
},
[
types
.
SET_PROJECT_TOKENS
](
state
,
tokens
)
{
state
.
projectTokens
=
tokens
;
},
[
types
.
REMOVE_PROJECT_TOKEN_AT
](
state
,
index
)
{
state
.
projectTokens
.
splice
(
index
,
1
);
},
[
types
.
TOGGLE_IS_LOADING_PROJECTS
](
state
)
{
state
.
isLoadingProjects
=
!
state
.
isLoadingProjects
;
},
};
ee/app/assets/javascripts/operations/store/state.js
0 → 100644
View file @
58dae1a2
export
default
()
=>
({
inputValue
:
''
,
isLoadingProjects
:
false
,
projectEndpoints
:
{
list
:
null
,
add
:
null
,
},
projects
:
[],
projectTokens
:
[],
projectSearchResults
:
[],
searchCount
:
0
,
});
ee/app/assets/javascripts/pages/operations/index.js
0 → 100644
View file @
58dae1a2
import
Vue
from
'
vue
'
;
import
store
from
'
ee/operations/store
'
;
import
DashboardComponent
from
'
ee/operations/components/dashboard/dashboard.vue
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
new
Vue
({
el
:
'
#js-operations
'
,
store
,
components
:
{
DashboardComponent
,
},
render
(
createElement
)
{
return
createElement
(
DashboardComponent
,
{
props
:
{
listPath
:
this
.
$el
.
dataset
.
listPath
,
addPath
:
this
.
$el
.
dataset
.
addPath
,
emptyDashboardSvgPath
:
this
.
$el
.
dataset
.
emptyDashboardSvgPath
,
},
});
},
}),
);
ee/app/assets/stylesheets/pages/operations.scss
0 → 100644
View file @
58dae1a2
.odds-md-pad-right
:nth-child
(
odd
)
{
padding
:
0
;
@include
media-breakpoint-up
(
md
)
{
padding-right
:
$gl-padding-8
;
}
}
.evens-md-pad-left
:nth-child
(
even
)
{
padding
:
0
;
@include
media-breakpoint-up
(
md
)
{
padding-left
:
$gl-padding-8
;
}
}
.operations-dashboard
{
.branch-commit
{
*
{
vertical-align
:
middle
;
}
.icon-container
,
.commit-icon
{
display
:
inline
;
color
:
$gl-text-color-tertiary
;
}
.ref-name
{
font-weight
:
$gl-font-weight-bold
;
color
:
$gl-text-color
;
}
}
}
.tokenized-input-wrapper
{
height
:
auto
;
padding
:
2px
$gl-padding-8
;
&
.focus
,
&
.focus
:hover
{
border-color
:
$blue-300
;
box-shadow
:
0
0
4px
$dropdown-input-focus-shadow
;
}
.tokenized-input
{
width
:
auto
;
border
:
0
;
margin
:
$gl-bar-padding
0
;
&
:focus
{
outline
:
none
;
box-shadow
:
none
;
}
}
.input-token
{
word-break
:
break-all
;
background-color
:
$gray-lighter
;
margin
:
$gl-bar-padding
0
;
}
.tokenized-input-token-remove
{
background-color
:
$gray-normal
;
margin
:
$gl-bar-padding
$gl-padding-4
$gl-bar-padding
0
;
}
}
ee/app/controllers/ee/root_controller.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
module
EE
module
RootController
extend
::
Gitlab
::
Utils
::
Override
override
:redirect_logged_user
def
redirect_logged_user
case
current_user
.
dashboard
when
'operations'
if
current_user
.
can?
(
:read_operations_dashboard
)
return
redirect_to
(
operations_path
)
end
end
super
end
end
end
ee/app/controllers/operations_controller.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
class
OperationsController
<
ApplicationController
layout
'fullscreen'
before_action
:authorize_read_operations_dashboard!
respond_to
:json
,
only:
[
:list
]
def
index
end
def
list
projects
=
load_projects
(
current_user
)
render
json:
{
projects:
serialize_as_json
(
projects
)
}
end
def
create
project_ids
=
params
[
'project_ids'
]
result
=
add_projects
(
current_user
,
project_ids
)
render
json:
{
added:
result
.
added_project_ids
,
duplicate:
result
.
duplicate_project_ids
,
invalid:
result
.
invalid_project_ids
}
end
def
destroy
project_id
=
params
[
'project_id'
]
if
remove_project
(
current_user
,
project_id
)
head
:ok
else
head
:no_content
end
end
private
def
authorize_read_operations_dashboard!
render_404
unless
can?
(
current_user
,
:read_operations_dashboard
)
end
def
load_projects
(
current_user
)
Dashboard
::
Operations
::
ListService
.
new
(
current_user
).
execute
end
def
add_projects
(
current_user
,
project_ids
)
UsersOpsDashboardProjects
::
CreateService
.
new
(
current_user
).
execute
(
project_ids
)
end
def
remove_project
(
current_user
,
project_id
)
UsersOpsDashboardProjects
::
DestroyService
.
new
(
current_user
).
execute
(
project_id
)
end
def
serialize_as_json
(
projects
)
DashboardOperationsSerializer
.
new
(
current_user:
current_user
).
represent
(
projects
).
as_json
end
end
ee/app/finders/ee/projects_finder.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
module
EE
# ProjectsFinder
#
# Extends ProjectsFinder
#
# Added arguments:
# params:
# plans: string[]
module
ProjectsFinder
extend
::
Gitlab
::
Utils
::
Override
private
override
:filter_projects
def
filter_projects
(
collection
)
collection
=
super
(
collection
)
collection
=
by_plans
(
collection
)
collection
end
def
by_plans
(
collection
)
if
names
=
params
[
:plans
].
presence
collection
.
for_plan_name
(
names
)
else
collection
end
end
end
end
ee/app/helpers/ee/preferences_helper.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
module
EE
module
PreferencesHelper
extend
::
Gitlab
::
Utils
::
Override
override
:excluded_dashboard_choices
def
excluded_dashboard_choices
return
[]
if
can?
(
current_user
,
:read_operations_dashboard
)
super
end
end
end
ee/app/helpers/operations_helper.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
module
OperationsHelper
def
operations_data
{
'add-path'
=>
add_operations_project_path
,
'list-path'
=>
operations_list_path
,
'empty-dashboard-svg-path'
=>
image_path
(
'illustrations/operations-dashboard_empty.svg'
)
}
end
end
ee/app/models/ee/project.rb
View file @
58dae1a2
...
@@ -74,6 +74,7 @@ module EE
...
@@ -74,6 +74,7 @@ module EE
scope
:verified_wikis
,
->
{
joins
(
:repository_state
).
merge
(
ProjectRepositoryState
.
verified_wikis
)
}
scope
:verified_wikis
,
->
{
joins
(
:repository_state
).
merge
(
ProjectRepositoryState
.
verified_wikis
)
}
scope
:verification_failed_repos
,
->
{
joins
(
:repository_state
).
merge
(
ProjectRepositoryState
.
verification_failed_repos
)
}
scope
:verification_failed_repos
,
->
{
joins
(
:repository_state
).
merge
(
ProjectRepositoryState
.
verification_failed_repos
)
}
scope
:verification_failed_wikis
,
->
{
joins
(
:repository_state
).
merge
(
ProjectRepositoryState
.
verification_failed_wikis
)
}
scope
:verification_failed_wikis
,
->
{
joins
(
:repository_state
).
merge
(
ProjectRepositoryState
.
verification_failed_wikis
)
}
scope
:for_plan_name
,
->
(
name
)
{
joins
(
namespace: :plan
).
where
(
plans:
{
name:
name
})
}
delegate
:shared_runners_minutes
,
:shared_runners_seconds
,
:shared_runners_seconds_last_reset
,
delegate
:shared_runners_minutes
,
:shared_runners_seconds
,
:shared_runners_seconds_last_reset
,
to: :statistics
,
allow_nil:
true
to: :statistics
,
allow_nil:
true
...
...
ee/app/models/ee/user.rb
View file @
58dae1a2
...
@@ -35,6 +35,9 @@ module EE
...
@@ -35,6 +35,9 @@ module EE
has_many
:developer_groups
,
->
{
where
(
members:
{
access_level:
::
Gitlab
::
Access
::
DEVELOPER
})
},
through: :group_members
,
source: :group
has_many
:developer_groups
,
->
{
where
(
members:
{
access_level:
::
Gitlab
::
Access
::
DEVELOPER
})
},
through: :group_members
,
source: :group
has_many
:users_ops_dashboard_projects
has_many
:ops_dashboard_projects
,
through: :users_ops_dashboard_projects
,
source: :project
# Protected Branch Access
# Protected Branch Access
has_many
:protected_branch_merge_access_levels
,
dependent: :destroy
,
class_name:
::
ProtectedBranch
::
MergeAccessLevel
# rubocop:disable Cop/ActiveRecordDependent
has_many
:protected_branch_merge_access_levels
,
dependent: :destroy
,
class_name:
::
ProtectedBranch
::
MergeAccessLevel
# rubocop:disable Cop/ActiveRecordDependent
has_many
:protected_branch_push_access_levels
,
dependent: :destroy
,
class_name:
::
ProtectedBranch
::
PushAccessLevel
# rubocop:disable Cop/ActiveRecordDependent
has_many
:protected_branch_push_access_levels
,
dependent: :destroy
,
class_name:
::
ProtectedBranch
::
PushAccessLevel
# rubocop:disable Cop/ActiveRecordDependent
...
@@ -122,7 +125,7 @@ module EE
...
@@ -122,7 +125,7 @@ module EE
def
available_custom_project_templates
(
search:
nil
)
def
available_custom_project_templates
(
search:
nil
)
templates
=
::
Gitlab
::
CurrentSettings
.
available_custom_project_templates
templates
=
::
Gitlab
::
CurrentSettings
.
available_custom_project_templates
ProjectsFinder
.
new
(
current_user:
self
,
::
ProjectsFinder
.
new
(
current_user:
self
,
project_ids_relation:
templates
,
project_ids_relation:
templates
,
params:
{
search:
search
,
sort:
'name_asc'
})
params:
{
search:
search
,
sort:
'name_asc'
})
.
execute
.
execute
...
...
ee/app/models/license.rb
View file @
58dae1a2
...
@@ -86,6 +86,7 @@ class License < ActiveRecord::Base
...
@@ -86,6 +86,7 @@ class License < ActiveRecord::Base
pod_logs
pod_logs
pseudonymizer
pseudonymizer
prometheus_alerts
prometheus_alerts
operations_dashboard
]
.
freeze
]
.
freeze
# List all features available for early adopters,
# List all features available for early adopters,
...
...
ee/app/models/prometheus_alert_event.rb
View file @
58dae1a2
...
@@ -47,6 +47,18 @@ class PrometheusAlertEvent < ActiveRecord::Base
...
@@ -47,6 +47,18 @@ class PrometheusAlertEvent < ActiveRecord::Base
scope
:firing
,
->
{
where
(
status:
status_value_for
(
:firing
))
}
scope
:firing
,
->
{
where
(
status:
status_value_for
(
:firing
))
}
scope
:resolved
,
->
{
where
(
status:
status_value_for
(
:resolved
))
}
scope
:resolved
,
->
{
where
(
status:
status_value_for
(
:resolved
))
}
scope
:for_environment
,
->
(
environment
)
do
joins
(
:prometheus_alert
).
where
(
prometheus_alerts:
{
environment_id:
environment
})
end
scope
:count_by_project_id
,
->
{
group
(
:project_id
).
count
}
scope
:with_prometheus_alert
,
->
{
includes
(
:prometheus_alert
)
}
def
self
.
last_by_project_id
ids
=
select
(
arel_table
[
:id
].
maximum
.
as
(
'id'
)).
group
(
:project_id
).
map
(
&
:id
)
with_prometheus_alert
.
find
(
ids
)
end
def
self
.
find_or_initialize_by_payload_key
(
project
,
alert
,
payload_key
)
def
self
.
find_or_initialize_by_payload_key
(
project
,
alert
,
payload_key
)
find_or_initialize_by
(
project:
project
,
prometheus_alert:
alert
,
payload_key:
payload_key
)
find_or_initialize_by
(
project:
project
,
prometheus_alert:
alert
,
payload_key:
payload_key
)
end
end
...
...
ee/app/models/users_ops_dashboard_project.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
class
UsersOpsDashboardProject
<
ActiveRecord
::
Base
belongs_to
:project
belongs_to
:user
validates
:user
,
presence:
true
validates
:user_id
,
uniqueness:
{
scope:
[
:project_id
]
}
validates
:project
,
presence:
true
end
ee/app/policies/ee/global_policy.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
module
EE
module
GlobalPolicy
extend
ActiveSupport
::
Concern
prepended
do
condition
(
:operations_dashboard_available
)
do
License
.
feature_available?
(
:operations_dashboard
)
end
rule
{
operations_dashboard_available
}.
enable
:read_operations_dashboard
end
end
end
ee/app/serializers/dashboard_operations_project_entity.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
class
DashboardOperationsProjectEntity
<
Grape
::
Entity
include
RequestAwareEntity
expose
:project
,
merge:
true
,
using:
API
::
Entities
::
BasicProjectDetails
expose
:remove_path
do
|
dashboard_project
|
remove_operations_project_path
(
project_id:
dashboard_project
.
project
.
id
)
end
expose
:last_deployment
,
if:
->
(
*
)
{
last_deployment?
}
do
|
dashboard_project
,
options
|
new_request
=
EntityRequest
.
new
(
current_user:
request
.
current_user
,
project:
dashboard_project
.
project
)
DeploymentEntity
.
represent
(
dashboard_project
.
last_deployment
,
options
.
merge
(
request:
new_request
))
end
expose
:alert_count
expose
:alert_path
,
if:
->
(
*
)
{
last_deployment?
}
do
|
dashboard_project
|
project
=
dashboard_project
.
project
environment
=
dashboard_project
.
last_deployment
.
environment
metrics_project_environment_path
(
project
,
environment
)
end
expose
:last_alert
,
using:
PrometheusAlertEntity
,
if:
->
(
*
)
{
last_alert?
}
private
alias_method
:dashboard_project
,
:object
def
last_deployment?
dashboard_project
.
last_deployment
end
def
last_alert?
dashboard_project
.
last_alert
end
end
ee/app/serializers/dashboard_operations_serializer.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
class
DashboardOperationsSerializer
<
BaseSerializer
entity
DashboardOperationsProjectEntity
end
ee/app/services/dashboard/operations/list_service.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
module
Dashboard
module
Operations
class
ListService
DashboardProject
=
Struct
.
new
(
:project
,
:last_deployment
,
:alert_count
,
:last_alert
)
def
initialize
(
user
)
@user
=
user
end
def
execute
projects
=
load_projects
(
user
)
environments
=
load_environments
(
projects
,
'production'
)
last_deployments
=
load_last_deployments
(
environments
)
event_counts
,
last_firing_events
=
load_last_firing_events
(
environments
)
collect_data
(
projects
,
last_deployments
,
event_counts
,
last_firing_events
)
end
private
attr_reader
:user
def
load_projects
(
user
)
projects
=
user
.
ops_dashboard_projects
ProjectsService
.
new
(
user
)
.
execute
(
projects
)
.
to_a
# 1 query
end
# 1 query
def
load_environments
(
projects
,
name
)
return
{}
if
projects
.
empty?
Environment
.
available
.
for_project
(
projects
)
.
for_name
(
name
)
.
index_by
(
&
:project_id
)
# 1 query
end
def
load_last_deployments
(
environments
)
return
{}
if
environments
.
empty?
Deployment
.
last_for_environment
(
environments
.
values
)
# 2 queries
.
index_by
(
&
:project_id
)
end
def
load_last_firing_events
(
environments
)
return
[
0
,
{}]
if
environments
.
empty?
events
=
PrometheusAlertEvent
.
firing
.
for_environment
(
environments
.
values
)
event_counts
=
events
.
count_by_project_id
# 1 query
last_firing_events
=
events
.
last_by_project_id
.
index_by
(
&
:project_id
)
# 2 queries
[
event_counts
,
last_firing_events
]
end
def
collect_data
(
projects
,
last_deployments
,
event_counts
,
last_firing_events
)
projects
.
map
do
|
project
|
last_deployment
=
last_deployments
[
project
.
id
]
alert_count
=
event_counts
[
project
.
id
]
||
0
last_alert
=
last_firing_events
[
project
.
id
]
&
.
prometheus_alert
DashboardProject
.
new
(
project
,
last_deployment
,
alert_count
,
last_alert
)
end
end
end
end
end
ee/app/services/dashboard/operations/projects_service.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
module
Dashboard
module
Operations
class
ProjectsService
def
initialize
(
user
)
@user
=
user
end
def
execute
(
project_ids
)
find_projects
(
user
,
project_ids
)
end
private
attr_reader
:user
,
:project_ids
def
find_projects
(
user
,
project_ids
)
ProjectsFinder
.
new
(
current_user:
user
,
project_ids_relation:
project_ids
,
params:
{
plans:
plan_names_for_operations_dashboard
,
min_access_level:
ProjectMember
::
DEVELOPER
}
).
execute
end
def
plan_names_for_operations_dashboard
return
unless
Gitlab
::
CurrentSettings
.
should_check_namespace_plan?
Namespace
.
plans_with_feature
(
:operations_dashboard
)
end
end
end
end
ee/app/services/ee/groups/update_service.rb
View file @
58dae1a2
...
@@ -37,7 +37,7 @@ module EE
...
@@ -37,7 +37,7 @@ module EE
end
end
def
file_template_project_visible?
def
file_template_project_visible?
ProjectsFinder
.
new
(
::
ProjectsFinder
.
new
(
current_user:
current_user
,
current_user:
current_user
,
project_ids_relation:
[
params
[
:file_template_project_id
]]
project_ids_relation:
[
params
[
:file_template_project_id
]]
).
execute
.
exists?
).
execute
.
exists?
...
...
ee/app/services/users_ops_dashboard_projects/base_service.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
module
UsersOpsDashboardProjects
class
BaseService
attr_reader
:user
def
initialize
(
user
)
@user
=
user
end
end
end
ee/app/services/users_ops_dashboard_projects/create_service.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
module
UsersOpsDashboardProjects
class
CreateService
<
UsersOpsDashboardProjects
::
BaseService
Result
=
Struct
.
new
(
:added_project_ids
,
:invalid_project_ids
,
:duplicate_project_ids
)
def
execute
(
project_ids
)
projects_to_add
=
load_projects
(
user
,
project_ids
)
invalid
=
find_invalid_ids
(
projects_to_add
,
project_ids
)
added
,
duplicate
=
add_projects
(
projects_to_add
,
user
)
Result
.
new
(
added
.
map
(
&
:id
),
invalid
,
duplicate
.
map
(
&
:id
))
end
private
def
load_projects
(
current_user
,
project_ids
)
Dashboard
::
Operations
::
ProjectsService
.
new
(
current_user
).
execute
(
project_ids
)
end
def
find_invalid_ids
(
projects_to_add
,
project_ids
)
by_string_id
=
projects_to_add
.
index_by
{
|
project
|
project
.
id
.
to_s
}
project_ids
.
reject
{
|
id
|
by_string_id
.
key?
(
id
.
to_s
)
}
end
def
add_projects
(
projects
,
user
)
projects
.
partition
{
|
project
|
add_project
(
project
,
user
)
}
end
def
add_project
(
project
,
user
)
user
.
ops_dashboard_projects
<<
project
true
rescue
ActiveRecord
::
RecordInvalid
false
end
end
end
ee/app/services/users_ops_dashboard_projects/destroy_service.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
module
UsersOpsDashboardProjects
class
DestroyService
<
UsersOpsDashboardProjects
::
BaseService
def
execute
(
project_id
)
remove_project
(
user
,
project_id
)
end
private
def
remove_project
(
user
,
project_id
)
user
.
ops_dashboard_projects
.
destroy
(
project_id
).
first
rescue
ActiveRecord
::
RecordNotFound
nil
end
end
end
ee/app/views/dashboard/operations/_nav_link.html.haml
0 → 100644
View file @
58dae1a2
-
if
can?
(
current_user
,
:read_operations_dashboard
)
=
nav_link
(
controller:
'operations'
)
do
=
link_to
operations_path
,
title:
_
(
'Operations'
),
aria:
{
label:
_
(
'Operations'
)
}
do
=
sprite_icon
(
'dashboard'
,
size:
18
)
ee/app/views/operations/index.html.haml
0 → 100644
View file @
58dae1a2
-
page_title
_
(
'Operations'
)
#js-operations
{
data:
operations_data
}
ee/changelogs/unreleased/5781-operations-homepage-mvc-frontend.yml
0 → 100644
View file @
58dae1a2
---
title
:
Add project operations dashboard
merge_request
:
7973
author
:
type
:
added
ee/config/routes/operations.rb
View file @
58dae1a2
# frozen_string_literal: true
# frozen_string_literal: true
# Placeholder for https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/7341
get
'operations'
=>
'operations#index'
# Added to resolve https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/8131
get
'operations/list'
=>
'operations#list'
post
'operations'
=>
'operations#create'
,
as: :add_operations_project
delete
'operations'
=>
'operations#destroy'
,
as: :remove_operations_project
ee/db/migrate/20181012151642_create_users_ops_dashboard_projects.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
class
CreateUsersOpsDashboardProjects
<
ActiveRecord
::
Migration
include
Gitlab
::
Database
::
MigrationHelpers
DOWNTIME
=
false
def
change
create_table
:users_ops_dashboard_projects
,
id: :bigserial
do
|
t
|
t
.
timestamps_with_timezone
null:
false
t
.
references
:user
,
null:
false
,
foreign_key:
{
on_delete: :cascade
}
t
.
references
:project
,
index:
true
,
foreign_key:
{
on_delete: :cascade
},
null:
false
t
.
index
[
:user_id
,
:project_id
],
unique:
true
end
end
end
ee/spec/controllers/ee/root_controller_spec.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
require
'spec_helper'
describe
RootController
do
describe
'GET index'
do
let
(
:user
)
{
create
(
:user
)
}
before
do
stub_licensed_features
(
operations_dashboard:
true
)
sign_in
(
user
)
allow
(
subject
).
to
receive
(
:current_user
).
and_return
(
user
)
end
context
'who has customized their dashboard setting for operations'
do
before
do
user
.
dashboard
=
'operations'
end
it
'redirects to operations dashboard'
do
get
:index
expect
(
response
).
to
redirect_to
operations_path
end
context
'when unlicensed'
do
before
do
stub_licensed_features
(
operations_dashboard:
false
)
end
it
'renders the default dashboard'
do
get
:index
expect
(
response
).
to
render_template
'dashboard/projects/index'
end
end
end
end
end
ee/spec/controllers/operations_controller_spec.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
require
'spec_helper'
describe
OperationsController
do
include
Rails
.
application
.
routes
.
url_helpers
let
(
:user
)
{
create
(
:user
)
}
let
(
:json_response
)
{
JSON
.
parse
(
response
.
body
)
}
shared_examples
'unlicensed'
do
|
http_method
,
action
|
before
do
stub_licensed_features
(
operations_dashboard:
false
)
end
it
'renders 404'
do
public_send
(
http_method
,
action
)
expect
(
response
).
to
have_gitlab_http_status
(
:not_found
)
end
end
before
do
stub_licensed_features
(
operations_dashboard:
true
)
sign_in
(
user
)
end
describe
'GET #index'
do
it_behaves_like
'unlicensed'
,
:get
,
:index
it
'renders index with 200 status code'
do
get
:index
expect
(
response
).
to
have_gitlab_http_status
(
200
)
expect
(
response
).
to
render_template
(
:index
)
end
context
'with an anonymous user'
do
before
do
sign_out
(
user
)
end
it
'redirects to sign-in page'
do
get
:index
expect
(
response
).
to
redirect_to
(
new_user_session_path
)
end
end
end
describe
'GET #list'
do
let
(
:now
)
{
Time
.
now
.
change
(
usec:
0
)
}
let
(
:project
)
{
create
(
:project
,
:repository
)
}
let
(
:commit
)
{
project
.
commit
}
let!
(
:environment
)
{
create
(
:environment
,
name:
'production'
,
project:
project
)
}
let!
(
:deployment
)
{
create
(
:deployment
,
environment:
environment
,
sha:
commit
.
id
,
created_at:
now
)
}
it_behaves_like
'unlicensed'
,
:get
,
:list
shared_examples
'empty project list'
do
it
'returns an empty list'
do
get
:list
expect
(
response
).
to
have_gitlab_http_status
(
200
)
expect
(
json_response
).
to
match_schema
(
'dashboard/operations/list'
,
dir:
'ee'
)
expect
(
json_response
[
'projects'
]).
to
eq
([])
end
end
context
'with added projects'
do
let
(
:alert1
)
{
create
(
:prometheus_alert
,
project:
project
,
environment:
environment
)
}
let
(
:alert2
)
{
create
(
:prometheus_alert
,
project:
project
,
environment:
environment
)
}
let!
(
:alert_events
)
do
[
create
(
:prometheus_alert_event
,
prometheus_alert:
alert1
),
create
(
:prometheus_alert_event
,
prometheus_alert:
alert2
),
create
(
:prometheus_alert_event
,
prometheus_alert:
alert1
),
create
(
:prometheus_alert_event
,
:resolved
,
prometheus_alert:
alert2
)
]
end
let
(
:firing_alert_events
)
{
alert_events
.
select
(
&
:firing?
)
}
let
(
:last_firing_alert
)
{
firing_alert_events
.
last
.
prometheus_alert
}
let
(
:alert_path
)
do
metrics_project_environment_path
(
project
,
environment
)
end
let
(
:alert_json_path
)
do
project_prometheus_alert_path
(
project
,
last_firing_alert
.
prometheus_metric_id
,
environment_id:
environment
,
format: :json
)
end
let
(
:expected_project
)
{
json_response
[
'projects'
].
first
}
before
do
user
.
update!
(
ops_dashboard_projects:
[
project
])
project
.
add_developer
(
user
)
end
it
'returns a list of added projects'
do
get
:list
expect
(
response
).
to
have_gitlab_http_status
(
200
)
expect
(
response
).
to
match_response_schema
(
'dashboard/operations/list'
,
dir:
'ee'
)
expect
(
json_response
[
'projects'
].
size
).
to
eq
(
1
)
expect
(
expected_project
[
'id'
]).
to
eq
(
project
.
id
)
expect
(
expected_project
[
'remove_path'
])
.
to
eq
(
remove_operations_project_path
(
project_id:
project
.
id
))
expect
(
expected_project
[
'last_deployment'
][
'id'
]).
to
eq
(
deployment
.
id
)
expect
(
expected_project
[
'alert_count'
]).
to
eq
(
firing_alert_events
.
size
)
expect
(
expected_project
[
'alert_path'
]).
to
eq
(
alert_path
)
expect
(
expected_project
[
'last_alert'
][
'id'
]).
to
eq
(
last_firing_alert
.
id
)
end
context
'without sufficient access level'
do
before
do
project
.
add_reporter
(
user
)
end
it_behaves_like
'empty project list'
end
end
context
'without projects'
do
it_behaves_like
'empty project list'
end
context
'with an anonymous user'
do
before
do
sign_out
(
user
)
end
it
'redirects to sign-in page'
do
get
:list
expect
(
response
).
to
redirect_to
(
new_user_session_path
)
end
end
end
describe
'POST #create'
do
it_behaves_like
'unlicensed'
,
:post
,
:create
context
'without added projects'
do
let
(
:project_a
)
{
create
(
:project
)
}
let
(
:project_b
)
{
create
(
:project
)
}
before
do
project_a
.
add_developer
(
user
)
project_b
.
add_developer
(
user
)
end
it
'adds projects to the dasboard'
do
post
:create
,
project_ids:
[
project_a
.
id
,
project_b
.
id
.
to_s
]
expect
(
response
).
to
have_gitlab_http_status
(
200
)
expect
(
json_response
).
to
match_schema
(
'dashboard/operations/add'
,
dir:
'ee'
)
expect
(
json_response
[
'added'
]).
to
contain_exactly
(
project_a
.
id
,
project_b
.
id
)
expect
(
json_response
[
'duplicate'
]).
to
be_empty
expect
(
json_response
[
'invalid'
]).
to
be_empty
user
.
reload
expect
(
user
.
ops_dashboard_projects
).
to
contain_exactly
(
project_a
,
project_b
)
end
it
'cannot add a project twice'
do
post
:create
,
project_ids:
[
project_a
.
id
,
project_a
.
id
]
expect
(
response
).
to
have_gitlab_http_status
(
200
)
expect
(
json_response
).
to
match_schema
(
'dashboard/operations/add'
,
dir:
'ee'
)
expect
(
json_response
[
'added'
]).
to
contain_exactly
(
project_a
.
id
)
expect
(
json_response
[
'duplicate'
]).
to
be_empty
expect
(
json_response
[
'invalid'
]).
to
be_empty
user
.
reload
expect
(
user
.
ops_dashboard_projects
).
to
eq
([
project_a
])
end
it
'does not add invalid project ids'
do
post
:create
,
project_ids:
[
nil
,
-
1
,
'-2'
]
expect
(
response
).
to
have_gitlab_http_status
(
200
)
expect
(
json_response
).
to
match_schema
(
'dashboard/operations/add'
,
dir:
'ee'
)
expect
(
json_response
[
'added'
]).
to
be_empty
expect
(
json_response
[
'duplicate'
]).
to
be_empty
expect
(
json_response
[
'invalid'
]).
to
contain_exactly
(
nil
,
'-1'
,
'-2'
)
user
.
reload
expect
(
user
.
ops_dashboard_projects
).
to
be_empty
end
end
context
'with added project'
do
let
(
:project
)
{
create
(
:project
)
}
before
do
user
.
ops_dashboard_projects
<<
project
project
.
add_developer
(
user
)
end
it
'does not add already added project'
do
post
:create
,
project_ids:
[
project
.
id
]
expect
(
response
).
to
have_gitlab_http_status
(
200
)
expect
(
json_response
).
to
match_schema
(
'dashboard/operations/add'
,
dir:
'ee'
)
expect
(
json_response
[
'added'
]).
to
be_empty
expect
(
json_response
[
'duplicate'
]).
to
contain_exactly
(
project
.
id
)
expect
(
json_response
[
'invalid'
]).
to
be_empty
user
.
reload
expect
(
user
.
ops_dashboard_projects
).
to
eq
([
project
])
end
end
context
'with an anonymous user'
do
before
do
sign_out
(
user
)
end
it
'redirects to sign-in page'
do
post
:create
expect
(
response
).
to
redirect_to
(
new_user_session_path
)
end
end
end
describe
'DELETE #destroy'
do
it_behaves_like
'unlicensed'
,
:delete
,
:destroy
context
'with added projects'
do
let
(
:project
)
{
create
(
:project
)
}
before
do
user
.
ops_dashboard_projects
<<
project
end
it
'removes a project succesfully'
do
delete
:destroy
,
project_id:
project
.
id
expect
(
response
).
to
have_gitlab_http_status
(
200
)
user
.
reload
expect
(
user
.
ops_dashboard_projects
).
not_to
eq
([
project
])
end
end
context
'without projects'
do
it
'cannot remove invalid project'
do
delete
:destroy
,
project_id:
-
1
expect
(
response
).
to
have_gitlab_http_status
(
204
)
end
end
context
'with an anonymous user'
do
before
do
sign_out
(
user
)
end
it
'redirects to sign-in page'
do
delete
:destroy
expect
(
response
).
to
redirect_to
(
new_user_session_path
)
end
end
end
end
ee/spec/finders/ee/projects_finder_spec.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
require
'spec_helper'
describe
ProjectsFinder
do
describe
'#execute'
do
subject
{
finder
.
execute
}
let
(
:user
)
{
create
(
:user
)
}
describe
'filter by plans'
do
let!
(
:gold_project
)
{
create_project
(
:gold_plan
)
}
let!
(
:gold_project2
)
{
create_project
(
:gold_plan
)
}
let!
(
:silver_project
)
{
create_project
(
:silver_plan
)
}
let!
(
:no_plan_project
)
{
create_project
(
nil
)
}
let
(
:finder
)
{
described_class
.
new
(
params:
{
plans:
plans
})
}
context
'with gold plan'
do
let
(
:plans
)
{
[
'gold'
]
}
it
{
is_expected
.
to
contain_exactly
(
gold_project
,
gold_project2
)
}
end
context
'with multiple plans'
do
let
(
:plans
)
{
%w[gold silver]
}
it
{
is_expected
.
to
contain_exactly
(
gold_project
,
gold_project2
,
silver_project
)
}
end
context
'with other plans'
do
let
(
:plans
)
{
[
'bronze'
]
}
it
{
is_expected
.
to
be_empty
}
end
context
'without plans'
do
let
(
:plans
)
{
nil
}
it
{
is_expected
.
to
contain_exactly
(
gold_project
,
gold_project2
,
silver_project
,
no_plan_project
)
}
end
context
'with empty plans'
do
let
(
:plans
)
{
[]
}
it
{
is_expected
.
to
contain_exactly
(
gold_project
,
gold_project2
,
silver_project
,
no_plan_project
)
}
end
private
def
create_project
(
plan
)
create
(
:project
,
:public
,
namespace:
create
(
:namespace
,
plan:
plan
))
end
end
end
end
ee/spec/fixtures/api/schemas/dashboard/operations/add.json
0 → 100644
View file @
58dae1a2
{
"type"
:
"object"
,
"required"
:
[
"added"
,
"duplicate"
,
"invalid"
],
"properties"
:
{
"added"
:
{
"type"
:
"array"
,
"items"
:
{
"type"
:
"integer"
}
},
"duplicate"
:
{
"type"
:
"array"
,
"items"
:
{
"type"
:
"integer"
}
},
"invalid"
:
{
"type"
:
"array"
,
"items"
:
{
"oneOf"
:
[
{
"type"
:
"string"
},
{
"type"
:
"null"
}
]
}
}
},
"additionalProperties"
:
false
}
ee/spec/fixtures/api/schemas/dashboard/operations/list.json
0 → 100644
View file @
58dae1a2
{
"type"
:
"object"
,
"required"
:
[
"projects"
],
"properties"
:
{
"projects"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"#/definitions/project"
}
}
},
"definitions"
:
{
"project"
:
{
"type"
:
"object"
,
"required"
:
[
"id"
,
"name"
,
"name_with_namespace"
,
"path"
,
"path_with_namespace"
,
"avatar_url"
,
"remove_path"
,
"alert_count"
],
"properties"
:
{
"id"
:
{
"type"
:
"integer"
},
"name"
:
{
"type"
:
"string"
},
"name_with_namespace"
:
{
"type"
:
"string"
},
"path"
:
{
"type"
:
"string"
},
"path_with_namespace"
:
{
"type"
:
"string"
},
"avatar_url"
:
{
"type"
:
[
"string"
,
"null"
]
},
"remove_path"
:
{
"type"
:
"string"
},
"last_deployment"
:
{
"$ref"
:
"../../../../../../../spec/fixtures/api/schemas/deployment.json"
},
"alert_count"
:
{
"type"
:
"integer"
},
"alert_path"
:
{
"type"
:
"string"
},
"last_alert"
:
{
"$ref"
:
"#/definitions/alert"
}
}
},
"alert"
:
{
"type"
:
"object"
,
"required"
:
[
"id"
,
"title"
,
"query"
,
"threshold"
,
"operator"
,
"alert_path"
],
"properties"
:
{
"id"
:
{
"type"
:
"integer"
},
"title"
:
{
"type"
:
"string"
}
}
}
},
"additionalProperties"
:
false
}
ee/spec/helpers/operations_helper_spec.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
require
'spec_helper'
describe
OperationsHelper
do
include
Gitlab
::
Routing
.
url_helpers
describe
'#operations_data'
do
it
'returns frontend configuration'
do
expect
(
operations_data
).
to
eq
(
'add-path'
=>
'/-/operations'
,
'list-path'
=>
'/-/operations/list'
,
'empty-dashboard-svg-path'
=>
'/images/illustrations/operations-dashboard_empty.svg'
)
end
end
end
ee/spec/helpers/preferences_helper_spec.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
require
'spec_helper'
describe
PreferencesHelper
do
describe
'#dashboard_choices'
do
let
(
:user
)
{
build
(
:user
)
}
before
do
allow
(
helper
).
to
receive
(
:current_user
).
and_return
(
user
)
end
context
'when allowed to read operations dashboard'
do
before
do
allow
(
helper
).
to
receive
(
:can?
).
with
(
user
,
:read_operations_dashboard
)
{
true
}
end
it
'does not contain operations dashboard'
do
expect
(
helper
.
dashboard_choices
).
to
include
([
'Operations Dashboard'
,
'operations'
])
end
end
context
'when not allowed to read operations dashboard'
do
before
do
allow
(
helper
).
to
receive
(
:can?
).
with
(
user
,
:read_operations_dashboard
)
{
false
}
end
it
'does not contain operations dashboard'
do
expect
(
helper
.
dashboard_choices
).
not_to
include
([
'Operations Dashboard'
,
'operations'
])
end
end
end
end
ee/spec/javascripts/operations/components/dashboard/alerts_spec.js
0 → 100644
View file @
58dae1a2
import
Vue
from
'
vue
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
Alerts
from
'
ee/operations/components/dashboard/alerts.vue
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
{
removeWhitespace
}
from
'
spec/helpers/vue_component_helper
'
;
import
{
getChildInstances
}
from
'
../../helpers
'
;
import
{
mockOneProject
}
from
'
../../mock_data
'
;
describe
(
'
alerts component
'
,
()
=>
{
const
AlertsComponent
=
Vue
.
extend
(
Alerts
);
const
IconComponent
=
Vue
.
extend
(
Icon
);
const
mockPath
=
'
https://mock-alert_path/
'
;
const
mount
=
(
props
=
{})
=>
mountComponentWithStore
(
AlertsComponent
,
{
props
});
let
vm
;
beforeEach
(()
=>
{
vm
=
mount
();
});
afterEach
(()
=>
{
if
(
vm
.
$destroy
)
{
vm
.
$destroy
();
}
});
it
(
'
renders multiple alert count when multiple alerts are present
'
,
()
=>
{
vm
=
mount
({
count
:
2
});
expect
(
vm
.
$el
.
querySelector
(
'
.js-alert-count
'
).
innerText
.
trim
()).
toBe
(
'
2 Alerts
'
);
});
it
(
'
renders count for one alert when there is one alert
'
,
()
=>
{
vm
=
mount
({
count
:
1
});
expect
(
vm
.
$el
.
querySelector
(
'
.js-alert-count
'
).
innerText
.
trim
()).
toBe
(
'
1 Alert
'
);
});
it
(
'
renders last alert when one has fired
'
,
()
=>
{
const
mockAlert
=
mockOneProject
.
last_alert
;
const
alertMessage
=
`
${
mockAlert
.
title
}
${
mockAlert
.
operator
}
${
mockAlert
.
threshold
}
`
;
vm
=
mount
({
count
:
1
,
alertPath
:
mockPath
,
lastAlert
:
mockAlert
,
});
const
lastAlert
=
vm
.
$el
.
querySelector
(
'
.js-last-alert
'
);
const
innerText
=
removeWhitespace
(
lastAlert
.
innerText
).
trim
();
expect
(
innerText
).
toBe
(
alertMessage
);
});
it
(
'
links last alert to metrics page
'
,
()
=>
{
vm
=
mount
({
alertPath
:
mockPath
});
expect
(
vm
.
$el
.
querySelector
(
'
.js-alert-link
'
).
href
).
toBe
(
mockPath
);
});
it
(
'
does not render last alert message when it has not fired
'
,
()
=>
{
vm
=
mount
({
alertPath
:
mockPath
});
const
lastAlert
=
vm
.
$el
.
querySelector
(
'
.js-last-alert
'
);
expect
(
lastAlert
.
innerText
.
trim
()).
toBe
(
'
None
'
);
});
describe
(
'
wrapped components
'
,
()
=>
{
describe
(
'
icon
'
,
()
=>
{
it
(
'
renders warning
'
,
()
=>
{
const
icons
=
getChildInstances
(
vm
,
IconComponent
);
expect
(
icons
.
length
).
toBe
(
1
);
const
[
icon
]
=
icons
;
expect
(
icon
.
name
).
toBe
(
'
warning
'
);
});
});
});
});
ee/spec/javascripts/operations/components/dashboard/dashboard_spec.js
0 → 100644
View file @
58dae1a2
import
Vue
from
'
vue
'
;
import
store
from
'
ee/operations/store/index
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
Dashboard
from
'
ee/operations/components/dashboard/dashboard.vue
'
;
import
ProjectSearch
from
'
ee/operations/components/dashboard/project_search.vue
'
;
import
DashboardProject
from
'
ee/operations/components/dashboard/project.vue
'
;
import
{
getChildInstances
,
clearState
}
from
'
../../helpers
'
;
import
{
mockProjectData
,
mockText
}
from
'
../../mock_data
'
;
describe
(
'
dashboard component
'
,
()
=>
{
const
DashboardComponent
=
Vue
.
extend
(
Dashboard
);
const
ProjectSearchComponent
=
Vue
.
extend
(
ProjectSearch
);
const
DashboardProjectComponent
=
Vue
.
extend
(
DashboardProject
);
const
projectTokens
=
mockProjectData
(
1
);
const
mount
=
()
=>
mountComponentWithStore
(
DashboardComponent
,
{
store
,
props
:
{
addPath
:
'
mock-addPath
'
,
listPath
:
'
mock-listPath
'
,
emptyDashboardSvgPath
:
'
/assets/illustrations/operations-dashboard_empty.svg
'
,
},
});
let
vm
;
beforeEach
(()
=>
{
vm
=
mount
();
});
afterEach
(()
=>
{
vm
.
$destroy
();
clearState
(
store
);
});
it
(
'
renders dashboard title
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-dashboard-title
'
).
innerText
.
trim
()).
toBe
(
mockText
.
DASHBOARD_TITLE
,
);
});
describe
(
'
add projects button
'
,
()
=>
{
let
button
;
beforeEach
(()
=>
{
button
=
vm
.
$el
.
querySelector
(
'
.js-add-projects-button
'
);
});
it
(
'
renders add projects text
'
,
()
=>
{
expect
(
button
.
innerText
.
trim
()).
toBe
(
mockText
.
ADD_PROJECTS
);
});
it
(
'
calls action to add projects on click if projectTokens have been added
'
,
()
=>
{
const
spy
=
spyOn
(
vm
,
'
addProjectsToDashboard
'
);
vm
.
$store
.
state
.
projectTokens
=
projectTokens
;
button
.
click
();
expect
(
spy
).
toHaveBeenCalled
();
});
it
(
'
does not call action to add projects on click when projectTokens is empty
'
,
()
=>
{
const
spy
=
spyOn
(
vm
,
'
addProjectsToDashboard
'
);
button
.
click
();
expect
(
spy
).
not
.
toHaveBeenCalled
();
});
});
describe
(
'
wrapped components
'
,
()
=>
{
describe
(
'
project search component
'
,
()
=>
{
it
(
'
renders project search component
'
,
()
=>
{
expect
(
getChildInstances
(
vm
,
ProjectSearchComponent
).
length
).
toBe
(
1
);
});
});
describe
(
'
dashboard project component
'
,
()
=>
{
const
projectCount
=
1
;
const
projects
=
mockProjectData
(
projectCount
);
beforeEach
(()
=>
{
store
.
state
.
projects
=
projects
;
vm
=
mount
();
});
it
(
'
includes a dashboard project component for each project
'
,
()
=>
{
expect
(
getChildInstances
(
vm
,
DashboardProjectComponent
).
length
).
toBe
(
projectCount
);
});
it
(
'
passes each project to the dashboard project component
'
,
()
=>
{
const
[
oneProject
]
=
projects
;
const
[
projectComponent
]
=
getChildInstances
(
vm
,
DashboardProjectComponent
);
expect
(
projectComponent
.
project
).
toEqual
(
oneProject
);
});
});
describe
(
'
empty state
'
,
()
=>
{
beforeAll
(
done
=>
{
vm
.
$store
.
dispatch
(
'
requestProjects
'
)
.
then
(()
=>
vm
.
$nextTick
(
done
))
.
catch
(
done
.
fail
);
});
it
(
'
renders empty state svg after requesting projects with no results
'
,
()
=>
{
const
svgSrc
=
vm
.
$el
.
querySelector
(
'
.js-empty-state-svg
'
)
.
src
.
slice
(
-
mockText
.
EMPTY_SVG_SOURCE
.
length
);
expect
(
svgSrc
).
toBe
(
mockText
.
EMPTY_SVG_SOURCE
);
});
it
(
'
renders title
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-title
'
).
innerText
.
trim
()).
toBe
(
mockText
.
EMPTY_TITLE
);
});
it
(
'
renders sub-title
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-sub-title
'
).
innerText
.
trim
()).
toBe
(
mockText
.
EMPTY_SUBTITLE
,
);
});
});
});
});
ee/spec/javascripts/operations/components/dashboard/project_header_spec.js
0 → 100644
View file @
58dae1a2
import
Vue
from
'
vue
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
ProjectHeader
from
'
ee/operations/components/dashboard/project_header.vue
'
;
import
ProjectAvatar
from
'
~/vue_shared/components/project_avatar/default.vue
'
;
import
{
removeWhitespace
}
from
'
spec/helpers/vue_component_helper
'
;
import
{
getChildInstances
}
from
'
../../helpers
'
;
import
{
mockOneProject
,
mockText
}
from
'
../../mock_data
'
;
describe
(
'
project header component
'
,
()
=>
{
const
ProjectHeaderComponent
=
Vue
.
extend
(
ProjectHeader
);
const
ProjectAvatarComponent
=
Vue
.
extend
(
ProjectAvatar
);
let
vm
;
beforeEach
(()
=>
{
vm
=
mountComponentWithStore
(
ProjectHeaderComponent
,
{
props
:
{
project
:
mockOneProject
,
},
});
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
it
(
'
renders project name with namespace
'
,
()
=>
{
const
name
=
vm
.
$el
.
querySelector
(
'
.js-name-with-namespace
'
).
innerText
;
expect
(
removeWhitespace
(
name
).
trim
()).
toBe
(
mockOneProject
.
name_with_namespace
);
});
it
(
'
links project name to project
'
,
()
=>
{
const
path
=
mockOneProject
.
web_url
;
expect
(
vm
.
$el
.
querySelector
(
'
.js-project-link
'
).
href
).
toBe
(
path
);
});
describe
(
'
wrapped components
'
,
()
=>
{
describe
(
'
project avatar
'
,
()
=>
{
it
(
'
renders
'
,
()
=>
{
expect
(
getChildInstances
(
vm
,
ProjectAvatarComponent
).
length
).
toBe
(
1
);
});
it
(
'
binds project
'
,
()
=>
{
const
[
avatar
]
=
getChildInstances
(
vm
,
ProjectAvatarComponent
);
expect
(
avatar
.
project
).
toEqual
(
vm
.
project
);
});
});
});
describe
(
'
dropdown menu
'
,
()
=>
{
it
(
'
renders removal button
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-remove-button
'
).
innerText
.
trim
()).
toBe
(
mockText
.
REMOVE_PROJECT
,
);
});
it
(
'
emits project removal link on click
'
,
()
=>
{
const
spy
=
spyOn
(
vm
,
'
$emit
'
);
vm
.
$el
.
querySelector
(
'
.js-remove-button
'
).
click
();
expect
(
spy
).
toHaveBeenCalledWith
(
'
remove
'
,
mockOneProject
.
remove_path
);
});
});
});
ee/spec/javascripts/operations/components/dashboard/project_search_spec.js
0 → 100644
View file @
58dae1a2
import
Vue
from
'
vue
'
;
import
store
from
'
ee/operations/store/index
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
GlLoadingIcon
}
from
'
@gitlab-org/gitlab-ui
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
ProjectAvatar
from
'
~/vue_shared/components/project_avatar/default.vue
'
;
import
ProjectSearch
from
'
ee/operations/components/dashboard/project_search.vue
'
;
import
TokenizedInput
from
'
ee/operations/components/tokenized_input/input.vue
'
;
import
{
mockText
,
mockProjectData
}
from
'
../../mock_data
'
;
import
{
getChildInstances
,
mouseEvent
,
clearState
}
from
'
../../helpers
'
;
describe
(
'
project search component
'
,
()
=>
{
const
ProjectSearchComponent
=
Vue
.
extend
(
ProjectSearch
);
const
IconComponent
=
Vue
.
extend
(
Icon
);
const
GlLoadingIconComponent
=
Vue
.
extend
(
GlLoadingIcon
);
const
TokenizedInputComponent
=
Vue
.
extend
(
TokenizedInput
);
const
ProjectAvatarComponent
=
Vue
.
extend
(
ProjectAvatar
);
const
mockProjects
=
mockProjectData
(
1
);
const
[
mockOneProject
]
=
mockProjects
;
const
mockInputValue
=
'
mock-inputValue
'
;
const
mount
=
()
=>
mountComponentWithStore
(
ProjectSearchComponent
,
{
store
});
let
vm
;
beforeEach
(()
=>
{
vm
=
mount
();
});
afterEach
(()
=>
{
vm
.
$destroy
();
clearState
(
store
);
});
describe
(
'
dropdown menu
'
,
()
=>
{
it
(
'
renders dropdown menu when input gains focus
'
,
done
=>
{
vm
.
$store
.
dispatch
(
'
setInputValue
'
,
mockInputValue
);
vm
.
isInputFocused
=
true
;
vm
.
$nextTick
(()
=>
{
expect
(
vm
.
$el
.
classList
.
contains
(
'
show
'
)).
toBe
(
true
);
expect
(
vm
.
$el
.
querySelector
(
'
.js-search-results
'
)).
not
.
toBeNull
();
done
();
});
});
it
(
'
does not render when input is not focused
'
,
()
=>
{
vm
.
$store
.
dispatch
(
'
setInputValue
'
,
mockInputValue
);
expect
(
vm
.
$el
.
classList
.
contains
(
'
show
'
)).
toBe
(
false
);
});
it
(
'
does not render when input value is empty
'
,
()
=>
{
vm
.
isInputFocused
=
true
;
expect
(
vm
.
$el
.
classList
.
contains
(
'
show
'
)).
toBe
(
false
);
});
it
(
'
renders search icon
'
,
()
=>
{
const
icons
=
getChildInstances
(
vm
,
IconComponent
);
expect
(
icons
.
length
).
toBe
(
1
);
const
[
searchIcon
]
=
icons
;
expect
(
searchIcon
.
name
).
toBe
(
'
search
'
);
});
it
(
'
renders search description
'
,
()
=>
{
store
.
state
.
inputValue
=
mockInputValue
;
vm
=
mountComponentWithStore
(
ProjectSearchComponent
,
{
store
});
expect
(
vm
.
$el
.
querySelector
(
'
.js-search-results
'
).
innerText
.
trim
()).
toBe
(
`"
${
mockInputValue
}
"
${
mockText
.
SEARCH_DESCRIPTION_SUFFIX
}
`
,
);
});
it
(
'
renders no search results after searching input with no matches
'
,
done
=>
{
vm
.
hasSearchedInput
=
true
;
vm
.
$nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-search-results
'
)
.
innerText
.
trim
()
.
slice
(
-
mockText
.
NO_SEARCH_RESULTS
.
length
),
).
toBe
(
mockText
.
NO_SEARCH_RESULTS
);
done
();
});
});
it
(
'
renders loading icon when searching
'
,
()
=>
{
store
.
state
.
searchCount
=
1
;
vm
=
mount
();
expect
(
getChildInstances
(
vm
,
GlLoadingIconComponent
).
length
).
toBe
(
1
);
});
it
(
'
renders search results
'
,
()
=>
{
store
.
state
.
projectSearchResults
=
mockProjects
;
vm
=
mount
();
expect
(
vm
.
$el
.
getElementsByClassName
(
'
js-search-result
'
).
length
).
toBe
(
mockProjects
.
length
);
});
});
it
(
'
searches projects when input value changes
'
,
done
=>
{
const
spy
=
spyOn
(
vm
,
'
queryInputInProjects
'
);
vm
.
$store
.
dispatch
(
'
setInputValue
'
,
mockInputValue
);
vm
.
$nextTick
(()
=>
{
expect
(
spy
).
toHaveBeenCalled
();
done
();
});
});
describe
(
'
project search item
'
,
()
=>
{
let
item
;
beforeEach
(()
=>
{
store
.
state
.
projectSearchResults
=
mockProjects
;
vm
=
mount
();
item
=
vm
.
$el
.
querySelector
(
'
.js-search-result
'
);
});
it
(
'
renders project name with namespace
'
,
()
=>
{
expect
(
item
.
querySelector
(
'
.js-name-with-namespace
'
).
innerText
.
trim
()).
toBe
(
mockOneProject
.
name_with_namespace
,
);
});
it
(
'
calls action to add project token on mousedown
'
,
done
=>
{
const
spy
=
spyOn
(
vm
.
$store
,
'
dispatch
'
);
mouseEvent
(
item
,
'
mousedown
'
);
vm
.
$nextTick
(()
=>
{
expect
(
spy
).
toHaveBeenCalledWith
(
'
addProjectToken
'
,
mockOneProject
);
done
();
});
});
});
describe
(
'
wrapped components
'
,
()
=>
{
describe
(
'
tokenized input
'
,
()
=>
{
const
getInput
=
parent
=>
getChildInstances
(
parent
,
TokenizedInputComponent
)[
0
];
it
(
'
renders
'
,
()
=>
{
expect
(
getChildInstances
(
vm
,
TokenizedInputComponent
).
length
).
toBe
(
1
);
});
it
(
'
handles focus
'
,
()
=>
{
getInput
(
vm
).
$emit
(
'
focus
'
);
expect
(
vm
.
isInputFocused
).
toBe
(
true
);
});
it
(
'
handles blur
'
,
()
=>
{
getInput
(
vm
).
$emit
(
'
blur
'
);
expect
(
vm
.
isInputFocused
).
toBe
(
false
);
});
});
describe
(
'
project avatar
'
,
()
=>
{
let
avatars
;
beforeEach
(()
=>
{
store
.
state
.
projectSearchResults
=
mockProjects
;
vm
=
mount
();
avatars
=
getChildInstances
(
vm
,
ProjectAvatarComponent
);
});
it
(
'
renders project avatar component
'
,
()
=>
{
expect
(
avatars
.
length
).
toBe
(
1
);
});
it
(
'
binds project to project
'
,
()
=>
{
const
[
avatar
]
=
avatars
;
expect
(
avatar
.
project
).
toEqual
(
mockOneProject
);
});
});
});
});
ee/spec/javascripts/operations/components/dashboard/project_spec.js
0 → 100644
View file @
58dae1a2
import
Vue
from
'
vue
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
Commit
from
'
~/vue_shared/components/commit.vue
'
;
import
Project
from
'
ee/operations/components/dashboard/project.vue
'
;
import
ProjectHeader
from
'
ee/operations/components/dashboard/project_header.vue
'
;
import
Alerts
from
'
ee/operations/components/dashboard/alerts.vue
'
;
import
{
getChildInstances
}
from
'
../../helpers
'
;
import
{
mockOneProject
}
from
'
../../mock_data
'
;
describe
(
'
project component
'
,
()
=>
{
const
ProjectComponent
=
Vue
.
extend
(
Project
);
const
ProjectHeaderComponent
=
Vue
.
extend
(
ProjectHeader
);
const
AlertsComponent
=
Vue
.
extend
(
Alerts
);
const
CommitComponent
=
Vue
.
extend
(
Commit
);
const
IconComponent
=
Vue
.
extend
(
Icon
);
let
vm
;
beforeEach
(()
=>
{
vm
=
mountComponentWithStore
(
ProjectComponent
,
{
props
:
{
project
:
mockOneProject
,
},
});
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'
wrapped components
'
,
()
=>
{
describe
(
'
project header
'
,
()
=>
{
it
(
'
binds project
'
,
()
=>
{
const
[
header
]
=
getChildInstances
(
vm
,
ProjectHeaderComponent
);
expect
(
header
.
project
).
toEqual
(
mockOneProject
);
});
});
describe
(
'
alerts
'
,
()
=>
{
let
alert
;
beforeEach
(()
=>
{
[
alert
]
=
getChildInstances
(
vm
,
AlertsComponent
);
});
it
(
'
binds alert count to count
'
,
()
=>
{
expect
(
alert
.
count
).
toBe
(
mockOneProject
.
alert_count
);
});
it
(
'
binds last alert
'
,
()
=>
{
expect
(
alert
.
lastAlert
).
toEqual
(
mockOneProject
.
last_alert
);
});
});
describe
(
'
commit
'
,
()
=>
{
let
commits
;
let
commit
;
beforeEach
(()
=>
{
commits
=
getChildInstances
(
vm
,
CommitComponent
);
[
commit
]
=
commits
;
});
it
(
'
renders
'
,
()
=>
{
expect
(
commits
.
length
).
toBe
(
1
);
});
it
(
'
binds commitRef
'
,
()
=>
{
expect
(
commit
.
commitRef
).
toBe
(
vm
.
commitRef
);
});
it
(
'
binds short_id to shortSha
'
,
()
=>
{
expect
(
commit
.
shortSha
).
toBe
(
vm
.
project
.
last_deployment
.
commit
.
short_id
);
});
it
(
'
binds web_url to commitUrl
'
,
()
=>
{
expect
(
commit
.
commitUrl
).
toBe
(
vm
.
project
.
last_deployment
.
commit
.
web_url
);
});
it
(
'
binds title
'
,
()
=>
{
expect
(
commit
.
title
).
toBe
(
vm
.
project
.
last_deployment
.
commit
.
title
);
});
it
(
'
binds author
'
,
()
=>
{
expect
(
commit
.
author
).
toBe
(
vm
.
author
);
});
it
(
'
binds tag
'
,
()
=>
{
expect
(
commit
.
tag
).
toBe
(
vm
.
project
.
last_deployment
.
commit
.
tag
);
});
});
describe
(
'
last deploy
'
,
()
=>
{
it
(
'
renders calendar icon
'
,
()
=>
{
const
icons
=
getChildInstances
(
vm
,
IconComponent
);
expect
(
icons
.
length
).
toBe
(
1
);
const
[
icon
]
=
icons
;
expect
(
icon
.
name
).
toBe
(
'
calendar
'
);
});
it
(
'
renders time ago of last deploy
'
,
()
=>
{
const
timeago
=
'
1 day ago
'
;
const
container
=
vm
.
$el
.
querySelector
(
'
.js-project-container
'
);
expect
(
container
.
innerText
.
trim
()).
toBe
(
timeago
);
});
});
});
});
ee/spec/javascripts/operations/components/tokenized_input/input_spec.js
0 → 100644
View file @
58dae1a2
import
Vue
from
'
vue
'
;
import
store
from
'
ee/operations/store/index
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
TokenizedInput
from
'
ee/operations/components/tokenized_input/input.vue
'
;
import
{
getChildInstances
,
clearState
}
from
'
../../helpers
'
;
import
{
mockProjectData
}
from
'
../../mock_data
'
;
describe
(
'
tokenized input component
'
,
()
=>
{
const
TokenizedInputComponent
=
Vue
.
extend
(
TokenizedInput
);
const
IconComponent
=
Vue
.
extend
(
Icon
);
const
mockProjects
=
mockProjectData
(
1
);
const
[
mockOneProject
]
=
mockProjects
;
const
mockInputValue
=
'
mock-inputValue
'
;
let
vm
;
const
getInput
=
()
=>
vm
.
$refs
.
input
;
beforeEach
(()
=>
{
store
.
state
.
projectTokens
=
mockProjects
;
vm
=
mountComponentWithStore
(
TokenizedInputComponent
,
{
store
});
});
afterEach
(()
=>
{
vm
.
$destroy
();
clearState
(
store
);
});
it
(
'
focuses input on click
'
,
()
=>
{
const
spy
=
spyOn
(
getInput
(),
'
focus
'
);
vm
.
$el
.
click
();
expect
(
spy
).
toHaveBeenCalled
();
});
it
(
'
renders input token
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-input-token
'
).
innerText
.
trim
()).
toBe
(
mockOneProject
.
name_with_namespace
,
);
});
it
(
'
removes input tokens on click
'
,
()
=>
{
const
spy
=
spyOn
(
vm
.
$store
,
'
dispatch
'
);
vm
.
$el
.
querySelector
(
'
.js-token-remove
'
).
click
();
expect
(
spy
).
toHaveBeenCalledWith
(
'
removeProjectTokenAt
'
,
mockOneProject
.
id
);
});
describe
(
'
input
'
,
()
=>
{
it
(
'
updates input value when local value changes
'
,
done
=>
{
vm
.
localInputValue
=
mockInputValue
;
vm
.
$nextTick
(()
=>
{
expect
(
getInput
().
value
).
toBe
(
mockInputValue
);
done
();
});
});
it
(
'
handles focus
'
,
()
=>
{
const
spy
=
spyOn
(
vm
,
'
$emit
'
);
vm
.
onFocus
();
expect
(
spy
).
toHaveBeenCalledWith
(
'
focus
'
);
});
it
(
'
handles blur
'
,
()
=>
{
const
spy
=
spyOn
(
vm
,
'
$emit
'
);
vm
.
onBlur
();
expect
(
spy
).
toHaveBeenCalledWith
(
'
blur
'
);
});
});
describe
(
'
wrapped components
'
,
()
=>
{
describe
(
'
icon
'
,
()
=>
{
it
(
'
should render close for input tokens
'
,
()
=>
{
expect
(
getChildInstances
(
vm
,
IconComponent
).
filter
(
icon
=>
icon
.
name
===
'
close
'
).
length
,
).
toBe
(
mockProjects
.
length
);
});
it
(
'
should render search
'
,
()
=>
{
const
search
=
getChildInstances
(
vm
,
IconComponent
)[
1
];
expect
(
search
.
name
).
toBe
(
'
search
'
);
});
});
});
});
ee/spec/javascripts/operations/helpers.js
0 → 100644
View file @
58dae1a2
import
state
from
'
ee/operations/store/state
'
;
export
function
clearState
(
store
)
{
store
.
replaceState
(
state
());
}
export
function
getChildInstances
(
vm
,
WrappedComponent
)
{
return
vm
.
$children
.
filter
(
child
=>
child
instanceof
WrappedComponent
);
}
export
function
mouseEvent
(
el
,
eventType
)
{
const
event
=
document
.
createEvent
(
'
MouseEvent
'
);
event
.
initMouseEvent
(
eventType
);
el
.
dispatchEvent
(
event
);
}
ee/spec/javascripts/operations/mock_data.js
0 → 100644
View file @
58dae1a2
export
const
mockText
=
{
ADD_PROJECTS
:
'
Add projects
'
,
ADD_PROJECTS_ERROR
:
'
Something went wrong, unable to add projects to dashboard
'
,
ADD_PROJECTS_DUPLICATE_ERROR
:
'
Some projects could not be added to dashboard
'
,
REMOVE_PROJECT_ERROR
:
'
Something went wrong, unable to remove project
'
,
DASHBOARD_TITLE
:
'
Operations Dashboard
'
,
EMPTY_TITLE
:
'
Add a project to the dashboard
'
,
EMPTY_SUBTITLE
:
"
The operations dashboard provides a summary of each project's operational health, including pipeline and alert status.
"
,
EMPTY_SVG_SOURCE
:
'
/assets/illustrations/operations-dashboard_empty.svg
'
,
NO_SEARCH_RESULTS
:
'
Sorry, no projects matched your search
'
,
RECEIVE_PROJECTS_ERROR
:
'
Something went wrong, unable to get operations projects
'
,
REMOVE_PROJECT
:
'
Remove
'
,
SEARCH_PROJECTS
:
'
Search your projects
'
,
SEARCH_DESCRIPTION_SUFFIX
:
'
in projects
'
,
};
export
function
mockProjectData
(
projectCount
=
1
,
deployTimeStamp
=
`
${
new
Date
(
Date
.
now
()
-
86400000
).
getTime
()}
`
,
alertCount
=
1
,
isTag
=
false
,
)
{
return
Array
(
projectCount
)
.
fill
(
null
)
.
map
((
_
,
index
)
=>
({
id
:
index
,
name
:
'
mock-name
'
,
name_with_namespace
:
'
mock-namespace / mock-name
'
,
path
:
'
mock-path
'
,
path_with_namespace
:
'
mock-path_with-namespace
'
,
avatar_url
:
null
,
last_deployment
:
{
created_at
:
deployTimeStamp
,
commit
:
{
short_id
:
'
mock-short_id
'
,
tag
:
isTag
,
title
:
'
mock-title
'
,
web_url
:
'
https://mock-web_url/
'
,
},
user
:
{
avatar_url
:
null
,
path
:
'
mock-path
'
,
username
:
'
mock-username
'
,
web_url
:
'
https://mock-web_url/
'
,
},
ref
:
{
name
:
'
mock-name
'
,
ref_path
:
'
mock-ref_path
'
,
web_url
:
'
https://mock-web_url/
'
,
},
},
alert_count
:
alertCount
,
alert_path
:
'
mock-alert_path
'
,
last_alert
:
{
id
:
index
,
title
:
'
mock-title
'
,
threshold
:
2
,
operator
:
'
mock-operator
'
,
alert_path
:
'
mock-alert_path
'
,
},
remove_path
:
'
mock-remove_path
'
,
web_url
:
'
https://mock-web_url/
'
,
}));
}
export
const
[
mockOneProject
]
=
mockProjectData
(
1
);
ee/spec/javascripts/operations/store/actions_spec.js
0 → 100644
View file @
58dae1a2
This diff is collapsed.
Click to expand it.
ee/spec/javascripts/operations/store/mutations_spec.js
0 → 100644
View file @
58dae1a2
import
state
from
'
ee/operations/store/state
'
;
import
mutations
from
'
ee/operations/store/mutations
'
;
import
*
as
types
from
'
ee/operations/store/mutation_types
'
;
import
{
mockProjectData
}
from
'
../mock_data
'
;
describe
(
'
mutations
'
,
()
=>
{
const
projects
=
mockProjectData
(
1
);
const
[
oneProject
]
=
projects
;
const
mockEndpoint
=
'
https://mock-endpoint
'
;
const
mockSearches
=
new
Array
(
5
).
fill
(
null
);
let
localState
;
beforeEach
(()
=>
{
localState
=
state
();
});
describe
(
'
ADD_PROJECT_TOKEN
'
,
()
=>
{
it
(
'
adds project token to projectTokens
'
,
()
=>
{
mutations
[
types
.
ADD_PROJECT_TOKEN
](
localState
,
oneProject
);
expect
(
localState
.
projectTokens
[
0
]).
toEqual
(
oneProject
);
});
});
describe
(
'
INCREMENT_PROJECT_SEARCH_COUNT
'
,
()
=>
{
it
(
'
adds search to searchCount
'
,
()
=>
{
mockSearches
.
forEach
(()
=>
{
mutations
[
types
.
INCREMENT_PROJECT_SEARCH_COUNT
](
localState
,
1
);
});
expect
(
localState
.
searchCount
).
toBe
(
mockSearches
.
length
);
});
});
describe
(
'
DECREMENT_PROJECT_SEARCH_COUNT
'
,
()
=>
{
it
(
'
removes search from searchCount
'
,
()
=>
{
localState
.
searchCount
=
mockSearches
.
length
+
2
;
mockSearches
.
forEach
(()
=>
{
mutations
[
types
.
DECREMENT_PROJECT_SEARCH_COUNT
](
localState
,
1
);
});
expect
(
localState
.
searchCount
).
toBe
(
2
);
});
});
describe
(
'
SET_PROJECT_ENDPOINT_LIST
'
,
()
=>
{
it
(
'
sets project list endpoint
'
,
()
=>
{
mutations
[
types
.
SET_PROJECT_ENDPOINT_LIST
](
localState
,
mockEndpoint
);
expect
(
localState
.
projectEndpoints
.
list
).
toBe
(
mockEndpoint
);
});
});
describe
(
'
SET_PROJECT_ENDPOINT_ADD
'
,
()
=>
{
it
(
'
sets project add endpoint
'
,
()
=>
{
mutations
[
types
.
SET_PROJECT_ENDPOINT_ADD
](
localState
,
mockEndpoint
);
expect
(
localState
.
projectEndpoints
.
add
).
toBe
(
mockEndpoint
);
});
});
describe
(
'
SET_PROJECT_SEARCH_RESULTS
'
,
()
=>
{
it
(
'
sets project search results
'
,
()
=>
{
mutations
[
types
.
SET_PROJECT_SEARCH_RESULTS
](
localState
,
projects
);
expect
(
localState
.
projectSearchResults
).
toEqual
(
projects
);
});
});
describe
(
'
SET_PROJECTS
'
,
()
=>
{
it
(
'
sets projects
'
,
()
=>
{
mutations
[
types
.
SET_PROJECTS
](
localState
,
projects
);
expect
(
localState
.
projects
).
toEqual
(
projects
);
});
});
describe
(
'
REMOVE_PROJECT_TOKEN_AT
'
,
()
=>
{
it
(
'
removes project token
'
,
()
=>
{
localState
.
projectTokens
=
projects
;
mutations
[
types
.
REMOVE_PROJECT_TOKEN_AT
](
localState
,
oneProject
.
id
);
expect
(
localState
.
projectTokens
.
length
).
toBe
(
0
);
});
});
});
ee/spec/policies/global_policy_spec.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
require
'spec_helper'
describe
GlobalPolicy
do
include
ExternalAuthorizationServiceHelpers
let
(
:current_user
)
{
create
(
:user
)
}
let
(
:user
)
{
create
(
:user
)
}
subject
{
described_class
.
new
(
current_user
,
[
user
])
}
describe
'reading operations dashboard'
do
before
do
stub_licensed_features
(
operations_dashboard:
true
)
end
it
{
is_expected
.
to
be_allowed
(
:read_operations_dashboard
)
}
context
'when unlicensed'
do
before
do
stub_licensed_features
(
operations_dashboard:
false
)
end
it
{
is_expected
.
not_to
be_allowed
(
:read_operations_dashboard
)
}
end
end
end
ee/spec/services/dashboard/operations/list_service_spec.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
require
'spec_helper'
describe
Dashboard
::
Operations
::
ListService
do
let
(
:subject
)
{
described_class
.
new
(
user
).
execute
}
let
(
:dashboard_project
)
{
subject
.
first
}
let!
(
:project
)
{
create
(
:project
,
:repository
)
}
let!
(
:user
)
{
create
(
:user
)
}
describe
'#execute'
do
shared_examples
'no projects'
do
it
'returns an empty list'
do
expect
(
subject
).
to
be_empty
end
it
'ensures only a single query'
do
queries
=
ActiveRecord
::
QueryRecorder
.
new
{
subject
}.
count
expect
(
queries
).
to
eq
(
1
)
end
end
shared_examples
'no deployment information'
do
it
'has no information'
do
expect
(
dashboard_project
.
last_deployment
).
to
be_nil
expect
(
dashboard_project
.
alert_count
).
to
eq
(
0
)
expect
(
dashboard_project
.
last_alert
).
to
be_nil
end
end
shared_examples
'avoiding N+1 queries'
do
it
'ensures a fixed amount of queries'
do
queries
=
ActiveRecord
::
QueryRecorder
.
new
{
subject
}.
count
expect
(
queries
).
to
eq
(
7
)
end
end
context
'with added projects'
do
let
(
:production
)
{
create
(
:environment
,
project:
project
,
name:
'production'
)
}
let
(
:staging
)
{
create
(
:environment
,
project:
project
,
name:
'staging'
)
}
let
(
:production_deployment
)
do
create
(
:deployment
,
project:
project
,
environment:
production
,
ref:
'master'
)
end
let
(
:staging_deployment
)
do
create
(
:deployment
,
project:
project
,
environment:
staging
,
ref:
'wip'
)
end
before
do
user
.
ops_dashboard_projects
<<
project
project
.
add_developer
(
user
)
end
it
'returns a list of projects'
do
expect
(
subject
.
size
).
to
eq
(
1
)
end
it
'has some project information'
do
expect
(
dashboard_project
.
project
).
to
eq
(
project
)
end
it_behaves_like
'no deployment information'
context
'with `production` deployment'
do
before
do
staging_deployment
production_deployment
end
it
'provides information about the `production` deployment'
do
last_deployment
=
dashboard_project
.
last_deployment
expect
(
last_deployment
.
ref
).
to
eq
(
production_deployment
.
ref
)
end
context
'with alerts'
do
let
(
:alert_prd1
)
{
create
(
:prometheus_alert
,
project:
project
,
environment:
production
)
}
let
(
:alert_prd2
)
{
create
(
:prometheus_alert
,
project:
project
,
environment:
production
)
}
let
(
:alert_stg
)
{
create
(
:prometheus_alert
,
project:
project
,
environment:
staging
)
}
let!
(
:alert_events
)
do
[
create
(
:prometheus_alert_event
,
prometheus_alert:
alert_prd1
),
create
(
:prometheus_alert_event
,
prometheus_alert:
alert_prd2
),
last_firing_event
,
create
(
:prometheus_alert_event
,
prometheus_alert:
alert_stg
),
create
(
:prometheus_alert_event
,
:resolved
,
prometheus_alert:
alert_prd2
)
]
end
let
(
:last_firing_event
)
{
create
(
:prometheus_alert_event
,
prometheus_alert:
alert_prd1
)
}
it_behaves_like
'avoiding N+1 queries'
it
'provides information about alerts'
do
expect
(
dashboard_project
.
alert_count
).
to
eq
(
3
)
expect
(
dashboard_project
.
last_alert
).
to
eq
(
last_firing_event
.
prometheus_alert
)
end
context
'with more projects'
do
before
do
project2
=
create
(
:project
)
production2
=
create
(
:environment
,
name:
'production'
,
project:
project2
)
alert2_prd
=
create
(
:prometheus_alert
,
project:
project2
,
environment:
production2
)
create
(
:prometheus_alert_event
,
prometheus_alert:
alert2_prd
)
project2
.
add_developer
(
user
)
user
.
ops_dashboard_projects
<<
project2
end
it_behaves_like
'avoiding N+1 queries'
end
end
describe
'checking plans'
do
using
RSpec
::
Parameterized
::
TableSyntax
where
(
:check_namespace_plan
,
:plan
,
:available
)
do
true
|
:gold_plan
|
true
true
|
:silver_plan
|
false
true
|
nil
|
false
false
|
:gold_plan
|
true
false
|
:silver_plan
|
true
false
|
nil
|
true
end
with_them
do
before
do
stub_application_setting
(
check_namespace_plan:
check_namespace_plan
)
project
.
namespace
.
update!
(
plan:
create
(
plan
))
if
plan
end
if
params
[
:available
]
it
'returns this project'
do
expect
(
subject
.
size
).
to
eq
(
1
)
expect
(
dashboard_project
.
project
).
to
eq
(
project
)
end
else
it
'does not return this project'
do
expect
(
subject
).
to
be_empty
end
end
end
end
end
context
'without any `production` deployments'
do
before
do
staging_deployment
end
it_behaves_like
'no deployment information'
end
context
'without deployments'
do
it_behaves_like
'no deployment information'
end
context
'without sufficient access level'
do
before
do
project
.
add_reporter
(
user
)
end
it_behaves_like
'no projects'
end
end
context
'without added projects'
do
it_behaves_like
'no projects'
end
end
end
ee/spec/services/dashboard/operations/projects_service_spec.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
require
'spec_helper'
describe
Dashboard
::
Operations
::
ProjectsService
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:project
)
{
create
(
:project
)
}
let
(
:service
)
{
described_class
.
new
(
user
)
}
describe
'#execute'
do
before
do
project
.
add_developer
(
user
)
end
it
'returns the project when passing a project id'
do
projects
=
service
.
execute
([
project
.
id
])
expect
(
projects
).
to
contain_exactly
(
project
)
end
it
'returns the project when passing a project record'
do
projects
=
service
.
execute
([
project
])
expect
(
projects
).
to
contain_exactly
(
project
)
end
describe
'with plans'
do
let!
(
:gold_project
)
{
create
(
:project
,
namespace:
create
(
:namespace
,
plan: :gold_plan
))
}
let!
(
:silver_project
)
{
create
(
:project
,
namespace:
create
(
:namespace
,
plan: :silver_plan
))
}
let!
(
:no_plan_project
)
{
create
(
:project
,
namespace:
create
(
:namespace
))
}
let
(
:projects
)
{
service
.
execute
([
gold_project
,
silver_project
,
no_plan_project
])
}
before
do
gold_project
.
add_developer
(
user
)
silver_project
.
add_developer
(
user
)
no_plan_project
.
add_developer
(
user
)
end
context
'when namespace plan check is enabled'
do
before
do
stub_application_setting
(
check_namespace_plan:
true
)
end
it
'returns the gold project'
do
expect
(
projects
).
to
contain_exactly
(
gold_project
)
end
end
context
'when namespace plan check is disabled'
do
before
do
stub_application_setting
(
check_namespace_plan:
false
)
end
it
'returns all projects'
do
expect
(
projects
).
to
contain_exactly
(
gold_project
,
silver_project
,
no_plan_project
)
end
end
end
context
'with insufficient access'
do
before
do
project
.
add_reporter
(
user
)
end
it
'returns an empty list'
do
projects
=
service
.
execute
([
project
.
id
])
expect
(
projects
).
to
be_empty
end
end
it
'does not find by invalid project id'
do
projects
=
service
.
execute
([
-
1
])
expect
(
projects
).
to
be_empty
end
end
end
ee/spec/services/users_ops_dashboard_projects/create_service_spec.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
require
'spec_helper'
describe
UsersOpsDashboardProjects
::
CreateService
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:service
)
{
described_class
.
new
(
user
)
}
let
(
:project
)
{
create
(
:project
,
:private
)
}
describe
'#execute'
do
context
'with at least developer access level'
do
before
do
project
.
add_developer
(
user
)
end
it
'adds a project'
do
result
=
service
.
execute
([
project
.
id
])
expect
(
result
).
to
eq
(
expected_result
(
added_project_ids:
[
project
.
id
]))
end
it
'adds a project with a string id'
do
result
=
service
.
execute
([
project
.
id
.
to_s
])
expect
(
result
).
to
eq
(
expected_result
(
added_project_ids:
[
project
.
id
]))
end
it
'adds a project only once'
do
result
=
service
.
execute
([
project
.
id
,
project
.
id
])
expect
(
result
).
to
eq
(
expected_result
(
added_project_ids:
[
project
.
id
]))
end
context
'with already added project'
do
before
do
user
.
ops_dashboard_projects
<<
project
end
it
'does not add duplicates'
do
result
=
service
.
execute
([
project
.
id
])
expect
(
result
).
to
eq
(
expected_result
(
duplicate_project_ids:
[
project
.
id
]))
end
end
context
'checking plans'
do
using
RSpec
::
Parameterized
::
TableSyntax
where
(
:check_namespace_plan
,
:plan
,
:can_add
)
do
true
|
:gold_plan
|
true
true
|
:silver_plan
|
false
true
|
nil
|
false
false
|
:gold_plan
|
true
false
|
:silver_plan
|
true
false
|
nil
|
true
end
with_them
do
before
do
stub_application_setting
(
check_namespace_plan:
check_namespace_plan
)
project
.
namespace
.
update!
(
plan:
create
(
plan
))
if
plan
end
subject
{
service
.
execute
([
project
.
id
])
}
if
params
[
:can_add
]
it
'adds a project'
do
expect
(
subject
).
to
eq
(
expected_result
(
added_project_ids:
[
project
.
id
]))
end
else
it
'is not allowed to add a project'
do
expect
(
subject
).
to
eq
(
expected_result
(
invalid_project_ids:
[
project
.
id
]))
end
end
end
end
end
context
'with access level lower than developer'
do
before
do
project
.
add_reporter
(
user
)
end
it
'does not add a project'
do
result
=
service
.
execute
([
project
.
id
])
expect
(
result
).
to
eq
(
expected_result
(
invalid_project_ids:
[
project
.
id
]))
end
end
context
'with invalid project ids'
do
let
(
:invalid_ids
)
{
[
nil
,
-
1
,
'-1'
,
:symbol
]
}
it
'does not add invalid project ids'
do
result
=
service
.
execute
(
invalid_ids
)
expect
(
result
).
to
eq
(
expected_result
(
invalid_project_ids:
invalid_ids
))
end
end
end
private
def
expected_result
(
added_project_ids:
[],
invalid_project_ids:
[],
duplicate_project_ids:
[]
)
UsersOpsDashboardProjects
::
CreateService
::
Result
.
new
(
added_project_ids
,
invalid_project_ids
,
duplicate_project_ids
)
end
end
ee/spec/services/users_ops_dashboard_projects/destroy_service_spec.rb
0 → 100644
View file @
58dae1a2
# frozen_string_literal: true
require
'spec_helper'
describe
UsersOpsDashboardProjects
::
DestroyService
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:service
)
{
described_class
.
new
(
user
)
}
let
(
:project
)
{
create
(
:project
,
:private
)
}
describe
'#execute'
do
context
'with an added project'
do
before
do
user
.
ops_dashboard_projects
<<
project
end
it
'removes the project'
do
expect
{
service
.
execute
(
project
.
id
)
}.
to
change
{
UsersOpsDashboardProject
.
count
}.
to
(
0
)
end
it
'returns the removed project'
do
removed
=
service
.
execute
(
project
.
id
)
expect
(
removed
).
to
eq
(
project
)
end
end
context
'without projects added'
do
it
'does not remove the project'
do
expect
{
service
.
execute
(
project
.
id
)
}.
not_to
change
{
UsersOpsDashboardProject
.
count
}
end
it
'returns nil'
do
expect
(
service
.
execute
(
project
.
id
)).
to
be_nil
end
end
end
end
locale/gitlab.pot
View file @
58dae1a2
...
@@ -32,6 +32,9 @@ msgid_plural " improved on %d points"
...
@@ -32,6 +32,9 @@ msgid_plural " improved on %d points"
msgstr[0] ""
msgstr[0] ""
msgstr[1] ""
msgstr[1] ""
msgid "\"%{query}\" in projects"
msgstr ""
msgid "%d addition"
msgid "%d addition"
msgid_plural "%d additions"
msgid_plural "%d additions"
msgstr[0] ""
msgstr[0] ""
...
@@ -116,6 +119,9 @@ msgstr ""
...
@@ -116,6 +119,9 @@ msgstr ""
msgid "%{counter_storage} (%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS)"
msgid "%{counter_storage} (%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS)"
msgstr ""
msgstr ""
msgid "%{count} %{alerts}"
msgstr ""
msgid "%{count} participant"
msgid "%{count} participant"
msgid_plural "%{count} participants"
msgid_plural "%{count} participants"
msgstr[0] ""
msgstr[0] ""
...
@@ -406,6 +412,9 @@ msgstr ""
...
@@ -406,6 +412,9 @@ msgstr ""
msgid "Add new directory"
msgid "Add new directory"
msgstr ""
msgstr ""
msgid "Add projects"
msgstr ""
msgid "Add reaction"
msgid "Add reaction"
msgstr ""
msgstr ""
...
@@ -499,6 +508,11 @@ msgstr ""
...
@@ -499,6 +508,11 @@ msgstr ""
msgid "Advanced settings"
msgid "Advanced settings"
msgstr ""
msgstr ""
msgid "Alert"
msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
msgid "All"
msgid "All"
msgstr ""
msgstr ""
...
@@ -5562,6 +5576,15 @@ msgstr ""
...
@@ -5562,6 +5576,15 @@ msgstr ""
msgid "Operations Dashboard"
msgid "Operations Dashboard"
msgstr ""
msgstr ""
msgid "OperationsDashboard|Add a project to the dashboard"
msgstr ""
msgid "OperationsDashboard|Some projects could not be added to dashboard"
msgstr ""
msgid "OperationsDashboard|The operations dashboard provides a summary of each project's operational health, including pipeline and alert status."
msgstr ""
msgid "Optionally, you can %{link_to_customize} how FogBugz email addresses and usernames are imported into GitLab."
msgid "Optionally, you can %{link_to_customize} how FogBugz email addresses and usernames are imported into GitLab."
msgstr ""
msgstr ""
...
@@ -6952,6 +6975,9 @@ msgstr ""
...
@@ -6952,6 +6975,9 @@ msgstr ""
msgid "Search users"
msgid "Search users"
msgstr ""
msgstr ""
msgid "Search your projects"
msgstr ""
msgid "SearchAutocomplete|All GitLab"
msgid "SearchAutocomplete|All GitLab"
msgstr ""
msgstr ""
...
@@ -7305,12 +7331,24 @@ msgstr ""
...
@@ -7305,12 +7331,24 @@ msgstr ""
msgid "Something went wrong while resolving this discussion. Please try again."
msgid "Something went wrong while resolving this discussion. Please try again."
msgstr ""
msgstr ""
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
msgid "Something went wrong, unable to get operations projects"
msgstr ""
msgid "Something went wrong, unable to remove project"
msgstr ""
msgid "Something went wrong. Please try again."
msgid "Something went wrong. Please try again."
msgstr ""
msgstr ""
msgid "Sorry, no epics matched your search"
msgid "Sorry, no epics matched your search"
msgstr ""
msgstr ""
msgid "Sorry, no projects matched your search"
msgstr ""
msgid "Sort by"
msgid "Sort by"
msgstr ""
msgstr ""
...
@@ -8592,6 +8630,9 @@ msgstr ""
...
@@ -8592,6 +8630,9 @@ msgstr ""
msgid "Version"
msgid "Version"
msgstr ""
msgstr ""
msgid "View %{alerts}"
msgstr ""
msgid "View app"
msgid "View app"
msgstr ""
msgstr ""
...
@@ -9624,6 +9665,11 @@ msgstr ""
...
@@ -9624,6 +9665,11 @@ msgstr ""
msgid "private key does not match certificate."
msgid "private key does not match certificate."
msgstr ""
msgstr ""
msgid "project"
msgid_plural "projects"
msgstr[0] ""
msgstr[1] ""
msgid "remaining"
msgid "remaining"
msgstr ""
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