Merge Request 위젯 확장

Merge Request 위젯의 확장은 디자인 프레임워크와 일치하는 새로운 기능을 추가할 수 있도록 합니다.
확장을 통해 많은 이점을 손쉽게 얻을 수 있습니다. 이러한 이점으로는 다음과 같은 것들이 있습니다.

  • 일관된 느낌과 모양.
  • 확장이 열린 경우 추적.
  • 성능을 위한 가상 스크롤링.

사용법

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

기본 객체 구조:

export default {
  name: '',       // Required: 위젯을 식별하는 데 도움이 되는 이름
  props: [],      // Required: 위젯 상태에서 전달된 props
  i18n: {         // Required: i18n 텍스트를 보관하는 객체
    label: '',    // Required: 툴팁과 aria-label에 사용됨
    loading: '',  // Required: 데이터를 불러올 때 표시되는 텍스트
  },
  expandEvent: '',      // Optional: 확장 콘텐츠를 추적하기 위한 RedisHLL 이벤트 이름
  enablePolling: false, // Optional: 확장에 데이터 폴링을 할지 여부
  modalComponent: null, // Optional: 모달에 사용할 컴포넌트
  telemetry: true,      // Optional: 확장을 위한 기본적인 텔레메트리 보고. 텔레메트리를 사용하지 않으려면 false로 설정
  computed: {
    summary(data) {},     // Required: 레벨 1 요약 텍스트
    statusIcon(data) {},  // Required: 레벨 1 상태 아이콘
    tertiaryButtons() {}, // Optional: 레벨 1 작업 버튼
    shouldCollapse(data) {}, // Optional: 위젯을 확장할지 여부를 결정하는 논리 추가
  },
  methods: {
    fetchCollapsedData(props) {}, // Required: 축소된 상태에 필요한 데이터 가져오기
    fetchFullData(props) {},      // Required: 전체 확장 콘텐츠용 데이터 가져오기
    fetchMultiData() {},          // Optional: `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 메서드에서 처리됩니다. 이 메서드는 props를 인수로 받기 때문에 상태에서 설정된 경로에 쉽게 액세스할 수 있습니다.

확장이 데이터를 설정하도록 허용하려면 이 메서드는 데이터를 반드시 반환해야 합니다. 특별한 포맷팅은 필요하지 않습니다. 확장이 이러한 데이터를받으면 collapsedData로 설정됩니다. 당신은 collapsedData에 어떠한 computed property 또는 메서드에서도 액세스할 수 있습니다.

사용자가 확장을 선택하면 fetchFullData 메서드가 호출됩니다. 이 메서드도 props를 인수로 받습니다. 그리고 이 메서드는 전체 데이터를 반드시 반환해야 합니다. 그러나 이 데이터는 데이터 구조 섹션에서 언급된 형식과 일치하도록 올바르게 포맷팅되어야 합니다.

기술적 부채

현재 일부 확장에서 데이터 가져오기가 분리되지 않은 경우가 있습니다. 모든 데이터가 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 뱃지 변형
  },
  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에 필요한 형식이 아닙니다. 폴맷팅이 필요합니다. fetchFullData가 호출될 때 데이터가 올바르게 포맷될 경우 summary computed property가 collapsedData에 의존할 수 있다면 데이터를 포맷하고 있을 때 로직을 작성할 수 있습니다.

export default {
  //...
  enablePolling: true
  methods: {
    fetchCollapsedData() {
      return axios.get(this.reportPath)
    },
    fetchFullData() {
      return Promise.resolve(this.prepareReports());
    },
    // 사용자 정의 메서드
    prepareReports() {
      // collapsedData에서 값들을 언팩
      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() {
      // collapsedData에서 값들을 언팩
      const { new_errors, existing_errors, resolved_errors } = this.collapsedData;
      
      // 데이터 포맷팅 수행
      
      return [...newErrors, ...existingErrors, ...resolvedErrors]
    }
  },
};

확장이 동시에 여러 엔드포인트에서 데이터를 폴링해야 하는 경우, fetchMultiData를 사용하여 함수 배열을 반환할 수 있습니다. 각 엔드포인트마다 새로운 poll 객체가 생성되어 별도로 폴링됩니다. 모든 엔드포인트가 해결되면 폴링이 멈추고 response.data의 배열을 인수로 setCollapsedData가 호출됩니다.

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가 정의되지 않은 경우 “로드 실패”
  • 발생한 오류는 Sentry에 전송되어 기록됩니다.

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

export default {
  //...
  i18n: {
    //...
    error: __('Your error text'),
  },
};

텔레메트리

위젯 확장 프레임워크의 기본 구현에는 몇 가지 텔레메트리 이벤트가 포함됩니다. 각 위젯은 다음을 보고합니다:

  • view: 화면에 렌더링될 때.
  • expand: 확장될 때.
  • full_report_clicked: (선택 사항) 전체 보고서를 보기 위해 클릭될 때.
  • Outcome (expand_success, expand_warning, 또는 expand_failed): 확장될 때 위젯의 상태와 관련된 세 가지 추가 이벤트 중 하나.

새 위젯 추가

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

note
EE 전용인 이벤트는 아래 셸 명령어 뒤에 --ee를 포함해야 합니다.

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

  1. 위젯은 Widget${CamelName}으로 명명해야 합니다.
    • 예: 테스트 보고서 위젯의 경우 WidgetTestReports여야 합니다.
  2. ${CamelName}을 lower snake case로 변환하여 위젯 이름 슬러그를 계산합니다.
    • 앞의 예제에서는 test_reports가 됩니다.
  3. lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb에 새 위젯 이름 슬러그를 추가합니다.
  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. 각 새로 생성된 파일을 기존의 Merge Request 위젯 확장 텔레메트리 파일과 일치하도록 수정합니다.
    • 예제 검토를 위해 현재 널리 존재하는 위젯 확장 텔레메트리 파일을 찾습니다. (예: 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).
        • 이 값은 텔레메트리 이벤트를 “메트릭”에 연결하는 방법이기 때문에 이것이 아마도 가장 중요한 값 중 하나일 것입니다.
      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. 각 이벤트(앞서 명령에서 나열된 이벤트를 각각 필요한 이름 슬러그로 대체하여)를 집계 파일에 추가합니다:
    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와 동일한 기술을 따릅니다. 다만, sprintf를 통해 이러한 사항을 지정하는 대신에 확장 프로그램이 이를 자동으로 수행합니다.

각 플레이스홀더에는 시작 및 종료 태그가 포함됩니다. 예를 들어 successHello %{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', // Optional
}

내부 작업 버튼의 경우, 다음 구조를 따르세요:

{
  text: '클릭하세요',
  onClick() {}
}