summaryrefslogtreecommitdiff
path: root/doc/development/fe_guide/merge_request_widget_extensions.md
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-08-18 08:17:02 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-08-18 08:17:02 +0000
commitb39512ed755239198a9c294b6a45e65c05900235 (patch)
treed234a3efade1de67c46b9e5a38ce813627726aa7 /doc/development/fe_guide/merge_request_widget_extensions.md
parentd31474cf3b17ece37939d20082b07f6657cc79a9 (diff)
downloadgitlab-ce-b39512ed755239198a9c294b6a45e65c05900235.tar.gz
Add latest changes from gitlab-org/gitlab@15-3-stable-eev15.3.0-rc42
Diffstat (limited to 'doc/development/fe_guide/merge_request_widget_extensions.md')
-rw-r--r--doc/development/fe_guide/merge_request_widget_extensions.md437
1 files changed, 437 insertions, 0 deletions
diff --git a/doc/development/fe_guide/merge_request_widget_extensions.md b/doc/development/fe_guide/merge_request_widget_extensions.md
new file mode 100644
index 00000000000..a2ff10cc57f
--- /dev/null
+++ b/doc/development/fe_guide/merge_request_widget_extensions.md
@@ -0,0 +1,437 @@
+---
+stage: Create
+group: Code Review
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+---
+
+# Merge request widget extensions **(FREE)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44616) in GitLab 13.6.
+
+Extensions in the merge request widget enable you to add new features
+into the merge request widget that match the design framework.
+With extensions we get a lot of benefits out of the box without much effort required, like:
+
+- A consistent look and feel.
+- Tracking when the extension is opened.
+- Virtual scrolling for performance.
+
+## Usage
+
+To use extensions you must first create a new extension object to fetch the
+data to render in the extension. For a working example, refer to the example file in
+`app/assets/javascripts/vue_merge_request_widget/extensions/issues.js`.
+
+The basic object structure:
+
+```javascript
+export default {
+ name: '', // Required: This helps identify the widget
+ props: [], // Required: Props passed from the widget state
+ i18n: { // Required: Object to hold i18n text
+ label: '', // Required: Used for tooltips and aria-labels
+ loading: '', // Required: Loading text for when data is loading
+ },
+ expandEvent: '', // Optional: RedisHLL event name to track expanding content
+ enablePolling: false, // Optional: Tells extension to poll for data
+ modalComponent: null, // Optional: The component to use for the modal
+ telemetry: true, // Optional: Reports basic telemetry for the extension. Set to false to disable telemetry
+ computed: {
+ summary(data) {}, // Required: Level 1 summary text
+ statusIcon(data) {}, // Required: Level 1 status icon
+ tertiaryButtons() {}, // Optional: Level 1 action buttons
+ shouldCollapse() {}, // Optional: Add logic to determine if the widget can expand or not
+ },
+ methods: {
+ fetchCollapsedData(props) {}, // Required: Fetches data required for collapsed state
+ fetchFullData(props) {}, // Required: Fetches data for the full expanded content
+ fetchMultiData() {}, // Optional: Works in conjunction with `enablePolling` and allows polling multiple endpoints
+ },
+};
+```
+
+By following the same data structure, each extension can follow the same registering structure,
+but each extension can manage its data sources.
+
+After creating this structure, you must register it. You can register the extension at any
+point _after_ the widget has been created. To register a extension:
+
+```javascript
+// Import the register method
+import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
+
+// Import the new extension
+import issueExtension from '~/vue_merge_request_widget/extensions/issues';
+
+// Register the imported extension
+registerExtension(issueExtension);
+```
+
+## Data fetching
+
+Each extension must fetch data. Fetching is handled when registering the extension,
+not by the core component itself. This approach allows for various different
+data fetching methods to be used, such as GraphQL or REST API calls.
+
+### API calls
+
+For performance reasons, it is best if the collapsed state fetches only the data required to
+render the collapsed state. This fetching happens in the `fetchCollapsedData` method.
+This method is called with the props as an argument, so you can easily access
+any paths set in the state.
+
+To allow the extension to set the data, this method **must** return the data. No
+special formatting is required. When the extension receives this data,
+it is set to `collapsedData`. You can access `collapsedData` in any computed property or
+method.
+
+When the user clicks **Expand**, the `fetchFullData` method is called. This method
+also gets called with the props as an argument. This method **must** also return
+the full data. However, this data must be correctly formatted to match the format
+mentioned in the data structure section.
+
+#### Technical debt
+
+For some of the current extensions, there is no split in data fetching. All the data
+is fetched through the `fetchCollapsedData` method. While less performant,
+it allows for faster iteration.
+
+To handle this the `fetchFullData` returns the data set through
+the `fetchCollapsedData` method call. In these cases, the `fetchFullData` must
+return a promise:
+
+```javascript
+fetchCollapsedData() {
+ return ['Some data'];
+},
+fetchFullData() {
+ return Promise.resolve(this.collapsedData)
+},
+```
+
+### Data structure
+
+The data returned from `fetchFullData` must match the format below. This format
+allows the core component to render the data in a way that matches
+the design framework. Any text properties can use the styling placeholders
+mentioned below:
+
+```javascript
+{
+ id: data.id, // Required: ID used as a key for each row
+ header: 'Header' || ['Header', 'sub-header'], // Required: String or array can be used for the header text
+ text: '', // Required: Main text for the row
+ subtext: '', // Optional: Smaller sub-text to be displayed below the main text
+ icon: { // Optional: Icon object
+ name: EXTENSION_ICONS.success, // Required: The icon name for the row
+ },
+ badge: { // Optional: Badge displayed after text
+ text: '', // Required: Text to be displayed inside badge
+ variant: '', // Optional: GitLab UI badge variant, defaults to info
+ },
+ link: { // Optional: Link to a URL displayed after text
+ text: '', // Required: Text of the link
+ href: '', // Optional: URL for the link
+ },
+ modal: { // Optional: Link to open a modal displayed after text
+ text: '', // Required: Text of the link
+ onClick: () => {} // Optional: Function to run when link is clicked, i.e. to set this.modalData
+ }
+ actions: [], // Optional: Action button for row
+ children: [], // Optional: Child content to render, structure matches the same structure
+}
+```
+
+### Polling
+
+To enable polling for an extension, an options flag must be present in the extension:
+
+```javascript
+export default {
+ //...
+ enablePolling: true
+};
+```
+
+This flag tells the base component we should poll the `fetchCollapsedData()`
+defined in the extension. Polling stops if the response has data, or if an error is present.
+
+When writing the logic for `fetchCollapsedData()`, a complete Axios response must be returned
+from the method. The polling utility needs data like polling headers to work correctly:
+
+```javascript
+export default {
+ //...
+ enablePolling: true
+ methods: {
+ fetchCollapsedData() {
+ return axios.get(this.reportPath)
+ },
+ },
+};
+```
+
+Most of the time the data returned from the extension's endpoint is not in the format
+the UI needs. We must format the data before setting the collapsed data in the base component.
+
+If the computed property `summary` can rely on `collapsedData`, you can format the data
+when `fetchFullData` is invoked:
+
+```javascript
+export default {
+ //...
+ enablePolling: true
+ methods: {
+ fetchCollapsedData() {
+ return axios.get(this.reportPath)
+ },
+ fetchFullData() {
+ return Promise.resolve(this.prepareReports());
+ },
+ // custom method
+ prepareReports() {
+ // unpack values from collapsedData
+ const { new_errors, existing_errors, resolved_errors } = this.collapsedData;
+
+ // perform data formatting
+
+ return [...newErrors, ...existingErrors, ...resolvedErrors]
+ }
+ },
+};
+```
+
+If the extension relies on `collapsedData` being formatted before invoking `fetchFullData()`,
+then `fetchCollapsedData()` must return the Axios response as well as the formatted data:
+
+```javascript
+export default {
+ //...
+ enablePolling: true
+ methods: {
+ fetchCollapsedData() {
+ return axios.get(this.reportPath).then(res => {
+ const formattedData = this.prepareReports(res.data)
+
+ return {
+ ...res,
+ data: formattedData,
+ }
+ })
+ },
+ // Custom method
+ prepareReports() {
+ // Unpack values from collapsedData
+ const { new_errors, existing_errors, resolved_errors } = this.collapsedData;
+
+ // Perform data formatting
+
+ return [...newErrors, ...existingErrors, ...resolvedErrors]
+ }
+ },
+};
+```
+
+If the extension must poll multiple endpoints at the same time, then `fetchMultiData`
+can be used to return an array of functions. A new `poll` object is created for each
+endpoint and they are polled separately. After all endpoints are resolved, polling is
+stopped and `setCollapsedData` is called with an array of `response.data`.
+
+```javascript
+export default {
+ //...
+ enablePolling: true
+ methods: {
+ fetchMultiData() {
+ return [
+ () => axios.get(this.reportPath1),
+ () => axios.get(this.reportPath2),
+ () => axios.get(this.reportPath3)
+ },
+ },
+};
+```
+
+WARNING:
+The function must return a `Promise` that resolves the `response` object.
+The implementation relies on the `POLL-INTERVAL` header to keep polling, therefore it is
+important not to alter the status code and headers.
+
+### 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, add it to the `i18n` object in your extension:
+
+```javascript
+export default {
+ //...
+ i18n: {
+ //...
+ error: __('Your error text'),
+ },
+};
+```
+
+## Telemetry
+
+The base implementation of the widget extension framework includes some telemetry events.
+Each widget reports:
+
+- `view`: When it is rendered to the screen.
+- `expand`: When it is expanded.
+- `full_report_clicked`: When an (optional) input is clicked to view the full report.
+- Outcome (`expand_success`, `expand_warning`, or `expand_failed`): One of three
+ additional events relating to the status of the widget when it was expanded.
+
+### Add new widgets
+
+When adding new widgets, the above events must be marked as `known`, and have metrics
+created, to be reportable.
+
+NOTE:
+Events that are only for EE should include `--ee` at the end of both shell commands below.
+
+To generate these known events for a single widget:
+
+1. Widgets should be named `Widget${CamelName}`.
+ - For example: a widget for **Test Reports** should be `WidgetTestReports`.
+1. Compute the widget name slug by converting the `${CamelName}` to lower-, snake-case.
+ - The previous example would be `test_reports`.
+1. Add the new widget name slug to `lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb`
+ in the `WIDGETS` list.
+1. Ensure the GDK is running (`gdk start`).
+1. Generate known events on the command line with the following command.
+ Replace `test_reports` with your appropriate name slug:
+
+ ```shell
+ bundle exec rails generate gitlab:usage_metric_definition \
+ counts.i_code_review_merge_request_widget_test_reports_count_view \
+ counts.i_code_review_merge_request_widget_test_reports_count_full_report_clicked \
+ counts.i_code_review_merge_request_widget_test_reports_count_expand \
+ counts.i_code_review_merge_request_widget_test_reports_count_expand_success \
+ counts.i_code_review_merge_request_widget_test_reports_count_expand_warning \
+ counts.i_code_review_merge_request_widget_test_reports_count_expand_failed \
+ --dir=all
+ ```
+
+1. Modify each newly generated file to match the existing files for the merge request widget extension telemetry.
+ - Find existing examples by doing a glob search, like: `metrics/**/*_i_code_review_merge_request_widget_*`
+ - Roughly speaking, each file should have these values:
+ 1. `description` = A plain English description of this value. Review existing widget extension telemetry files for examples.
+ 1. `product_section` = `dev`
+ 1. `product_stage` = `create`
+ 1. `product_group` = `code_review`
+ 1. `product_category` = `code_review`
+ 1. `introduced_by_url` = `'[your MR]'`
+ 1. `options.events` = (the event in the command from above that generated this file, like `i_code_review_merge_request_widget_test_reports_count_view`)
+ - This value is how the telemetry events are linked to "metrics" so this is probably one of the more important values.
+ 1. `data_source` = `redis`
+ 1. `data_category` = `optional`
+1. Generate known HLL events on the command line with the following command.
+ Replace `test_reports` with your appropriate name slug.
+
+ ```shell
+ bundle exec rails generate gitlab:usage_metric_definition:redis_hll code_review \
+ i_code_review_merge_request_widget_test_reports_view \
+ i_code_review_merge_request_widget_test_reports_full_report_clicked \
+ i_code_review_merge_request_widget_test_reports_expand \
+ i_code_review_merge_request_widget_test_reports_expand_success \
+ i_code_review_merge_request_widget_test_reports_expand_warning \
+ i_code_review_merge_request_widget_test_reports_expand_failed \
+ --class_name=RedisHLLMetric
+ ```
+
+1. Repeat step 6, but change the `data_source` to `redis_hll`.
+1. Add each of the HLL metrics to `lib/gitlab/usage_data_counters/known_events/code_review_events.yml`:
+ 1. `name` = (the event)
+ 1. `redis_slot` = `code_review`
+ 1. `category` = `code_review`
+ 1. `aggregation` = `weekly`
+1. Add each event to the appropriate aggregates in `config/metrics/aggregates/code_review.yml`
+
+### Add new events
+
+If you are adding a new event to our known events, include the new event in the
+`KNOWN_EVENTS` list in `lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb`.
+
+## Icons
+
+Level 1 and all subsequent levels can have their own status icons. To keep with
+the design framework, import the `EXTENSION_ICONS` constant
+from the `constants.js` file:
+
+```javascript
+import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants.js';
+```
+
+This constant has the below icons available for use. Per the design framework,
+only some of these icons should be used on level 1:
+
+- `failed`
+- `warning`
+- `success`
+- `neutral`
+- `error`
+- `notice`
+- `severityCritical`
+- `severityHigh`
+- `severityMedium`
+- `severityLow`
+- `severityInfo`
+- `severityUnknown`
+
+## Text styling
+
+Any area that has text can be styled with the placeholders below. This
+technique follows the same technique as `sprintf`. However, instead of specifying
+these through `sprintf`, the extension does this automatically.
+
+Every placeholder contains starting and ending tags. For example, `success` uses
+`Hello %{success_start}world%{success_end}`. The extension then
+adds the start and end tags with the correct styling classes.
+
+| Placeholder | Style |
+|-------------|-----------------------------------------|
+| success | `gl-font-weight-bold gl-text-green-500` |
+| danger | `gl-font-weight-bold gl-text-red-500` |
+| critical | `gl-font-weight-bold gl-text-red-800` |
+| same | `gl-font-weight-bold gl-text-gray-700` |
+| strong | `gl-font-weight-bold` |
+| small | `gl-font-sm` |
+
+## Action buttons
+
+You can add action buttons to all level 1 and 2 in each extension. These buttons
+are meant as a way to provide links or actions for each row:
+
+- Action buttons for level 1 can be set through the `tertiaryButtons` computed property.
+ This property should return an array of objects for each action button.
+- Action buttons for level 2 can be set by adding the `actions` key to the level 2 rows object.
+ The value for this key must also be an array of objects for each action button.
+
+Links must follow this structure:
+
+```javascript
+{
+ text: 'Click me',
+ href: this.someLinkHref,
+ target: '_blank', // Optional
+}
+```
+
+For internal action buttons, follow this structure:
+
+```javascript
+{
+ text: 'Click me',
+ onClick() {}
+}
+```