マージリクエストウィジェット拡張機能

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 メソッドを通してフェッチされます。パフォーマンスは落ちますが、より高速な反復が可能になります。

これを処理するために、fetchFullDatafetchCollapsedData メソッド呼び出しを通してデータセットを返します。このような場合、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 が必要とする形式ではありません。ベースコンポーネントに折りたたまれたデータを設定する前に、データをフォーマットする必要があります。

計算されたプロパティsummarycollapsedData に依存できる場合、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 オブジェクトが作成され、個別にポーリングされます。すべてのエンドポイントが解決された後、ポーリングは停止され、setCollapsedDataresponse.data の配列とともに呼び出されます。

export default {
  //...
  enablePolling: true
  methods: {
    fetchMultiData() {
      return [
        () => axios.get(this.reportPath1),
        () => axios.get(this.reportPath2),
        () => axios.get(this.reportPath3)
    },
  },
};
caution
この関数は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 としてマークされ、メトリクスが作成されている必要があります。

note
EEのみのイベントは、以下の両方のShellコマンドの最後に--ee

1つのウィジェットに対してこれらの既知のイベントを生成するには、以下のようにします:

  1. ウィジェットの名前はWidget${CamelName} にします。
    • 例:テストレポートのウィジェットはWidgetTestReports とします。
  2. ${CamelName} を小文字のスネークケースに変換することで、ウィジェット名のスラッグを計算します。
    • 前の例では、test_reports
  3. WIDGETS リストのlib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb に新しいウィジェット名 slug を追加します。
  4. GDK が実行されていることを確認してください (gdk start)。
  5. 以下のコマンドを使用して、コマンドラインで既知のイベントを生成します。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
    
  6. マージリクエストウィジェットの拡張子 telemetry の既存のファイルと一致するように、新しく生成された各ファイルを修正します。
    • のようにグロブ検索して既存の例を見つけます:metrics/**/*_i_code_review_merge_request_widget_*
    • 大まかに言えば、各ファイルは以下の値を持っているはずです:
      1. description = この値のわかりやすい英語の説明。既存のウィジェット拡張機能の遠隔測定ファイルの例をレビューしてください。
      2. product_section =dev
      3. product_stage =create
      4. product_group =code_review
      5. introduced_by_url ='[your MR]'
      6. options.events = (i_code_review_merge_request_widget_test_reports_count_view のように、このファイルを生成した上記のコマンドのイベント)
        • この値は、遠隔測定イベントが “メトリクス “にリンクされる方法なので、おそらくより重要な値の1つです。
      7. data_source =redis
      8. data_category =optional
  7. 以下のコマンドを使用して、コマンドラインで既知の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
    
  8. ステップ6を繰り返しますが、data_sourceredis_hll に変更します。

  9. 各イベント(ステップ7のコマンドでリストされたもので、test_reports を適切な名前のスラッグに置き換えたもの)を集約ファイルに追加します:
    1. config/metrics/counts_7d/{timestamp}_code_review_category_monthly_active_users.yml
    2. config/metrics/counts_7d/{timestamp}_code_review_group_monthly_active_users.yml
    3. config/metrics/counts_28d/{timestamp}_code_review_category_monthly_active_users.yml
    4. config/metrics/counts_28d/{timestamp}_code_review_group_monthly_active_users.yml

新しいイベントを追加

既知のイベントに新しいイベントを追加する場合は、lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rbKNOWN_EVENTS リストに新しいイベントを追加してください。

アイコン

レベル1とそれに続くすべてのレベルは、独自のステータスアイコンを持つことができます。デザインの枠組みを維持するために、constants.js ファイルからEXTENSION_ICONS 定数をインポートします:

import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants.js';

この定数には、使用可能な以下のアイコンがあります。この定数には、使用可能な以下のアイコンがあります。デザイン・フレームワークに従って、レベル 1 ではこれらのアイコンの一部のみを使用します:

  • failed
  • warning
  • success
  • neutral
  • error
  • notice
  • severityCritical
  • severityHigh
  • severityMedium
  • severityLow
  • severityInfo
  • severityUnknown

テキスト・スタイリング

テキストを含むすべての領域は、以下のプレースホルダでスタイル設定できます。このテクニックはsprintf. sprintfNET と同じテクニックにsprintf従いますが sprintf、.NETsprintfを通してこれらを指定する代わりに sprintf、エクステンションが自動的に行います。

すべてのプレースホルダは開始タグと終了タグを含みます。たとえば、successHello %{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() {}
}