병합 요청 위젯 확장
- GitLab 13.6에서 도입되었습니다.
병합 요청 위젯의 확장은 디자인 프레임워크와 일치하는 새로운 기능을 병합 요청 위젯에 추가할 수 있게 해줍니다. 확장을 통해 많은 이점을 손쉽게 얻을 수 있는데요, 그중 몇 가지는 다음과 같습니다.
- 일관된 외관 및 느낌.
- 확장이 열릴 때 추적.
- 성능을 위한 가상 스크롤링.
사용 방법
확장을 사용하려면 먼저 새로운 확장 객체를 만들어서 확장에 렌더링할 데이터를 가져와야 합니다. 작동하는 예제는 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
메서드를 통해 가져와집니다. 성능이 떨어지지만 더 빠른 이터레이션이 가능합니다.
이를 처리하려면 fetchFullData
는 fetchCollapsedData
메서드 호출을 통해 설정된 데이터를 반환합니다. 이러한 경우에 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에서 필요로 하는 형식이 아닙니다. 따라서, 기본 컴포넌트에 축소된 데이터를 설정하기 전에 데이터를 형식화해야 합니다.
계산된 속성 summary
이 collapsedData
에 의존할 수 있는 경우, 데이터 형식을 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
객체가 생성되어 별도로 폴링됩니다. 모든 엔드포인트가 해결된 후 폴링이 중지되고, setCollapsedData
가 response.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
를 포함해야 합니다.
단일 위젯에 대해 이러한 알려진 이벤트를 생성하려면:
- 위젯은
Widget${CamelName}
으로 이름을 붙여야 합니다.- 예를 들어, 테스트 보고서에 대한 위젯은
WidgetTestReports
여야 합니다.
- 예를 들어, 테스트 보고서에 대한 위젯은
- 위젯 이름 slug은
${CamelName}
을 소문자, 스네이크 케이스로 변환하여 계산합니다.- 이전 예시는
test_reports
가 될 것입니다.
- 이전 예시는
- 새 위젯 이름 slug을
lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb
의WIDGETS
목록에 추가하세요. - GDK가 실행 중인지 확인하세요 (
gdk start
). -
다음 명령어를 사용하여 명령줄에서 알려진 이벤트를 생성하세요.
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
- 각 새로 생성된 파일을 수정하여 MR 위젯 확장 텔레메트리의 기존 파일과 일치하도록 만드세요.
- 예시를 찾으려면
metrics/**/*_i_code_review_merge_request_widget_*
과 같이 글로브 검색을 통해 기존 위젯 확장 텔레메트리 파일을 찾으세요. - 대강 말하면, 각 파일은 다음 값을 가져야 합니다:
-
description
= 이 값에 대한 일반적인 영어 설명. 예시를 보려면 기존 위젯 확장 텔레메트리 파일을 확인하세요. -
product_section
=dev
-
product_stage
=create
-
product_group
=code_review
-
introduced_by_url
='[당신의 MR]'
-
options.events
= (위의 명령어에서 생성된 이벤트, 예를 들어i_code_review_merge_request_widget_test_reports_count_view
) - 이 값은 텔레메트리 이벤트가 “메트릭”에 어떻게 연결되는지이므로 가장 중요한 값 중 하나일 것입니다. -
data_source
=redis
-
data_category
=optional
-
- 예시를 찾으려면
-
다음 명령어를 사용하여 알려진 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
-
단계 6을 반복하되,
data_source
를redis_hll
로 변경하세요. - 각 이벤트(이 명령어의 목록에 나열된 이벤트들,
test_reports
를 적절한 이름 slug로 변경하세요)를 다음 파일들에 추가하세요:config/metrics/counts_7d/{timestamp}_code_review_category_monthly_active_users.yml
config/metrics/counts_7d/{timestamp}_code_review_group_monthly_active_users.yml
config/metrics/counts_28d/{timestamp}_code_review_category_monthly_active_users.yml
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() {}
}