병합 요청 위젯 확장

병합 요청 위젯의 확장은 디자인 프레임워크와 일치하는 새로운 기능을 병합 요청 위젯에 추가할 수 있게 해줍니다. 확장을 통해 많은 이점을 손쉽게 얻을 수 있는데요, 그중 몇 가지는 다음과 같습니다.

  • 일관된 외관 및 느낌.
  • 확장이 열릴 때 추적.
  • 성능을 위한 가상 스크롤링.

사용 방법

확장을 사용하려면 먼저 새로운 확장 객체를 만들어서 확장에 렌더링할 데이터를 가져와야 합니다. 작동하는 예제는 app/assets/javascripts/vue_merge_request_widget/extensions/issues.js의 예제 파일을 참조하세요.

기본 객체 구조:

export default {
  name: '',       // 필수: 위젯을 식별하는 데 도움이 되는 이름
  props: [],      // 필수: 위젯 상태에서 전달된 프롭
  i18n: {         // 필수: i18n 텍스트를 보유하는 객체
    label: '',    // 필수: 툴팁 및 aria-label에 사용
    loading: '',  // 필수: 데이터 로딩 중 텍스트
  },
  expandEvent: '',      // 선택 사항: 확장 콘텐츠를 추적하는 RedisHLL 이벤트 이름
  enablePolling: false, // 선택 사항: 확장이 데이터를 폴링하도록 지시함
  modalComponent: null, // 선택 사항: 모달에 사용할 컴포넌트
  telemetry: true,      // 선택 사항: 확장의 기본 텔레메트리 보고. 텔레메트리를 비활성화하려면 false로 설정
  computed: {
    summary(data) {},     // 필수: 레벨 1 요약 텍스트
    statusIcon(data) {},  // 필수: 레벨 1 상태 아이콘
    tertiaryButtons() {}, // 선택 사항: 레벨 1 작업 버튼
    shouldCollapse(data) {}, // 선택 사항: 위젯이 확장될 수 있는지 여부를 결정하는 로직 추가
  },
  methods: {
    fetchCollapsedData(props) {}, // 필수: 축소된 상태에 필요한 데이터를 가져옴
    fetchFullData(props) {},      // 필수: 전체 확장 콘텐츠용 데이터를 가져옴
    fetchMultiData() {},          // 선택 사항: `enablePolling`과 함께 작동하며 여러 엔드포인트를 폴링할 수 있음
  },
};

동일한 데이터 구조를 따르면 각 확장이 동일한 등록 구조를 따르지만, 각 확장은 자체 데이터 소스를 관리할 수 있습니다.

이 구조를 따라 만든 후에는 등록해야 합니다. 위젯이 생성된 이후 아무 때나 확장을 등록할 수 있습니다. 확장을 등록하려면:

// 등록 메서드를 가져옴
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';

// 새로운 확장을 가져옴
import issueExtension from '~/vue_merge_request_widget/extensions/issues';

// 가져온 확장을 등록
registerExtension(issueExtension);

데이터 가져오기

각 확장은 데이터를 가져와야 합니다. 가져오기는 핵심 컴포넌트 자체가 아닌 확장을 등록할 때 처리됩니다. 이 접근 방식은 GraphQL 또는 REST API 호출과 같은 다양한 데이터 가져오기 방법을 사용할 수 있도록 합니다.

API 호출

성능상의 이유로 축소된 상태에서는 축소된 상태를 렌더링하는 데 필요한 데이터만 가져오는 것이 가장 좋습니다. 이 가져오기는 fetchCollapsedData 메서드에서 발생합니다. 이 메서드는 프롭과 함께 호출되므로 상태에서 설정된 경로에 쉽게 액세스할 수 있습니다.

확장이 데이터를 설정할 수 있도록 이 메서드는 데이터를 반드시 반환해야 합니다. 특별한 형식으로 포맷팅할 필요는 없습니다. 확장이 이 데이터를 받으면 이를 collapsedData로 설정합니다. 이를 computed 속성이나 메서드에서 collapsedData에 액세스할 수 있습니다.

사용자가 확장을 선택하면 fetchFullData 메서드가 호출됩니다. 이 메서드도 프롭과 함께 호출됩니다. 그러나 이 데이터를 포맷팅하는데 필요한 전체 데이터도 반드시 반환해야 합니다. 그러나 이 데이터는 데이터 구조 섹션에서 언급된 형식과 일치하도록 올바르게 포맷팅해야 합니다.

