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
33fd54f3
Commit
33fd54f3
authored
Nov 24, 2021
by
Robert Hunt
Committed by
Phil Hughes
Nov 24, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Report Widgets Core: Add support for fetch failures
parent
05004557
Changes
5
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
208 additions
and
17 deletions
+208
-17
app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
...s/vue_merge_request_widget/components/extensions/base.vue
+27
-13
doc/development/new_fe_guide/modules/widget_extensions.md
doc/development/new_fe_guide/modules/widget_extensions.md
+23
-0
locale/gitlab.pot
locale/gitlab.pot
+3
-0
spec/frontend/vue_mr_widget/mr_widget_options_spec.js
spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+56
-4
spec/frontend/vue_mr_widget/test_extensions.js
spec/frontend/vue_mr_widget/test_extensions.js
+99
-0
No files found.
app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
View file @
33fd54f3
...
@@ -9,10 +9,11 @@ import {
...
@@ -9,10 +9,11 @@ import {
GlIntersectionObserver
,
GlIntersectionObserver
,
}
from
'
@gitlab/ui
'
;
}
from
'
@gitlab/ui
'
;
import
{
once
}
from
'
lodash
'
;
import
{
once
}
from
'
lodash
'
;
import
*
as
Sentry
from
'
@sentry/browser
'
;
import
api
from
'
~/api
'
;
import
api
from
'
~/api
'
;
import
{
sprintf
,
s__
,
__
}
from
'
~/locale
'
;
import
{
sprintf
,
s__
,
__
}
from
'
~/locale
'
;
import
SmartVirtualList
from
'
~/vue_shared/components/smart_virtual_list.vue
'
;
import
SmartVirtualList
from
'
~/vue_shared/components/smart_virtual_list.vue
'
;
import
{
EXTENSION_ICON_CLASS
}
from
'
../../constants
'
;
import
{
EXTENSION_ICON_CLASS
,
EXTENSION_ICONS
}
from
'
../../constants
'
;
import
StatusIcon
from
'
./status_icon.vue
'
;
import
StatusIcon
from
'
./status_icon.vue
'
;
import
Actions
from
'
./actions.vue
'
;
import
Actions
from
'
./actions.vue
'
;
...
@@ -20,6 +21,7 @@ export const LOADING_STATES = {
...
@@ -20,6 +21,7 @@ export const LOADING_STATES = {
collapsedLoading
:
'
collapsedLoading
'
,
collapsedLoading
:
'
collapsedLoading
'
,
collapsedError
:
'
collapsedError
'
,
collapsedError
:
'
collapsedError
'
,
expandedLoading
:
'
expandedLoading
'
,
expandedLoading
:
'
expandedLoading
'
,
expandedError
:
'
expandedError
'
,
};
};
export
default
{
export
default
{
...
@@ -40,8 +42,8 @@ export default {
...
@@ -40,8 +42,8 @@ export default {
data
()
{
data
()
{
return
{
return
{
loadingState
:
LOADING_STATES
.
collapsedLoading
,
loadingState
:
LOADING_STATES
.
collapsedLoading
,
collapsedData
:
null
,
collapsedData
:
{}
,
fullData
:
null
,
fullData
:
[]
,
isCollapsed
:
true
,
isCollapsed
:
true
,
showFade
:
false
,
showFade
:
false
,
};
};
...
@@ -53,6 +55,9 @@ export default {
...
@@ -53,6 +55,9 @@ export default {
widgetLoadingText
()
{
widgetLoadingText
()
{
return
this
.
$options
.
i18n
?.
loading
||
__
(
'
Loading...
'
);
return
this
.
$options
.
i18n
?.
loading
||
__
(
'
Loading...
'
);
},
},
widgetErrorText
()
{
return
this
.
$options
.
i18n
?.
error
||
__
(
'
Failed to load
'
);
},
isLoadingSummary
()
{
isLoadingSummary
()
{
return
this
.
loadingState
===
LOADING_STATES
.
collapsedLoading
;
return
this
.
loadingState
===
LOADING_STATES
.
collapsedLoading
;
},
},
...
@@ -60,11 +65,16 @@ export default {
...
@@ -60,11 +65,16 @@ export default {
return
this
.
loadingState
===
LOADING_STATES
.
expandedLoading
;
return
this
.
loadingState
===
LOADING_STATES
.
expandedLoading
;
},
},
isCollapsible
()
{
isCollapsible
()
{
if
(
this
.
isLoadingSummary
)
{
return
!
this
.
isLoadingSummary
&&
this
.
loadingState
!==
LOADING_STATES
.
collapsedError
;
return
false
;
},
}
hasFullData
()
{
return
this
.
fullData
.
length
>
0
;
return
true
;
},
hasFetchError
()
{
return
(
this
.
loadingState
===
LOADING_STATES
.
collapsedError
||
this
.
loadingState
===
LOADING_STATES
.
expandedError
);
},
},
collapseButtonLabel
()
{
collapseButtonLabel
()
{
return
sprintf
(
return
sprintf
(
...
@@ -75,6 +85,7 @@ export default {
...
@@ -75,6 +85,7 @@ export default {
);
);
},
},
statusIconName
()
{
statusIconName
()
{
if
(
this
.
hasFetchError
)
return
EXTENSION_ICONS
.
error
;
if
(
this
.
isLoadingSummary
)
return
null
;
if
(
this
.
isLoadingSummary
)
return
null
;
return
this
.
statusIcon
(
this
.
collapsedData
);
return
this
.
statusIcon
(
this
.
collapsedData
);
...
@@ -100,7 +111,8 @@ export default {
...
@@ -100,7 +111,8 @@ export default {
})
})
.
catch
((
e
)
=>
{
.
catch
((
e
)
=>
{
this
.
loadingState
=
LOADING_STATES
.
collapsedError
;
this
.
loadingState
=
LOADING_STATES
.
collapsedError
;
throw
e
;
Sentry
.
captureException
(
e
);
});
});
},
},
methods
:
{
methods
:
{
...
@@ -115,7 +127,7 @@ export default {
...
@@ -115,7 +127,7 @@ export default {
this
.
triggerRedisTracking
();
this
.
triggerRedisTracking
();
},
},
loadAllData
()
{
loadAllData
()
{
if
(
this
.
f
ullData
)
return
;
if
(
this
.
hasF
ullData
)
return
;
this
.
loadingState
=
LOADING_STATES
.
expandedLoading
;
this
.
loadingState
=
LOADING_STATES
.
expandedLoading
;
...
@@ -125,8 +137,9 @@ export default {
...
@@ -125,8 +137,9 @@ export default {
this
.
fullData
=
data
;
this
.
fullData
=
data
;
})
})
.
catch
((
e
)
=>
{
.
catch
((
e
)
=>
{
this
.
loadingState
=
null
;
this
.
loadingState
=
LOADING_STATES
.
expandedError
;
throw
e
;
Sentry
.
captureException
(
e
);
});
});
},
},
appear
(
index
)
{
appear
(
index
)
{
...
@@ -158,6 +171,7 @@ export default {
...
@@ -158,6 +171,7 @@ export default {
>
>
<div
class=
"gl-flex-grow-1"
>
<div
class=
"gl-flex-grow-1"
>
<template
v-if=
"isLoadingSummary"
>
{{
widgetLoadingText
}}
</
template
>
<template
v-if=
"isLoadingSummary"
>
{{
widgetLoadingText
}}
</
template
>
<
template
v-else-if=
"hasFetchError"
>
{{
widgetErrorText
}}
</
template
>
<div
v-else
v-safe-html=
"summary(collapsedData)"
></div>
<div
v-else
v-safe-html=
"summary(collapsedData)"
></div>
</div>
</div>
<actions
<actions
...
@@ -189,7 +203,7 @@ export default {
...
@@ -189,7 +203,7 @@ export default {
<gl-loading-icon
size=
"sm"
inline
/>
{{ __('Loading...') }}
<gl-loading-icon
size=
"sm"
inline
/>
{{ __('Loading...') }}
</div>
</div>
<smart-virtual-list
<smart-virtual-list
v-else-if=
"
f
ullData"
v-else-if=
"
hasF
ullData"
:length=
"fullData.length"
:length=
"fullData.length"
:remain=
"20"
:remain=
"20"
:size=
"32"
:size=
"32"
...
...
doc/development/new_fe_guide/modules/widget_extensions.md
View file @
33fd54f3
...
@@ -54,3 +54,26 @@ import issueExtension from '~/vue_merge_request_widget/extensions/issues';
...
@@ -54,3 +54,26 @@ import issueExtension from '~/vue_merge_request_widget/extensions/issues';
// Register the imported extension
// Register the imported extension
registerExtension
(
issueExtension
);
registerExtension
(
issueExtension
);
```
```
## Fetching errors
If
`fetchCollapsedData()`
or
`fetchFullData()`
methods throw an error:
-
The loading state of the extension is updated to
`LOADING_STATES.collapsedError`
and
`LOADING_STATES.expandedError`
respectively.
-
The extensions header displays an error icon and updates the text to be either:
-
The text defined in
`$options.i18n.error`
.
-
"Failed to load" if
`$options.i18n.error`
is not defined.
-
The error is sent to Sentry to log that it occurred.
To customise the error text, you need to add it to the
`i18n`
object in your extension:
```
javascript
export
default
{
//...
i18n
:
{
//...
error
:
__
(
'
Your error text
'
),
},
};
```
locale/gitlab.pot
View file @
33fd54f3
...
@@ -14343,6 +14343,9 @@ msgstr ""
...
@@ -14343,6 +14343,9 @@ msgstr ""
msgid "Failed to install."
msgid "Failed to install."
msgstr ""
msgstr ""
msgid "Failed to load"
msgstr ""
msgid "Failed to load assignees."
msgid "Failed to load assignees."
msgstr ""
msgstr ""
...
...
spec/frontend/vue_mr_widget/mr_widget_options_spec.js
View file @
33fd54f3
...
@@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils';
...
@@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils';
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
Vue
,
{
nextTick
}
from
'
vue
'
;
import
Vue
,
{
nextTick
}
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
*
as
Sentry
from
'
@sentry/browser
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
{
securityReportMergeRequestDownloadPathsQueryResponse
}
from
'
jest/vue_shared/security_reports/mock_data
'
;
import
{
securityReportMergeRequestDownloadPathsQueryResponse
}
from
'
jest/vue_shared/security_reports/mock_data
'
;
...
@@ -19,10 +20,15 @@ import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/consta
...
@@ -19,10 +20,15 @@ import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/consta
import
eventHub
from
'
~/vue_merge_request_widget/event_hub
'
;
import
eventHub
from
'
~/vue_merge_request_widget/event_hub
'
;
import
MrWidgetOptions
from
'
~/vue_merge_request_widget/mr_widget_options.vue
'
;
import
MrWidgetOptions
from
'
~/vue_merge_request_widget/mr_widget_options.vue
'
;
import
{
stateKey
}
from
'
~/vue_merge_request_widget/stores/state_maps
'
;
import
{
stateKey
}
from
'
~/vue_merge_request_widget/stores/state_maps
'
;
import
StatusIcon
from
'
~/vue_merge_request_widget/components/extensions/status_icon.vue
'
;
import
securityReportMergeRequestDownloadPathsQuery
from
'
~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql
'
;
import
securityReportMergeRequestDownloadPathsQuery
from
'
~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql
'
;
import
{
faviconDataUrl
,
overlayDataUrl
}
from
'
../lib/utils/mock_data
'
;
import
{
faviconDataUrl
,
overlayDataUrl
}
from
'
../lib/utils/mock_data
'
;
import
mockData
from
'
./mock_data
'
;
import
mockData
from
'
./mock_data
'
;
import
testExtension
from
'
./test_extension
'
;
import
{
workingExtension
,
collapsedDataErrorExtension
,
fullDataErrorExtension
,
}
from
'
./test_extensions
'
;
jest
.
mock
(
'
~/api.js
'
);
jest
.
mock
(
'
~/api.js
'
);
...
@@ -892,7 +898,7 @@ describe('MrWidgetOptions', () => {
...
@@ -892,7 +898,7 @@ describe('MrWidgetOptions', () => {
describe
(
'
mock extension
'
,
()
=>
{
describe
(
'
mock extension
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
registerExtension
(
test
Extension
);
registerExtension
(
working
Extension
);
createComponent
();
createComponent
();
});
});
...
@@ -914,7 +920,7 @@ describe('MrWidgetOptions', () => {
...
@@ -914,7 +920,7 @@ describe('MrWidgetOptions', () => {
.
find
(
'
[data-testid="widget-extension"] [data-testid="toggle-button"]
'
)
.
find
(
'
[data-testid="widget-extension"] [data-testid="toggle-button"]
'
)
.
trigger
(
'
click
'
);
.
trigger
(
'
click
'
);
await
Vue
.
nextTick
();
await
nextTick
();
expect
(
api
.
trackRedisHllUserEvent
).
toHaveBeenCalledWith
(
'
test_expand_event
'
);
expect
(
api
.
trackRedisHllUserEvent
).
toHaveBeenCalledWith
(
'
test_expand_event
'
);
});
});
...
@@ -926,7 +932,7 @@ describe('MrWidgetOptions', () => {
...
@@ -926,7 +932,7 @@ describe('MrWidgetOptions', () => {
.
find
(
'
[data-testid="widget-extension"] [data-testid="toggle-button"]
'
)
.
find
(
'
[data-testid="widget-extension"] [data-testid="toggle-button"]
'
)
.
trigger
(
'
click
'
);
.
trigger
(
'
click
'
);
await
Vue
.
nextTick
();
await
nextTick
();
expect
(
expect
(
wrapper
.
find
(
'
[data-testid="widget-extension-top-level"]
'
).
find
(
GlDropdown
).
exists
(),
wrapper
.
find
(
'
[data-testid="widget-extension-top-level"]
'
).
find
(
GlDropdown
).
exists
(),
...
@@ -952,4 +958,50 @@ describe('MrWidgetOptions', () => {
...
@@ -952,4 +958,50 @@ describe('MrWidgetOptions', () => {
expect
(
collapsedSection
.
find
(
GlButton
).
text
()).
toBe
(
'
Full report
'
);
expect
(
collapsedSection
.
find
(
GlButton
).
text
()).
toBe
(
'
Full report
'
);
});
});
});
});
describe
(
'
mock extension errors
'
,
()
=>
{
let
captureException
;
const
itHandlesTheException
=
()
=>
{
expect
(
captureException
).
toHaveBeenCalledTimes
(
1
);
expect
(
captureException
).
toHaveBeenCalledWith
(
new
Error
(
'
Fetch error
'
));
expect
(
wrapper
.
findComponent
(
StatusIcon
).
props
(
'
iconName
'
)).
toBe
(
'
error
'
);
};
beforeEach
(()
=>
{
captureException
=
jest
.
spyOn
(
Sentry
,
'
captureException
'
);
});
afterEach
(()
=>
{
registeredExtensions
.
extensions
=
[];
captureException
=
null
;
});
it
(
'
handles collapsed data fetch errors
'
,
async
()
=>
{
registerExtension
(
collapsedDataErrorExtension
);
createComponent
();
await
waitForPromises
();
expect
(
wrapper
.
find
(
'
[data-testid="widget-extension"] [data-testid="toggle-button"]
'
).
exists
(),
).
toBe
(
false
);
itHandlesTheException
();
});
it
(
'
handles full data fetch errors
'
,
async
()
=>
{
registerExtension
(
fullDataErrorExtension
);
createComponent
();
await
waitForPromises
();
expect
(
wrapper
.
findComponent
(
StatusIcon
).
props
(
'
iconName
'
)).
not
.
toBe
(
'
error
'
);
wrapper
.
find
(
'
[data-testid="widget-extension"] [data-testid="toggle-button"]
'
)
.
trigger
(
'
click
'
);
await
nextTick
();
await
waitForPromises
();
itHandlesTheException
();
});
});
});
});
spec/frontend/vue_mr_widget/test_extension.js
→
spec/frontend/vue_mr_widget/test_extension
s
.js
View file @
33fd54f3
import
{
EXTENSION_ICONS
}
from
'
~/vue_merge_request_widget/constants
'
;
import
{
EXTENSION_ICONS
}
from
'
~/vue_merge_request_widget/constants
'
;
export
default
{
export
const
workingExtension
=
{
name
:
'
WidgetTestExtension
'
,
name
:
'
WidgetTestExtension
'
,
props
:
[
'
targetProjectFullPath
'
],
props
:
[
'
targetProjectFullPath
'
],
expandEvent
:
'
test_expand_event
'
,
expandEvent
:
'
test_expand_event
'
,
...
@@ -37,3 +37,63 @@ export default {
...
@@ -37,3 +37,63 @@ export default {
},
},
},
},
};
};
export
const
collapsedDataErrorExtension
=
{
name
:
'
WidgetTestCollapsedErrorExtension
'
,
props
:
[
'
targetProjectFullPath
'
],
expandEvent
:
'
test_expand_event
'
,
computed
:
{
summary
({
count
,
targetProjectFullPath
})
{
return
`Test extension summary count:
${
count
}
&
${
targetProjectFullPath
}
`
;
},
statusIcon
({
count
})
{
return
count
>
0
?
EXTENSION_ICONS
.
warning
:
EXTENSION_ICONS
.
success
;
},
},
methods
:
{
fetchCollapsedData
()
{
return
Promise
.
reject
(
new
Error
(
'
Fetch error
'
));
},
fetchFullData
()
{
return
Promise
.
resolve
([
{
id
:
1
,
text
:
'
Hello world
'
,
icon
:
{
name
:
EXTENSION_ICONS
.
failed
,
},
badge
:
{
text
:
'
Closed
'
,
},
link
:
{
href
:
'
https://gitlab.com
'
,
text
:
'
GitLab.com
'
,
},
actions
:
[{
text
:
'
Full report
'
,
href
:
'
https://gitlab.com
'
,
target
:
'
_blank
'
}],
},
]);
},
},
};
export
const
fullDataErrorExtension
=
{
name
:
'
WidgetTestCollapsedErrorExtension
'
,
props
:
[
'
targetProjectFullPath
'
],
expandEvent
:
'
test_expand_event
'
,
computed
:
{
summary
({
count
,
targetProjectFullPath
})
{
return
`Test extension summary count:
${
count
}
&
${
targetProjectFullPath
}
`
;
},
statusIcon
({
count
})
{
return
count
>
0
?
EXTENSION_ICONS
.
warning
:
EXTENSION_ICONS
.
success
;
},
},
methods
:
{
fetchCollapsedData
({
targetProjectFullPath
})
{
return
Promise
.
resolve
({
targetProjectFullPath
,
count
:
1
});
},
fetchFullData
()
{
return
Promise
.
reject
(
new
Error
(
'
Fetch error
'
));
},
},
};
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