マージリクエストウィジェット拡張機能
GitLab 13.6で導入されました。
マージリクエストウィジェットの拡張機能によって、デザインフレームワークにマッチした新しい機能をマージリクエストウィジェットに追加することができます。拡張機能を使うことで、以下のような多くのメリットが得られます:
- 一貫したルック&フィール
- エクステンションを開いたときのトラッキング。
- パフォーマンスのための仮想スクロール。
使用方法
エクステンションを使用するには、まず新しいエクステンションオブジェクトを作成して、エクステンションでレンダリングするデータを取得する必要があります。動作例については、app/assets/javascripts/vue_merge_request_widget/extensions/issues.js にあるサンプルファイルを参照してください。
基本的なオブジェクト構造
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(data) {}, // 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
},
};
同じデータ構造に従うことで、各エクステンションは同じ登録構造に従うことができますが、各エクステンションはデータソースを管理することができます。
この構造を作成した後、登録する必要があります。拡張機能の登録は、ウィジェットが作成された_後の_どの時点でも可能です。エクステンションを登録するには
// 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);
データ取得
各エクステンションはデータをフェッチしなければなりません。フェッチは拡張機能の登録時に処理され、Core コンポーネント自体では処理されません。このアプローチでは、GraphQL や REST API 呼び出しなど、さまざまな異なるデータ取得方法を使用できます。
APIコール
パフォーマンス上の理由から、折りたたまれた状態をレンダリングするために必要なデータのみをフェッチするのが最善です。このフェッチはfetchCollapsedData メソッドで行われます。このメソッドはpropsを引数として呼び出されるので、ステートで設定された任意のパスに簡単にアクセスできます。
拡張機能がデータを設定できるようにするために、このメソッドはデータを返さなければなりません。特別なフォーマットは必要ありません。エクステンションがこのデータを受信すると、そのデータがcollapsedData. collapsedDataPATH に設定されます。 任意の計算プロパティまたはメソッドでcollapsedDataアクセスできます collapsedData。
ユーザーがExpandを選択すると、fetchFullData メソッドが呼び出されます。このメソッドもpropsを引数として呼び出されます。このメソッドも完全なデータを返す必要があります。しかし、このデータはデータ構造のセクションで述べたフォーマットと一致するように正しくフォーマットされなければなりません。
技術的負債
現在のいくつかの拡張機能では、データフェッチに分割がありません。すべてのデータはfetchCollapsedData メソッドを通してフェッチされます。パフォーマンスは落ちますが、より高速な反復が可能になります。
これを処理するために、fetchFullData はfetchCollapsedData メソッド呼び出しを通してデータセットを返します。このような場合、fetchFullData はプロミスを返さなければなりません:
fetchCollapsedData() {
return ['Some data'];
},
fetchFullData() {
return Promise.resolve(this.collapsedData)
},
データ構造
fetchFullData から返されるデータは、以下のフォーマットと一致する必要があります。このフォーマットにより、Coreコンポーネントはデザインフレームワークにマッチした方法でデータをレンダリングすることができます。テキスト・プロパティには、後述のスタイリング・プレースホルダを使用できます:
{
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
}
ポーリング
拡張モジュールでポーリングを有効にするには、その拡張モジュールにオプションフラグがなければなりません:
export default {
//...
enablePolling: true
};
このフラグは、拡張モジュールで定義されているfetchCollapsedData() をポーリングするようにベースコンポーネントに伝えます。ポーリングは、レスポンスにデータがあるかエラーがあれば停止します。
fetchCollapsedData() のロジックを記述する場合、メソッドから完全な Axios レスポンスが返される必要があります。ポーリングユーティリティが正しく動作するには、ポーリングヘッダのようなデータが必要です:
export default {
//...
enablePolling: true
methods: {
fetchCollapsedData() {
return axios.get(this.reportPath)
},
},
};
ほとんどの場合、拡張機能のエンドポイントから返されるデータは UI が必要とする形式ではありません。ベースコンポーネントに折りたたまれたデータを設定する前に、データをフォーマットする必要があります。
計算されたプロパティsummary がcollapsedData に依存できる場合、fetchFullData が呼び出されたときにデータをフォーマットできます:
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]
}
},
};
拡張機能がfetchFullData() を呼び出す前にcollapsedData がフォーマットされていることに依存する場合、fetchCollapsedData() はフォーマットされたデータと同様に Axios 応答を返す必要があります:
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]
}
},
};
拡張機能が複数のエンドポイントを同時にポーリングする必要がある場合、fetchMultiData を使用して関数の配列を返すことができます。各エンドポイントに対して新しいpoll オブジェクトが作成され、個別にポーリングされます。すべてのエンドポイントが解決された後、ポーリングは停止され、setCollapsedData がresponse.data の配列とともに呼び出されます。
export default {
//...
enablePolling: true
methods: {
fetchMultiData() {
return [
() => axios.get(this.reportPath1),
() => axios.get(this.reportPath2),
() => axios.get(this.reportPath3)
},
},
};
response オブジェクトを解決するPromise を返さなければなりません。この実装では、ポーリングを継続するためにPOLL-INTERVAL ヘッダに依存しています。したがって、ステータスコードとヘッダを変更しないことが重要です。エラー
fetchCollapsedData() またはfetchFullData() のメソッドがエラーをスローした場合:
-
fetchCollapsedData()メソッドがエラーをスローした場合、拡張機能のロード状態はLOADING_STATES.collapsedErrorに更新されます。 -
fetchFullData()メソッドがエラーをスローした場合、拡張機能のロード状態はLOADING_STATES.expandedErrorに更新されます。 - エクステンションのヘッダはエラーアイコンを表示し、テキストを更新します:
-
$options.i18n.errorで定義されたテキスト。 -
$options.i18n.errorが定義されていない場合は、”Failed to load” となります。
-
- このエラーはSentryに送られ、発生したことが記録されます。
エラーテキストをカスタマイズするには、エクステンションのi18n オブジェクトに追加してください:
export default {
//...
i18n: {
//...
error: __('Your error text'),
},
};
遠隔測定
ウィジェット拡張フレームワークの基本実装は、いくつかの遠隔測定イベントを含みます。それぞれのウィジェットはレポーターします:
-
view:画面にレンダリングされたとき。 -
expand:拡大時 -
full_report_clicked:(オプションの)入力がクリックされ、完全なレポートが表示されたとき。 - 結果 (
expand_success,expand_warning, またはexpand_failed):ウィジェットが展開されたときのステータスに関連する3つの追加イベントの1つ。
新しいウィジェットの追加
新しいウィジェットを追加する場合は、上記のイベントがknown としてマークされ、メトリクスが作成されている必要があります。
--ee 。1つのウィジェットに対してこれらの既知のイベントを生成するには、以下のようにします:
- ウィジェットの名前は
Widget${CamelName}にします。- 例:テストレポートのウィジェットは
WidgetTestReportsとします。
- 例:テストレポートのウィジェットは
-
${CamelName}を小文字のスネークケースに変換することで、ウィジェット名のスラッグを計算します。- 前の例では、
test_reports。
- 前の例では、
-
WIDGETSリストのlib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rbに新しいウィジェット名 slug を追加します。 - GDK が実行されていることを確認してください (
gdk start)。 -
以下のコマンドを使用して、コマンドラインで既知のイベントを生成します。
test_reportsを適切な名前のスラッグに置き換えてください: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 - マージリクエストウィジェットの拡張子 telemetry の既存のファイルと一致するように、新しく生成された各ファイルを修正します。
- のようにグロブ検索して既存の例を見つけます:
metrics/**/*_i_code_review_merge_request_widget_* - 大まかに言えば、各ファイルは以下の値を持っているはずです:
-
description= この値のわかりやすい英語の説明。既存のウィジェット拡張機能の遠隔測定ファイルの例をレビューしてください。 -
product_section=dev -
product_stage=create -
product_group=code_review -
introduced_by_url='[your MR]' -
options.events= (i_code_review_merge_request_widget_test_reports_count_viewのように、このファイルを生成した上記のコマンドのイベント)- この値は、遠隔測定イベントが “メトリクス “にリンクされる方法なので、おそらくより重要な値の1つです。
-
data_source=redis -
data_category=optional
-
- のようにグロブ検索して既存の例を見つけます:
-
以下のコマンドを使用して、コマンドラインで既知のHLLイベントを生成します。
test_reportsを適切な名前のスラッグに置き換えてください。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 -
ステップ6を繰り返しますが、
data_sourceをredis_hllに変更します。 - 各イベント(ステップ7のコマンドでリストされたもので、
test_reportsを適切な名前のスラッグに置き換えたもの)を集約ファイルに追加します:config/metrics/counts_7d/{timestamp}_code_review_category_monthly_active_users.ymlconfig/metrics/counts_7d/{timestamp}_code_review_group_monthly_active_users.ymlconfig/metrics/counts_28d/{timestamp}_code_review_category_monthly_active_users.ymlconfig/metrics/counts_28d/{timestamp}_code_review_group_monthly_active_users.yml
新しいイベントを追加
既知のイベントに新しいイベントを追加する場合は、lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb のKNOWN_EVENTS リストに新しいイベントを追加してください。
アイコン
レベル1とそれに続くすべてのレベルは、独自のステータスアイコンを持つことができます。デザインの枠組みを維持するために、constants.js ファイルからEXTENSION_ICONS 定数をインポートします:
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants.js';
この定数には、使用可能な以下のアイコンがあります。この定数には、使用可能な以下のアイコンがあります。デザイン・フレームワークに従って、レベル 1 ではこれらのアイコンの一部のみを使用します:
failedwarningsuccessneutralerrornoticeseverityCriticalseverityHighseverityMediumseverityLowseverityInfoseverityUnknown
テキスト・スタイリング
テキストを含むすべての領域は、以下のプレースホルダでスタイル設定できます。このテクニックはsprintf. sprintfNET と同じテクニックにsprintf従いますが sprintf、.NETsprintfを通してこれらを指定する代わりに sprintf、エクステンションが自動的に行います。
すべてのプレースホルダは開始タグと終了タグを含みます。たとえば、success はHello %{success_start}world%{success_end} を使用します。そして、エクステンションは正しいスタイリングクラスで開始タグと終了タグを追加します。
| プレースホルダー | スタイル |
|---|---|
| 成功 | gl-font-weight-bold gl-text-green-500 |
| 危険 | gl-font-weight-bold gl-text-red-500 |
| 危ない | gl-font-weight-bold gl-text-red-800 |
| 同じ | gl-font-weight-bold gl-text-gray-700 |
| 強い | gl-font-weight-bold |
| 小さい | gl-font-sm |
アクションボタン
各エクステンションのすべてのレベル1と2にアクションボタンを追加できます。これらのボタンは、各行にリンクまたはアクションを提供するためのものです:
- レベル 1 のアクション・ボタンは、
tertiaryButtons計算プロパティによって設定できます。このプロパティは、各アクション・ボタンのオブジェクトの配列を返す必要があります。 - レベル 2 のアクション・ボタンは、レベル 2 行オブジェクトに
actionsキーを追加することで設定できます。このキーの値も、各アクション・ボタンのオブジェクトの配列である必要があります。
リンクは、この構造に従う必要があります:
{
text: 'Click me',
href: this.someLinkHref,
target: '_blank', // Optional
}
内部アクションボタンはこの構造に従ってください:
{
text: 'Click me',
onClick() {}
}