기술적 부채

현재 일부 확장에서 데이터 가져오기를 분할하지 않는 경우가 있습니다. 모든 데이터가 fetchCollapsedData 메서드를 통해 가져와집니다. 성능이 떨어지지만 더 빠른 이터레이션이 가능합니다.

이를 처리하려면 fetchFullDatafetchCollapsedData 메서드 호출을 통해 설정된 데이터를 반환합니다. 이러한 경우에 fetchFullData는 프라미스를 반환해야 합니다.

fetchCollapsedData() {
  return ['일부 데이터'];
},
fetchFullData() {
  return Promise.resolve(this.collapsedData)
},

데이터 구조

fetchFullData에서 반환된 데이터는 아래 형식과 일치해야 합니다. 이 형식을 사용하면 핵심 컴포넌트가 디자인 프레임워크와 일치하는 방식으로 데이터를 렌더링할 수 있습니다. 텍스트 속성은 아래에서 언급된 스타일링 플레이스홀더를 사용할 수 있습니다.

{
  id: data.id,    // 필수: 각 행의 키로 사용되는 ID
  header: '헤더' || ['헤더', '서브 헤더'], // 필수: 헤더 텍스트에는 문자열 또는 배열을 사용할 수 있음
  text: '',       // 필수: 행의 주된 텍스트
  subtext: '',    // 선택 사항: 주된 텍스트 아래에 표시되는 보다 작은 서브 텍스트
  icon: {         // 선택 사항: 아이콘 객체
    name: EXTENSION_ICONS.success, // 필수: 행의 아이콘 이름
  },
  badge: {        // 선택 사항: 텍스트 뒤에 표시되는 배지
    text: '',     // 필수: 배지 내에 표시할 텍스트
    variant: '',  // 선택 사항: GitLab UI 배지 변형, 기본값은 info
  },
  link: {         // 선택 사항: 텍스트 뒤에 표시되는 URL로 이동하는 링크
    text: '',     // 필수: 링크의 텍스트
    href: '',     // 선택 사항: 링크의 URL
  },
  modal: {        // 선택 사항: 텍스트 뒤에 열리는 모달에 대한 링크
    text: '',     // 필수: 링크의 텍스트
    onClick: () => {} // 선택 사항: 링크를 클릭할 때 실행할 함수, 즉, 이.modalData를 설정하는 데 사용됨
  }
  actions: [],    // 선택 사항: 행에 대한 작업 버튼
  children: [],   // 선택 사항: 렌더링할 자식 콘텐츠, 구조가 동일한 구조와 일치함
}

폴링

확장 기능에서 폴링을 가능하게 하려면, 확장 기능에 옵션 플래그가 있어야 합니다:

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());
    },
    // 사용자 정의 메서드
    prepareReports() {
      // 축소된 데이터에서 값들을 해체
      const { new_errors, existing_errors, resolved_errors } = this.collapsedData;

      // 데이터 형식 수행

      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,
        }
      })
    },
    // 사용자 정의 메서드
    prepareReports() {
      // 축소된 데이터에서 값들을 해체
      const { new_errors, existing_errors, resolved_errors } = this.collapsedData;

      // 데이터 형식 수행

      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)
    },
  },
};

경고: 함수는 Promise를 반환하여 응답 객체를 해결해야 합니다. 구현은 폴링을 유지하기 위해 POLL-INTERVAL 헤더에 의존하기 때문에 상태 코드와 헤더를 변경하지 않는 것이 중요합니다.

오류

만약 fetchCollapsedData() 또는 fetchFullData() 메서드에서 오류가 발생한다면:

  • fetchCollapsedData() 메서드가 오류를 던지면, 확장 기능의 로딩 상태가 LOADING_STATES.collapsedError로 업데이트됩니다.
  • fetchFullData() 메서드가 오류를 던지면, 확장 기능의 로딩 상태가 LOADING_STATES.expandedError로 업데이트됩니다.
  • 확장 기능의 헤더가 오류 아이콘을 표시하고 텍스트가 다음 중 하나로 업데이트됩니다:
    • $options.i18n.error에서 정의된 텍스트.
    • $options.i18n.error가 정의되지 않았다면, “로드 실패”로 업데이트됩니다.
  • 발생한 오류는 발생했음을 기록하기 위해 Sentry로 전송됩니다.

