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
2f848944
Commit
2f848944
authored
Aug 16, 2021
by
Justin Ho Tuan Duong
Committed by
Bob Van Landuyt
Aug 16, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add frontend for instance-level integration overrides
parent
100a2dd9
Changes
11
Show whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
326 additions
and
22 deletions
+326
-22
app/assets/javascripts/integrations/overrides/api.js
app/assets/javascripts/integrations/overrides/api.js
+10
-0
app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
...tegrations/overrides/components/integration_overrides.vue
+113
-1
app/controllers/admin/integrations_controller.rb
app/controllers/admin/integrations_controller.rb
+0
-4
app/helpers/integrations_helper.rb
app/helpers/integrations_helper.rb
+11
-0
app/views/shared/integrations/_form.html.haml
app/views/shared/integrations/_form.html.haml
+0
-3
app/views/shared/integrations/_tabs.html.haml
app/views/shared/integrations/_tabs.html.haml
+16
-12
app/views/shared/integrations/edit.html.haml
app/views/shared/integrations/edit.html.haml
+5
-1
app/views/shared/integrations/overrides.html.haml
app/views/shared/integrations/overrides.html.haml
+1
-1
locale/gitlab.pot
locale/gitlab.pot
+6
-0
spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
...egrations/user_activates_mattermost_slash_command_spec.rb
+18
-0
spec/frontend/integrations/overrides/components/integration_overrides_spec.js
...ations/overrides/components/integration_overrides_spec.js
+146
-0
No files found.
app/assets/javascripts/integrations/overrides/api.js
0 → 100644
View file @
2f848944
import
axios
from
'
~/lib/utils/axios_utils
'
;
export
const
fetchOverrides
=
(
overridesPath
,
{
page
,
perPage
})
=>
{
return
axios
.
get
(
overridesPath
,
{
params
:
{
page
,
per_page
:
perPage
,
},
});
};
app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
View file @
2f848944
<
script
>
import
{
GlLink
,
GlLoadingIcon
,
GlPagination
,
GlTable
}
from
'
@gitlab/ui
'
;
import
{
DEFAULT_PER_PAGE
}
from
'
~/api
'
;
import
createFlash
from
'
~/flash
'
;
import
{
fetchOverrides
}
from
'
~/integrations/overrides/api
'
;
import
{
parseIntPagination
,
normalizeHeaders
}
from
'
~/lib/utils/common_utils
'
;
import
{
truncateNamespace
}
from
'
~/lib/utils/text_utility
'
;
import
{
__
,
s__
}
from
'
~/locale
'
;
import
ProjectAvatar
from
'
~/vue_shared/components/project_avatar.vue
'
;
export
default
{
name
:
'
IntegrationOverrides
'
,
components
:
{
GlLink
,
GlLoadingIcon
,
GlPagination
,
GlTable
,
ProjectAvatar
,
},
props
:
{
overridesPath
:
{
type
:
String
,
required
:
true
,
},
},
fields
:
[
{
key
:
'
name
'
,
label
:
__
(
'
Project
'
),
},
],
data
()
{
return
{
isLoading
:
true
,
overrides
:
[],
page
:
1
,
totalItems
:
0
,
};
},
computed
:
{
showPagination
()
{
return
this
.
totalItems
>
this
.
$options
.
DEFAULT_PER_PAGE
&&
this
.
overrides
.
length
>
0
;
},
},
mounted
()
{
this
.
loadOverrides
();
},
methods
:
{
loadOverrides
(
page
=
this
.
page
)
{
this
.
isLoading
=
true
;
fetchOverrides
(
this
.
overridesPath
,
{
page
,
perPage
:
this
.
$options
.
DEFAULT_PER_PAGE
,
})
.
then
(({
data
,
headers
})
=>
{
const
{
page
:
newPage
,
total
}
=
parseIntPagination
(
normalizeHeaders
(
headers
));
this
.
page
=
newPage
;
this
.
totalItems
=
total
;
this
.
overrides
=
data
;
})
.
catch
((
error
)
=>
{
createFlash
({
message
:
this
.
$options
.
i18n
.
defaultErrorMessage
,
error
,
captureError
:
true
,
});
})
.
finally
(()
=>
{
this
.
isLoading
=
false
;
});
},
truncateNamespace
,
},
DEFAULT_PER_PAGE
,
i18n
:
{
defaultErrorMessage
:
s__
(
'
Integrations|An error occurred while loading projects using custom settings.
'
,
),
tableEmptyText
:
s__
(
'
Integrations|There are no projects using custom settings
'
),
},
};
</
script
>
<
template
>
<div></div>
<div>
<gl-table
:items=
"overrides"
:fields=
"$options.fields"
:busy=
"isLoading"
show-empty
:empty-text=
"$options.i18n.tableEmptyText"
>
<template
#cell(name)=
"
{ item }">
<gl-link
class=
"gl-display-inline-flex gl-align-items-center gl-hover-text-decoration-none gl-text-body!"
:href=
"item.full_path"
>
<project-avatar
class=
"gl-mr-3"
:project-avatar-url=
"item.avatar_url"
:project-name=
"item.name"
aria-hidden=
"true"
/>
{{
truncateNamespace
(
item
.
full_name
)
}}
/
<strong>
{{
item
.
name
}}
</strong>
</gl-link>
</
template
>
<
template
#table-busy
>
<gl-loading-icon
size=
"md"
class=
"gl-my-2"
/>
</
template
>
</gl-table>
<div
class=
"gl-display-flex gl-justify-content-center gl-mt-5"
>
<gl-pagination
v-if=
"showPagination"
:per-page=
"$options.DEFAULT_PER_PAGE"
:total-items=
"totalItems"
:value=
"page"
:disabled=
"isLoading"
@
input=
"loadOverrides"
/>
</div>
</div>
</template>
app/controllers/admin/integrations_controller.rb
View file @
2f848944
...
...
@@ -26,8 +26,4 @@ class Admin::IntegrationsController < Admin::ApplicationController
def
find_or_initialize_non_project_specific_integration
(
name
)
Integration
.
find_or_initialize_non_project_specific_integration
(
name
,
instance:
true
)
end
def
instance_level_integration_overrides?
Feature
.
enabled?
(
:instance_level_integration_overrides
,
default_enabled: :yaml
)
end
end
app/helpers/integrations_helper.rb
View file @
2f848944
...
...
@@ -125,6 +125,17 @@ module IntegrationsHelper
!
Gitlab
.
com?
end
def
integration_tabs
(
integration
:)
[
{
key:
'edit'
,
text:
_
(
'Settings'
),
href:
scoped_edit_integration_path
(
integration
)
},
({
key:
'overrides'
,
text:
s_
(
'Integrations|Projects using custom settings'
),
href:
scoped_overrides_integration_path
(
integration
)
}
if
instance_level_integration_overrides?
)
].
compact
end
def
instance_level_integration_overrides?
Feature
.
enabled?
(
:instance_level_integration_overrides
,
default_enabled: :yaml
)
end
def
jira_issue_breadcrumb_link
(
issue_reference
)
link_to
''
,
{
class:
'gl-display-flex gl-align-items-center gl-white-space-nowrap'
}
do
icon
=
image_tag
image_path
(
'illustrations/logos/jira.svg'
),
width:
15
,
height:
15
,
class:
'gl-mr-2'
...
...
app/views/shared/integrations/_form.html.haml
View file @
2f848944
-
integration
=
local_assigns
.
fetch
(
:integration
)
%h3
.page-title
=
integration
.
title
=
form_for
integration
,
as: :service
,
url:
scoped_integration_path
(
integration
),
method: :put
,
html:
{
class:
'gl-show-field-errors integration-settings-form js-integration-settings-form'
,
data:
{
'test-url'
=>
scoped_test_integration_path
(
integration
)
}
}
do
|
form
|
=
render
'shared/service_settings'
,
form:
form
,
integration:
integration
app/views/shared/integrations/_tabs.html.haml
View file @
2f848944
.tabs.gl-tabs
-
active_tab
=
local_assigns
.
fetch
(
:active_tab
,
'edit'
)
-
active_classes
=
'gl-tab-nav-item-active gl-tab-nav-item-active-indigo active'
-
tabs
=
integration_tabs
(
integration:
integration
)
-
if
tabs
.
length
<=
1
=
yield
-
else
.tabs.gl-tabs
%div
%ul
.nav.gl-tabs-nav
{
role:
'tablist'
}
-
tabs
.
each
do
|
tab
|
%li
.nav-item
{
role:
'presentation'
}
%a
.nav-link.gl-tab-nav-item
{
role:
'tab'
,
href:
scoped_edit_integration_path
(
integration
)
}
=
_
(
'Settings'
)
%li
.nav-item
{
role:
'presentation'
}
%a
.nav-link.gl-tab-nav-item.gl-tab-nav-item-active.gl-tab-nav-item-active-indigo.active
{
role:
'tab'
,
href:
scoped_overrides_integration_path
(
integration
)
}
=
s_
(
'Integrations|Projects using custom settings'
)
%a
.nav-link.gl-tab-nav-item
{
role:
'tab'
,
class:
(
active_classes
if
tab
[
:key
]
==
active_tab
),
href:
tab
[
:href
]
}
=
tab
[
:text
]
.tab-content.gl-tab-content
.tab-pane
.active
{
role:
'tabpanel'
}
.tab-pane.gl-pt-3
.active
{
role:
'tabpanel'
}
=
yield
app/views/shared/integrations/edit.html.haml
View file @
2f848944
...
...
@@ -3,4 +3,8 @@
-
page_title
@integration
.
title
,
_
(
'Integrations'
)
-
@content_class
=
'limit-container-width'
unless
fluid_layout
=
render
'shared/integrations/form'
,
integration:
@integration
%h3
.page-title
=
@integration
.
title
=
render
'shared/integrations/tabs'
,
integration:
@integration
,
active_tab:
'edit'
do
=
render
'shared/integrations/form'
,
integration:
@integration
app/views/shared/integrations/overrides.html.haml
View file @
2f848944
...
...
@@ -6,5 +6,5 @@
%h3
.page-title
=
@integration
.
title
=
render
'shared/integrations/tabs'
,
integration:
@integration
do
=
render
'shared/integrations/tabs'
,
integration:
@integration
,
active_tab:
'overrides'
do
.js-vue-integration-overrides
{
data:
integration_overrides_data
(
@integration
)
}
locale/gitlab.pot
View file @
2f848944
...
...
@@ -17912,6 +17912,9 @@ msgstr ""
msgid "Integrations|All projects inheriting these settings will also be reset."
msgstr ""
msgid "Integrations|An error occurred while loading projects using custom settings."
msgstr ""
msgid "Integrations|Browser limitations"
msgstr ""
...
...
@@ -18032,6 +18035,9 @@ msgstr ""
msgid "Integrations|Standard"
msgstr ""
msgid "Integrations|There are no projects using custom settings"
msgstr ""
msgid "Integrations|This integration, and inheriting projects were reset."
msgstr ""
...
...
spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
View file @
2f848944
...
...
@@ -11,6 +11,24 @@ RSpec.describe 'User activates the instance-level Mattermost Slash Command integ
end
let
(
:edit_path
)
{
edit_admin_application_settings_integration_path
(
:mattermost_slash_commands
)
}
let
(
:overrides_path
)
{
overrides_admin_application_settings_integration_path
(
:mattermost_slash_commands
)
}
include_examples
'user activates the Mattermost Slash Command integration'
it
'displays navigation tabs'
do
expect
(
page
).
to
have_link
(
'Settings'
,
href:
edit_path
)
expect
(
page
).
to
have_link
(
'Projects using custom settings'
,
href:
overrides_path
)
end
context
'when instance_level_integration_overrides is disabled'
do
before
do
stub_feature_flags
(
instance_level_integration_overrides:
false
)
visit_instance_integration
(
'Mattermost slash commands'
)
end
it
'does not display the overrides tab'
do
expect
(
page
).
not_to
have_link
(
'Settings'
,
href:
edit_path
)
expect
(
page
).
not_to
have_link
(
'Projects using custom settings'
,
href:
overrides_path
)
end
end
end
spec/frontend/integrations/overrides/components/integration_overrides_spec.js
0 → 100644
View file @
2f848944
import
{
GlTable
,
GlLink
,
GlPagination
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
,
mount
}
from
'
@vue/test-utils
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
{
DEFAULT_PER_PAGE
}
from
'
~/api
'
;
import
createFlash
from
'
~/flash
'
;
import
IntegrationOverrides
from
'
~/integrations/overrides/components/integration_overrides.vue
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
httpStatus
from
'
~/lib/utils/http_status
'
;
import
ProjectAvatar
from
'
~/vue_shared/components/project_avatar.vue
'
;
jest
.
mock
(
'
~/flash
'
);
const
mockOverrides
=
Array
(
DEFAULT_PER_PAGE
*
3
)
.
fill
(
1
)
.
map
((
_
,
index
)
=>
({
name
:
`test-proj-
${
index
}
`
,
avatar_url
:
`avatar-
${
index
}
`
,
full_path
:
`test-proj-
${
index
}
`
,
full_name
:
`test-proj-
${
index
}
`
,
}));
describe
(
'
IntegrationOverrides
'
,
()
=>
{
let
wrapper
;
let
mockAxios
;
const
defaultProps
=
{
overridesPath
:
'
mock/overrides
'
,
};
const
createComponent
=
({
mountFn
=
shallowMount
}
=
{})
=>
{
wrapper
=
mountFn
(
IntegrationOverrides
,
{
propsData
:
defaultProps
,
});
};
beforeEach
(()
=>
{
mockAxios
=
new
MockAdapter
(
axios
);
mockAxios
.
onGet
(
defaultProps
.
overridesPath
).
reply
(
httpStatus
.
OK
,
mockOverrides
,
{
'
X-TOTAL
'
:
mockOverrides
.
length
,
'
X-PAGE
'
:
1
,
});
});
afterEach
(()
=>
{
mockAxios
.
restore
();
wrapper
.
destroy
();
});
const
findGlTable
=
()
=>
wrapper
.
findComponent
(
GlTable
);
const
findPagination
=
()
=>
wrapper
.
findComponent
(
GlPagination
);
const
findRowsAsModel
=
()
=>
findGlTable
()
.
findAllComponents
(
GlLink
)
.
wrappers
.
map
((
link
)
=>
{
const
avatar
=
link
.
findComponent
(
ProjectAvatar
);
return
{
href
:
link
.
attributes
(
'
href
'
),
avatarUrl
:
avatar
.
props
(
'
projectAvatarUrl
'
),
avatarName
:
avatar
.
props
(
'
projectName
'
),
text
:
link
.
text
(),
};
});
describe
(
'
while loading
'
,
()
=>
{
it
(
'
sets GlTable `busy` attribute to `true`
'
,
()
=>
{
createComponent
();
const
table
=
findGlTable
();
expect
(
table
.
exists
()).
toBe
(
true
);
expect
(
table
.
attributes
(
'
busy
'
)).
toBe
(
'
true
'
);
});
});
describe
(
'
when initial request is successful
'
,
()
=>
{
it
(
'
sets GlTable `busy` attribute to `false`
'
,
async
()
=>
{
createComponent
();
await
waitForPromises
();
const
table
=
findGlTable
();
expect
(
table
.
exists
()).
toBe
(
true
);
expect
(
table
.
attributes
(
'
busy
'
)).
toBeFalsy
();
});
describe
(
'
table template
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createComponent
({
mountFn
:
mount
});
await
waitForPromises
();
});
it
(
'
renders overrides as rows in table
'
,
()
=>
{
expect
(
findRowsAsModel
()).
toEqual
(
mockOverrides
.
map
((
x
)
=>
({
href
:
x
.
full_path
,
avatarUrl
:
x
.
avatar_url
,
avatarName
:
x
.
name
,
text
:
expect
.
stringContaining
(
x
.
full_name
),
})),
);
});
});
});
describe
(
'
when request fails
'
,
()
=>
{
beforeEach
(
async
()
=>
{
mockAxios
.
onGet
(
defaultProps
.
overridesPath
).
reply
(
httpStatus
.
INTERNAL_SERVER_ERROR
);
createComponent
();
await
waitForPromises
();
});
it
(
'
calls createFlash
'
,
()
=>
{
expect
(
createFlash
).
toHaveBeenCalledTimes
(
1
);
expect
(
createFlash
).
toHaveBeenCalledWith
({
message
:
IntegrationOverrides
.
i18n
.
defaultErrorMessage
,
captureError
:
true
,
error
:
expect
.
any
(
Error
),
});
});
});
describe
(
'
pagination
'
,
()
=>
{
it
(
'
triggers fetch when `input` event is emitted
'
,
async
()
=>
{
createComponent
();
jest
.
spyOn
(
axios
,
'
get
'
);
await
waitForPromises
();
await
findPagination
().
vm
.
$emit
(
'
input
'
,
2
);
expect
(
axios
.
get
).
toHaveBeenCalledWith
(
defaultProps
.
overridesPath
,
{
params
:
{
page
:
2
,
per_page
:
DEFAULT_PER_PAGE
},
});
});
it
(
'
does not render with <=1 page
'
,
async
()
=>
{
mockAxios
.
onGet
(
defaultProps
.
overridesPath
).
reply
(
httpStatus
.
OK
,
[
mockOverrides
[
0
]],
{
'
X-TOTAL
'
:
1
,
'
X-PAGE
'
:
1
,
});
createComponent
();
await
waitForPromises
();
expect
(
findPagination
().
exists
()).
toBe
(
false
);
});
});
});
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