오류 텍스트를 사용자 정의하려면, 확장에서 i18n 객체에 추가하세요:

export default {
  //...
  i18n: {
    //...
    error: __('여러분의 오류 텍스트'),
  },
};

텔레메트리

위젯 확장 프레임워크의 기본 구현에는 일부 텔레메트리 이벤트가 포함되어 있습니다. 각 위젯이 다음과 같은 것들을 보고합니다:

  • view: 화면에 렌더링될 때.
  • expand: 확장될 때.
  • full_report_clicked: 전체 보고서를 보기 위해 (옵션으로) 입력이 클릭될 때.
  • 결과 (expand_success, expand_warning, 또는 expand_failed): 확장될 때 위젯 상태와 관련된 세 가지 추가 이벤트 중 하나.

새로운 위젯 추가

새로운 위젯을 추가할 때, 위의 이벤트들은 known로 표시되고 보고 가능하도록 메트릭이 생성되어야 합니다.

참고: EE 전용 이벤트는 아래 셸 명령어 두 개 뒤에 --ee를 포함해야 합니다.

단일 위젯에 대해 이러한 알려진 이벤트를 생성하려면:

  1. 위젯은 Widget${CamelName}으로 이름을 붙여야 합니다.
    • 예를 들어, 테스트 보고서에 대한 위젯은 WidgetTestReports여야 합니다.
  2. 위젯 이름 slug은 ${CamelName}을 소문자, 스네이크 케이스로 변환하여 계산합니다.
    • 이전 예시는 test_reports가 될 것입니다.
  3. 새 위젯 이름 slug을 lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rbWIDGETS 목록에 추가하세요.
  4. GDK가 실행 중인지 확인하세요 (gdk start).
  5. 다음 명령어를 사용하여 명령줄에서 알려진 이벤트를 생성하세요. test_reports를 적절한 이름 slug로 바꿔주세요:

    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. 각 새로 생성된 파일을 수정하여 MR 위젯 확장 텔레메트리의 기존 파일과 일치하도록 만드세요.
    • 예시를 찾으려면 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 = '[당신의 MR]'
      6. options.events = (위의 명령어에서 생성된 이벤트, 예를 들어 i_code_review_merge_request_widget_test_reports_count_view) - 이 값은 텔레메트리 이벤트가 “메트릭”에 어떻게 연결되는지이므로 가장 중요한 값 중 하나일 것입니다.
      7. data_source = redis
      8. data_category = optional
  7. 다음 명령어를 사용하여 알려진 HLL 이벤트를 생성하세요. 이 때 test_reports를 적절한 이름 slug로 변경하세요.

    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. 각 이벤트(이 명령어의 목록에 나열된 이벤트들, test_reports를 적절한 이름 slug로 변경하세요)를 다음 파일들에 추가하세요:
    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.rb 파일의 KNOWN_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와 동일한 기술을 따릅니다. 그러나 sprintf를 통해 지정하는 대신, 확장은 자동으로 이 작업을 수행합니다.

각 플레이스홀더에는 시작 태그와 종료 태그가 포함되어 있습니다. 예를 들어, success안녕하세요 %{success_start}world%{success_end}를 사용합니다. 그런 다음 확장은 올바른 스타일 클래스로 시작 및 종료 태그를 추가합니다.

플레이스홀더 스타일
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

작업 버튼

각 확장의 레벨 1 및 2에 작업 버튼을 추가할 수 있습니다. 이러한 버튼은 각 행에 대한 링크 또는 작업을 제공하는 방법으로 사용됩니다:

  • 레벨 1의 작업 버튼은 tertiaryButtons 계산된 속성을 통해 설정할 수 있습니다. 이 속성은 각 작업 버튼에 대한 객체 배열을 반환해야 합니다.
  • 레벨 2의 작업 버튼은 레벨 2 행 객체에 actions 키를 추가함으로써 설정할 수 있습니다. 이 키의 값 또한 각 작업 버튼에 대한 객체 배열이어야 합니다.

링크는 다음 구조를 따라야 합니다:

{
  text: '클릭',
  href: this.someLinkHref,
  target: '_blank', // 선택 사항
}

내부 작업 버튼의 경우, 다음 구조를 따르십시오:

{
  text: '클릭',
  onClick() {}
}