Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-12-02 15:32:14 +00:00
parent 0545a43e37
commit f690cfa8c6
99 changed files with 1357 additions and 680 deletions

View File

@ -6,7 +6,7 @@ workflow:
include:
- local: .gitlab/ci/version.yml
- component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@9.6.2"
- component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@9.6.3"
inputs:
job_name: "e2e-test-report"
job_stage: "report"
@ -16,7 +16,7 @@ include:
gitlab_auth_token_variable_name: "PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE"
allure_job_name: "${QA_RUN_TYPE}"
- project: gitlab-org/quality/pipeline-common
ref: 9.6.2
ref: 9.6.3
file:
- /ci/base.gitlab-ci.yml
- /ci/knapsack-report.yml

View File

@ -18,4 +18,4 @@ variables:
# Retry failed specs in separate process
QA_RETRY_FAILED_SPECS: "true"
# helm chart ref used by test-on-cng pipeline
GITLAB_HELM_CHART_REF: "04b75b6610e7f1e4a013bef4964563d0219cbd72"
GITLAB_HELM_CHART_REF: "37f35b47e7762d1c4b229ed7f00579666d9c0d9c"

View File

@ -1,101 +0,0 @@
<svg width="280" height="140" viewBox="0 0 280 140" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_187_122567)">
<g clip-path="url(#clip1_187_122567)">
<circle cx="189.5" cy="-42.5" r="131.5" fill="url(#paint0_radial_187_122567)"/>
<circle cx="-41.5" cy="-97.5" r="198.5" fill="url(#paint1_radial_187_122567)"/>
<circle cx="309.5" cy="-7.5" r="121.5" fill="url(#paint2_radial_187_122567)"/>
<g filter="url(#filter0_b_187_122567)">
<path d="M0 4C0 1.79086 1.79086 0 4 0H276C278.209 0 280 1.79086 280 4V130H0V4Z" fill="white" fill-opacity="0.01"/>
</g>
</g>
<path d="M183.948 47.9647H100.897V100.482H183.948V47.9647Z" fill="white"/>
<path d="M100.64 47.9647H184.06V98.9817C184.06 104.1 179.908 108.256 174.793 108.256H100.638V47.9647H100.64Z" fill="white"/>
<path d="M184.314 34.7452H100.64V47.9676H184.314V34.7452Z" fill="#AEA5D6"/>
<path d="M109.594 43.2574C110.644 43.2574 111.495 42.4056 111.495 41.3549C111.495 40.3043 110.644 39.4525 109.594 39.4525C108.544 39.4525 107.693 40.3043 107.693 41.3549C107.693 42.4056 108.544 43.2574 109.594 43.2574Z" fill="#10B1B1"/>
<path d="M116.482 43.2574C117.532 43.2574 118.383 42.4056 118.383 41.3549C118.383 40.3043 117.532 39.4525 116.482 39.4525C115.432 39.4525 114.581 40.3043 114.581 41.3549C114.581 42.4056 115.432 43.2574 116.482 43.2574Z" fill="#A888F4"/>
<path d="M123.368 43.2574C124.418 43.2574 125.269 42.4056 125.269 41.3549C125.269 40.3043 124.418 39.4525 123.368 39.4525C122.318 39.4525 121.467 40.3043 121.467 41.3549C121.467 42.4056 122.318 43.2574 123.368 43.2574Z" fill="#FF9D73"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M100.556 34.4038H165.377V35.0858H101.238V61.7159H100.556V34.4038Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M101.238 66.3383V83.4486H100.556V66.3383H101.238Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M183.721 99.0231V89.5341H184.403V99.0231C184.403 104.311 180.118 108.599 174.834 108.599H120.244V107.917H174.834C179.741 107.917 183.721 103.935 183.721 99.0231Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M183.721 83.1296V62.2422H184.403V83.1296H183.721Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M109.596 39.7936C108.734 39.7936 108.036 40.4924 108.036 41.355C108.036 42.2176 108.734 42.9164 109.596 42.9164C110.457 42.9164 111.155 42.2176 111.155 41.355C111.155 40.4924 110.457 39.7936 109.596 39.7936ZM107.354 41.355C107.354 40.1162 108.357 39.1116 109.596 39.1116C110.834 39.1116 111.837 40.1162 111.837 41.355C111.837 42.5938 110.834 43.5985 109.596 43.5985C108.357 43.5985 107.354 42.5938 107.354 41.355Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M116.48 39.7936C115.619 39.7936 114.92 40.4924 114.92 41.355C114.92 42.2176 115.619 42.9164 116.48 42.9164C117.342 42.9164 118.04 42.2176 118.04 41.355C118.04 40.4924 117.342 39.7936 116.48 39.7936ZM114.238 41.355C114.238 40.1162 115.242 39.1116 116.48 39.1116C117.719 39.1116 118.722 40.1162 118.722 41.355C118.722 42.5938 117.719 43.5985 116.48 43.5985C115.242 43.5985 114.238 42.5938 114.238 41.355Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M123.367 39.7936C122.506 39.7936 121.808 40.4924 121.808 41.355C121.808 42.2176 122.506 42.9164 123.367 42.9164C124.229 42.9164 124.927 42.2176 124.927 41.355C124.927 40.4924 124.229 39.7936 123.367 39.7936ZM121.125 41.355C121.125 40.1162 122.129 39.1116 123.367 39.1116C124.606 39.1116 125.609 40.1162 125.609 41.355C125.609 42.5938 124.606 43.5985 123.367 43.5985C122.129 43.5985 121.125 42.5938 121.125 41.355Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M160.466 48.3058H100.897V47.6238H160.466V48.3058Z" fill="#171321"/>
<path d="M173.511 80.7124H152.485V83.4598H173.511V80.7124Z" fill="#D0C5E2"/>
<path d="M149.08 80.7009H111.703V83.4484H149.08V80.7009Z" fill="#E7E4F2"/>
<path d="M111.702 68.5293H132.728V65.7819H111.702V68.5293Z" fill="#E7E4F2"/>
<path d="M136.131 68.5336H173.508V65.7861H136.131V68.5336Z" fill="#AEA5D6"/>
<path d="M111.703 75.9889H120.838V73.2415H111.703V75.9889Z" fill="#AEA5D6"/>
<path d="M155.891 75.9978H173.512V73.2504H155.891V75.9978Z" fill="#E7E4F2"/>
<path d="M124.244 75.9978H152.485V73.2504H124.244V75.9978Z" fill="#D0C5E2"/>
<path d="M141.099 58.3217H124.332V61.0691H141.099V58.3217Z" fill="#D0C5E2"/>
<path d="M173.512 58.3217H144.31V61.0691H173.512V58.3217Z" fill="#E7E4F2"/>
<path d="M120.926 58.3198H111.703V61.0673H120.926V58.3198Z" fill="#AEA5D6"/>
<path d="M144.115 90.9242H160.882V88.1768H144.115V90.9242Z" fill="#AEA5D6"/>
<path d="M111.703 90.908H140.905V88.1605H111.703V90.908Z" fill="#D0C5E2"/>
<path d="M164.288 90.9242H173.512V88.1768H164.288V90.9242Z" fill="#D0C5E2"/>
<path d="M173.508 95.6178H158.224V98.3652H173.508V95.6178Z" fill="#D0C5E2"/>
<path d="M154.82 95.6199H111.703V98.3673H154.82V95.6199Z" fill="#E7E4F2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M167.801 34.8965L168.463 35.0598L162.398 59.6461L189.091 57.5112L189.145 58.1911L161.509 60.4014L167.801 34.8965Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M134.795 81.6335L109.428 116.595L108.876 116.195L133.312 82.5167L92.5055 87.8873L92.4165 87.2111L134.795 81.6335Z" fill="#171321"/>
<path d="M187.019 57.9366C197.646 57.9366 206.262 49.3147 206.262 38.6791C206.262 28.0435 197.646 19.4216 187.019 19.4216C176.392 19.4216 167.777 28.0435 167.777 38.6791C167.777 49.3147 176.392 57.9366 187.019 57.9366Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M187.019 19.6948C176.543 19.6948 168.05 28.1943 168.05 38.6794C168.05 49.1646 176.543 57.6641 187.019 57.6641C197.495 57.6641 205.989 49.1646 205.989 38.6794C205.989 28.1943 197.495 19.6948 187.019 19.6948ZM167.504 38.6794C167.504 27.8934 176.241 19.1492 187.019 19.1492C197.797 19.1492 206.534 27.8934 206.534 38.6794C206.534 49.4655 197.797 58.2097 187.019 58.2097C176.241 58.2097 167.504 49.4655 167.504 38.6794Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M212.255 38.6791C212.255 24.7294 200.956 13.4218 187.018 13.4218V12.677C201.368 12.677 213 24.3186 213 38.6791H212.255Z" fill="#AEA5D6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M67.0944 108.321C67.0944 122.27 78.3936 133.578 92.3316 133.578V134.323C77.9817 134.323 66.3496 122.681 66.3496 108.321H67.0944Z" fill="#AEA5D6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M181.794 28.4035C183.212 28.0221 184.673 28.8643 185.054 30.2837L184.395 30.4605C184.112 29.4049 183.026 28.7787 181.971 29.0622L181.794 28.4035Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M192.066 29.0622C191.011 28.7787 189.925 29.4049 189.642 30.4605L188.983 30.2837C189.364 28.8643 190.824 28.0221 192.243 28.4035L192.066 29.0622Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M195.892 33.5398V36.1862L192.04 37.4786L191.823 36.832L195.21 35.6956V33.5398H195.892Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M195.213 44.8355L192.045 43.7774L192.262 43.1305L195.895 44.3442V46.991H195.213V44.8355Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M178.827 35.6957V33.5398H178.145V36.1861L181.995 37.4786L182.212 36.832L178.827 35.6957Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M181.776 43.1305L181.992 43.7773L178.827 44.8354V46.991H178.145V44.3443L181.776 43.1305Z" fill="#171321"/>
<path d="M182.675 34.3942L181.129 37.5941C179.03 41.9415 182.192 46.9908 187.019 46.9908C191.845 46.9908 195.008 41.9415 192.908 37.5941L191.363 34.3942" fill="#A888F4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M186.678 46.9908V34.3942H187.36V46.9908H186.678Z" fill="#171321"/>
<path d="M191.25 34.3939H182.788V33.2534C182.788 28.2918 191.25 28.2987 191.25 33.2534V34.3939Z" fill="#7759C1"/>
<path d="M92.4612 126.064C103.088 126.064 111.704 117.442 111.704 106.806C111.704 96.1706 103.088 87.5487 92.4612 87.5487C81.8339 87.5487 73.2188 96.1706 73.2188 106.806C73.2188 117.442 81.8339 126.064 92.4612 126.064Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M92.4604 87.89C82.0217 87.89 73.559 96.359 73.559 106.806C73.559 117.254 82.0217 125.723 92.4604 125.723C102.899 125.723 111.362 117.254 111.362 106.806C111.362 96.359 102.899 87.89 92.4604 87.89ZM72.877 106.806C72.877 95.9828 81.6445 87.208 92.4604 87.208C103.276 87.208 112.044 95.9828 112.044 106.806C112.044 117.63 103.276 126.405 92.4604 126.405C81.6445 126.405 72.877 117.63 72.877 106.806Z" fill="#171321"/>
<path d="M98.2885 105.28H95.9653V101.385C95.9653 99.3302 94.2951 97.6587 92.2419 97.6587C90.1887 97.6587 88.5184 99.3302 88.5184 101.385V105.28H86.1953V101.385C86.1953 98.0489 88.9083 95.3337 92.2419 95.3337C95.5754 95.3337 98.2885 98.0489 98.2885 101.385V105.28Z" fill="#7759C1"/>
<path d="M100.894 104.186H83.3677V116.836H100.894V104.186Z" fill="#A888F4"/>
<path d="M92.1343 111.015C92.8235 111.015 93.3823 110.456 93.3823 109.766C93.3823 109.076 92.8235 108.517 92.1343 108.517C91.445 108.517 90.8862 109.076 90.8862 109.766C90.8862 110.456 91.445 111.015 92.1343 111.015Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M91.792 112.209V110.722H92.474V112.209H91.792Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M87.2401 53.2567V60.9103C87.2401 61.9966 88.1212 62.878 89.2061 62.878H121.061C122.523 62.878 123.709 64.0653 123.709 65.5278V67.1601H123.027V65.5278C123.027 64.4415 122.146 63.5601 121.061 63.5601H89.2061C87.7441 63.5601 86.5581 62.3728 86.5581 60.9103V53.2567H87.2401Z" fill="#171321"/>
<path d="M123.368 67.735C123.685 67.735 123.942 67.4776 123.942 67.1601C123.942 66.8426 123.685 66.5852 123.368 66.5852C123.051 66.5852 122.793 66.8426 122.793 67.1601C122.793 67.4776 123.051 67.735 123.368 67.735Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M123.368 66.9262C123.239 66.9262 123.134 67.0306 123.134 67.16C123.134 67.2895 123.239 67.3939 123.368 67.3939C123.496 67.3939 123.601 67.2895 123.601 67.16C123.601 67.0306 123.496 66.9262 123.368 66.9262ZM122.452 67.16C122.452 66.6545 122.862 66.2441 123.368 66.2441C123.873 66.2441 124.283 66.6545 124.283 67.16C124.283 67.6656 123.873 68.0759 123.368 68.0759C122.862 68.0759 122.452 67.6656 122.452 67.16Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M167.208 82.5551V84.88C167.208 85.5854 167.778 86.1551 168.482 86.1551H193.305V86.8371H168.482C167.4 86.8371 166.526 85.9616 166.526 84.88V82.5551H167.208Z" fill="#171321"/>
<path d="M166.867 83.13C167.184 83.13 167.441 82.8726 167.441 82.5551C167.441 82.2376 167.184 81.9802 166.867 81.9802C166.55 81.9802 166.292 82.2376 166.292 82.5551C166.292 82.8726 166.55 83.13 166.867 83.13Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.866 82.3209C166.737 82.3209 166.633 82.4254 166.633 82.5548C166.633 82.6842 166.737 82.7887 166.866 82.7887C166.995 82.7887 167.1 82.6842 167.1 82.5548C167.1 82.4255 166.995 82.3209 166.866 82.3209ZM165.951 82.5548C165.951 82.0492 166.36 81.6389 166.866 81.6389C167.372 81.6389 167.782 82.0492 167.782 82.5548C167.782 83.0605 167.372 83.4707 166.866 83.4707C166.36 83.4707 165.951 83.0604 165.951 82.5548Z" fill="#171321"/>
<path d="M86.9023 37.1553C82.8536 41.5743 77.6099 40.6 77.6099 40.6V48.5768C77.6099 49.962 77.8867 51.3404 78.4796 52.5917C80.8973 57.6918 86.9023 59.5873 86.9023 59.5873C86.9023 59.5873 92.9051 57.6918 95.3251 52.5917C95.918 51.3404 96.1948 49.962 96.1948 48.5768V40.6C96.1948 40.6 90.9511 41.5743 86.9023 37.1553V37.1553Z" fill="#10B1B1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M90.6411 45.6638L85.4674 50.8415L83.1592 48.5289L83.6419 48.0471L85.4677 49.8764L90.1586 45.1818L90.6411 45.6638Z" fill="#171321"/>
<path d="M197.955 76.3168C193.902 80.7427 188.651 79.7684 188.651 79.7684V87.7567C188.651 89.1443 188.928 90.5249 189.521 91.7786C191.943 96.8856 197.955 98.7834 197.955 98.7834C197.955 98.7834 203.967 96.8856 206.39 91.7786C206.985 90.5249 207.259 89.1443 207.259 87.7567V79.7684C207.259 79.7684 202.009 80.7427 197.955 76.3168Z" fill="#FC6D26"/>
<path d="M197.19 83.1044L197.566 88.4584H198.343L198.719 83.1044H197.19Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M196.806 82.7468H199.102L198.676 88.8156H197.232L196.806 82.7468ZM197.572 83.4616L197.898 88.1009H198.009L198.335 83.4616H197.572Z" fill="#171321"/>
<path d="M198.343 90.2083H197.566V91.1248H198.343V90.2083Z" fill="#171321"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M197.208 89.851H198.7V91.4823H197.208V89.851ZM197.922 90.5657V90.7675H197.985V90.5657H197.922Z" fill="#171321"/>
</g>
<defs>
<filter id="filter0_b_187_122567" x="-50" y="-50" width="380" height="230" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feGaussianBlur in="BackgroundImageFix" stdDeviation="25"/>
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_187_122567"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_187_122567" result="shape"/>
</filter>
<radialGradient id="paint0_radial_187_122567" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(189.5 -42.5) rotate(89.5818) scale(125.986)">
<stop stop-color="#7759C2"/>
<stop offset="1" stop-color="#7759C2" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint1_radial_187_122567" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(-41.5 -97.5) rotate(89.5818) scale(190.176)">
<stop stop-color="#D64028"/>
<stop offset="1" stop-color="#D64028" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint2_radial_187_122567" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(309.5 -7.5) rotate(89.5818) scale(116.405)">
<stop stop-color="#EF76F1"/>
<stop offset="1" stop-color="#EF76F1" stop-opacity="0"/>
</radialGradient>
<clipPath id="clip0_187_122567">
<rect width="280" height="140" fill="white"/>
</clipPath>
<clipPath id="clip1_187_122567">
<path d="M0 4C0 1.79086 1.79086 0 4 0H276C278.209 0 280 1.79086 280 4V140H0V4Z" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -18,7 +18,7 @@ export default ({ el, router }) => {
const { projectPath, ref, isBlob, webIdeUrl, ...options } = convertObjectPropsToCamelCase(
JSON.parse(el.dataset.options),
);
const { webIdePromoPopoverImg, cssClasses, defaultBranch } = el.dataset;
const { cssClasses, defaultBranch } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
@ -33,7 +33,6 @@ export default ({ el, router }) => {
return h(WebIdeButton, {
props: {
isBlob,
webIdePromoPopoverImg,
webIdeUrl: isBlob
? webIdeUrl
: webIDEUrl(

View File

@ -74,7 +74,13 @@ export default {
setSelected({ data }) {
if (!data) return;
this.selected = data[ACCESS_LEVELS.CREATE].map(
({ id, user_id: userId, group_id: groupId, access_level: accessLevel }) => {
({
id,
user_id: userId,
group_id: groupId,
access_level: accessLevel,
deploy_key_id: deployKeyId,
}) => {
if (userId) {
return {
id,
@ -91,6 +97,14 @@ export default {
};
}
if (deployKeyId) {
return {
id,
deploy_key_id: deployKeyId,
type: LEVEL_TYPES.DEPLOY_KEY,
};
}
return {
id,
access_level: accessLevel,

View File

@ -7,11 +7,15 @@ import { keysFor, START_SEARCH_PROJECT_FILE } from '~/behaviors/shortcuts/keybin
import { sanitize } from '~/lib/dompurify';
import { InternalEvents } from '~/tracking';
import { FIND_FILE_BUTTON_CLICK } from '~/tracking/constants';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import { visitUrl, joinPaths, webIDEUrl } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { generateRefDestinationPath } from '~/repository/utils/ref_switcher_utils';
import RefSelector from '~/ref/components/ref_selector.vue';
import Breadcrumbs from '~/repository/components/header_area/breadcrumbs.vue';
import BlobControls from '~/repository/components/header_area/blob_controls.vue';
import CodeDropdown from '~/vue_shared/components/code_dropdown/code_dropdown.vue';
import SourceCodeDownloadDropdown from '~/vue_shared/components/download_dropdown/download_dropdown.vue';
import CloneCodeDropdown from '~/vue_shared/components/code_dropdown/clone_code_dropdown.vue';
export default {
name: 'HeaderArea',
@ -24,6 +28,10 @@ export default {
RefSelector,
Breadcrumbs,
BlobControls,
CodeDropdown,
SourceCodeDownloadDropdown,
CloneCodeDropdown,
WebIdeLink: () => import('ee_else_ce/vue_shared/components/web_ide_link.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
@ -45,6 +53,26 @@ export default {
'projectRootPath',
'comparePath',
'isReadmeView',
'isFork',
'needsToFork',
'gitpodEnabled',
'isBlob',
'showEditButton',
'showWebIdeButton',
'showGitpodButton',
'showPipelineEditorUrl',
'webIdeUrl',
'editUrl',
'pipelineEditorUrl',
'gitpodUrl',
'userPreferencesGitpodPath',
'userProfileEnableGitpodPath',
'httpUrl',
'xcodeUrl',
'sshUrl',
'kerberosUrl',
'downloadLinks',
'downloadArtifacts',
],
props: {
projectPath: {
@ -84,6 +112,24 @@ export default {
refSelectorValue() {
return this.refType ? joinPaths('refs', this.refType, this.currentRef) : this.currentRef;
},
webIDEUrl() {
return this.isBlob
? this.webIdeUrl
: webIDEUrl(
joinPaths(
'/',
this.projectPath,
'edit',
this.currentRef,
'-',
this.$route?.params.path || '',
'/',
),
);
},
projectIdAsNumber() {
return getIdFromGraphQLId(this.projectId);
},
findFileTooltip() {
const { description } = START_SEARCH_PROJECT_FILE;
const key = this.findFileShortcutKey;
@ -108,7 +154,7 @@ export default {
</script>
<template>
<section class="nav-block gl-flex gl-flex-col gl-items-stretch sm:gl-flex-row">
<section class="nav-block gl-flex gl-flex-col gl-items-stretch sm:gl-flex-row sm:gl-items-center">
<div class="tree-ref-container mb-2 mb-md-0 gl-flex gl-flex-wrap gl-gap-2">
<ref-selector
v-if="!isReadmeView"
@ -155,13 +201,58 @@ export default {
v-gl-tooltip.html="findFileTooltip"
:aria-keyshortcuts="findFileShortcutKey"
data-testid="tree-find-file-control"
class="gl-mt-3 gl-w-full sm:gl-mt-0 sm:gl-w-auto"
class="gl-w-full sm:gl-w-auto"
@click="handleFindFile"
>
{{ $options.i18n.findFile }}
</gl-button>
<!-- web ide -->
<web-ide-link
class="gl-w-full sm:!gl-ml-0 sm:gl-w-auto"
data-testid="js-tree-web-ide-link"
:project-id="projectIdAsNumber"
:project-path="projectPath"
:is-fork="isFork"
:needs-to-fork="needsToFork"
:gitpod-enabled="gitpodEnabled"
:is-blob="isBlob"
:show-edit-button="showEditButton"
:show-web-ide-button="showWebIdeButton"
:show-gitpod-button="showGitpodButton"
:show-pipeline-editor-url="showPipelineEditorUrl"
:web-ide-url="webIDEUrl"
:edit-url="editUrl"
:pipeline-editor-url="pipelineEditorUrl"
:gitpod-url="gitpodUrl"
:user-preferences-gitpod-path="userPreferencesGitpodPath"
:user-profile-enable-gitpod-path="userProfileEnableGitpodPath"
disable-fork-modal
v-on="$listeners"
/>
<!-- code + mobile panel -->
<div v-if="!isReadmeView" class="project-code-holder gl-w-full sm:gl-w-auto">
<code-dropdown
class="git-clone-holder js-git-clone-holder gl-hidden sm:gl-inline-block"
:ssh-url="sshUrl"
:http-url="httpUrl"
:kerberos-url="kerberosUrl"
:xcode-url="xcodeUrl"
:current-path="currentPath"
:directory-download-links="downloadLinks"
/>
<div class="gl-flex gl-items-stretch gl-gap-3 sm:gl-hidden">
<source-code-download-dropdown
:download-links="downloadLinks"
:download-artifacts="downloadArtifacts"
/>
<clone-code-dropdown
class="git-clone-holder js-git-clone-holder !gl-w-full"
:ssh-url="sshUrl"
:http-url="httpUrl"
:kerberos-url="kerberosUrl"
/>
</div>
</div>
</div>
<!-- Blob controls -->

View File

@ -1,5 +1,5 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import apolloProvider from './graphql';
import projectShortPathQuery from './queries/project_short_path.query.graphql';
import projectPathQuery from './queries/project_path.query.graphql';
@ -52,9 +52,33 @@ export default function initHeaderApp({ router, isReadmeView = false, isBlobView
projectRootPath,
comparePath,
projectPath,
webIdeButtonOptions,
sshUrl,
httpUrl,
xcodeUrl,
kerberosUrl,
downloadLinks,
downloadArtifacts,
projectShortPath,
} = headerEl.dataset;
const {
isFork,
needsToFork,
gitpodEnabled,
isBlob,
showEditButton,
showWebIdeButton,
showGitpodButton,
showPipelineEditorUrl,
webIdeUrl,
editUrl,
pipelineEditorUrl,
gitpodUrl,
userPreferencesGitpodPath,
userProfileEnableGitpodPath,
} = convertObjectPropsToCamelCase(JSON.parse(webIdeButtonOptions));
initClientQueries({ projectPath, projectShortPath, ref, escapedRef });
// eslint-disable-next-line no-new
@ -78,6 +102,26 @@ export default function initHeaderApp({ router, isReadmeView = false, isBlobView
projectShortPath,
comparePath,
isReadmeView,
isFork: parseBoolean(isFork),
needsToFork: parseBoolean(needsToFork),
gitpodEnabled: parseBoolean(gitpodEnabled),
isBlob: parseBoolean(isBlob),
showEditButton: parseBoolean(showEditButton),
showWebIdeButton: parseBoolean(showWebIdeButton),
showGitpodButton: parseBoolean(showGitpodButton),
showPipelineEditorUrl: parseBoolean(showPipelineEditorUrl),
webIdeUrl,
editUrl,
pipelineEditorUrl,
gitpodUrl,
userPreferencesGitpodPath,
userProfileEnableGitpodPath,
httpUrl,
xcodeUrl,
sshUrl,
kerberosUrl,
downloadLinks: downloadLinks ? JSON.parse(downloadLinks) : null,
downloadArtifacts: downloadArtifacts ? JSON.parse(downloadArtifacts) : [],
isBlobView,
},
apolloProvider,

View File

@ -17,7 +17,7 @@ import { fetchPolicies } from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { __, s__, sprintf } from '~/locale';
import SnippetCodeDropdown from '~/vue_shared/components/code_dropdown/snippet_code_dropdown.vue';
import CloneCodeDropdown from '~/vue_shared/components/code_dropdown/clone_code_dropdown.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { createAlert, VARIANT_DANGER, VARIANT_SUCCESS } from '~/alert';
import { VISIBILITY_LEVEL_PUBLIC_STRING } from '~/visibility_level/constants';
@ -38,7 +38,7 @@ export const i18n = {
export default {
components: {
SnippetCodeDropdown,
CloneCodeDropdown,
GlIcon,
GlSprintf,
GlModal,
@ -290,10 +290,10 @@ export default {
{{ editItem.text }}
</gl-button>
<snippet-code-dropdown
<clone-code-dropdown
v-if="canBeClonedOrEmbedded"
:ssh-link="snippet.sshUrlToRepo"
:http-link="snippet.httpUrlToRepo"
:ssh-url="snippet.sshUrlToRepo"
:http-url="snippet.httpUrlToRepo"
:url="snippet.webUrl"
:embeddable="embeddable"
data-testid="code-button"

View File

@ -107,7 +107,7 @@ export default {
:disabled="btn.disabled || btn.loading"
category="tertiary"
size="small"
class="gl-float-left md:gl-block"
class="gl-float-left md:gl-inline-flex"
@click="onClickAction(btn)"
>
{{ btn.text }}
@ -160,7 +160,7 @@ export default {
:disabled="btn.disabled || btn.loading"
category="tertiary"
size="small"
class="gl-float-left gl-hidden md:gl-block"
class="gl-float-left gl-hidden md:gl-inline-flex"
@click="onClickAction(btn)"
>
{{ btn.text }}

View File

@ -123,7 +123,7 @@ export default {
:disabled="btn.loading"
category="tertiary"
size="small"
class="gl-float-left gl-hidden md:gl-block"
class="gl-float-left gl-hidden md:gl-inline-flex"
@click="($event) => onClickAction(btn, $event)"
>
<template v-if="btn.text">

View File

@ -0,0 +1,21 @@
import CloneCodeDropdown from './clone_code_dropdown.vue';
export default {
component: CloneCodeDropdown,
title: 'vue_shared/components/clone_code_dropdown',
};
const Template = (args, { argTypes }) => ({
components: { CloneCodeDropdown },
props: Object.keys(argTypes),
template: '<clone-code-dropdown v-bind="$props" />',
});
const sshUrl = 'ssh://some-ssh-link';
const httpLink = 'https://some-http-link';
export const Default = Template.bind({});
Default.args = {
sshUrl,
httpUrl: httpLink,
};

View File

@ -13,19 +13,25 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
sshLink: {
sshUrl: {
type: String,
required: false,
default: '',
},
httpLink: {
httpUrl: {
type: String,
required: false,
default: '',
},
kerberosUrl: {
type: String,
required: false,
default: '',
},
url: {
type: String,
required: true,
required: false,
default: '',
},
embeddable: {
type: Boolean,
@ -35,16 +41,24 @@ export default {
},
computed: {
httpLabel() {
const protocol = this.httpLink ? getHTTPProtocol(this.httpLink)?.toUpperCase() : '';
const protocol = this.httpUrl ? getHTTPProtocol(this.httpUrl)?.toUpperCase() : '';
return sprintf(__('Clone with %{protocol}'), { protocol });
},
sections() {
const sections = [
{ label: __('Clone with SSH'), link: this.sshLink, testId: 'copy-ssh-url' },
{ label: this.httpLabel, link: this.httpLink, testId: 'copy-http-url' },
{ label: __('Clone with SSH'), link: this.sshUrl, testId: 'copy-ssh-url' },
{ label: this.httpLabel, link: this.httpUrl, testId: 'copy-http-url' },
];
if (this.embeddable) {
if (this.kerberosUrl) {
sections.push({
label: __('Clone with KRB5'),
link: this.kerberosUrl,
testId: 'copy-kerberos-url',
});
}
if (this.embeddable && this.url) {
sections.push(
{
label: __('Embed'),

View File

@ -1,33 +0,0 @@
import SnippetCodeDropdown from './snippet_code_dropdown.vue';
export default {
component: SnippetCodeDropdown,
title: 'vue_shared/components/snippet_code_dropdown',
};
const Template = (args, { argTypes }) => ({
components: { SnippetCodeDropdown },
props: Object.keys(argTypes),
template: '<clone-dropdown v-bind="$props" />',
});
const sshLink = 'ssh://some-ssh-link';
const httpLink = 'https://some-http-link';
export const Default = Template.bind({});
Default.args = {
sshLink,
httpLink,
};
export const HttpLink = Template.bind({});
HttpLink.args = {
httpLink,
sshLink: '',
};
export const SSHLink = Template.bind({});
SSHLink.args = {
sshLink,
httpLink: '',
};

View File

@ -124,11 +124,6 @@ export default {
required: false,
default: '',
},
webIdePromoPopoverImg: {
type: String,
required: false,
default: '',
},
cssClasses: {
type: String,
required: false,

View File

@ -152,6 +152,13 @@ export const autocompleteDataSources = ({ fullPath, iid, workItemTypeId, isGroup
workItemTypeId,
isGroup,
}),
milestones: autocompleteSourcesPath({
autocompleteType: 'milestones',
fullPath,
iid,
workItemTypeId,
isGroup,
}),
});
export const markdownPreviewPath = ({ fullPath, iid, isGroup = false }) => {

View File

@ -34,12 +34,12 @@ module Ci
true
end
def policy_allowed?(accessed_project, policy)
def policies_allowed?(accessed_project, policies)
return true if self_referential?(accessed_project)
return true unless accessed_project.ci_inbound_job_token_scope_enabled?
return false unless inbound_accessible?(accessed_project)
policy_allowed_for_accessed_project?(accessed_project, policy)
policies_allowed_for_accessed_project?(accessed_project, policies)
end
def outbound_projects
@ -91,11 +91,13 @@ module Ci
end
end
def policy_allowed_for_accessed_project?(accessed_project, policy)
def policies_allowed_for_accessed_project?(accessed_project, policies)
scope = nearest_scope(accessed_project)
return true if scope.default_permissions?
return false if policies.empty?
policy.to_s.in?(scope.job_token_policies)
allowed_policies = scope.job_token_policies.map(&:to_sym)
(policies - allowed_policies).empty?
end
def nearest_scope(accessed_project)

View File

@ -683,6 +683,10 @@ class User < ApplicationRecord
scope :left_join_user_detail, -> { left_joins(:user_detail) }
scope :preload_user_detail, -> { preload(:user_detail) }
scope :by_bot_namespace_ids, ->(namespace_ids) do
project_bot.joins(:user_detail).where(user_detail: { bot_namespace_id: namespace_ids })
end
def self.supported_keyset_orderings
{
id: [:asc, :desc],

View File

@ -4,6 +4,14 @@ module WorkItems
module Widgets
class Milestone < Base
delegate :milestone, to: :work_item
def self.quick_action_commands
[:milestone, :remove_milestone]
end
def self.quick_action_params
[:milestone_id]
end
end
end
end

View File

@ -15,6 +15,8 @@ module Ci
WINDOW_LENGTH = 30.days
def execute
return execute_with_new_table if ::Feature.enabled?(:ci_catalog_ranking_from_new_usage_table, :instance)
return ServiceResponse.success(message: "Processing complete for #{today}") if done_processing?
aggregator = Gitlab::Ci::Components::Usages::Aggregator.new(
@ -36,8 +38,45 @@ module Ci
end
end
def execute_with_new_table
update_component_counts
update_resource_counts
ServiceResponse.success(message: 'Usage counts updated for components and resources')
end
private
def update_component_counts
Ci::Catalog::Resources::Component.find_each(batch_size: 1000) do |component|
usage_count = component
.last_usages
.select(:used_by_project_id)
.distinct
.count
component.update_columns(
last_30_day_usage_count: usage_count
)
end
end
def update_resource_counts
# Using a subquery to sum component counts for each resource
resource_counts = Component
.group(:catalog_resource_id)
.select(
:catalog_resource_id,
'SUM(last_30_day_usage_count) as total_usage'
)
resource_counts.each do |result|
Ci::Catalog::Resource.where(id: result.catalog_resource_id).update_all(
last_30_day_usage_count: result.total_usage
)
end
end
# NOTE: New catalog resources added today are considered already processed
# because their `last_30_day_usage_count_updated_at` is defaulted to NOW().
def done_processing?

View File

@ -282,13 +282,9 @@ class IssuableBaseService < ::BaseContainerService
change_additional_attributes(issuable)
assign_requested_assignees(issuable)
assign_requested_crm_contacts(issuable)
widget_params = filter_widget_params
initialize_callbacks!(issuable)
if issuable.changed? || params.present? || widget_params.present? || @callbacks.present?
if issuable.changed? || params.present? || @callbacks.present?
issuable.assign_attributes(allowed_update_params(params))
before_update(issuable)
@ -447,6 +443,9 @@ class IssuableBaseService < ::BaseContainerService
change_subscription(issuable)
change_todo(issuable)
toggle_award(issuable)
assign_requested_assignees(issuable)
assign_requested_crm_contacts(issuable)
end
def change_state(issuable)
@ -627,10 +626,6 @@ class IssuableBaseService < ::BaseContainerService
issuable_sla.update(issuable_closed: issuable.closed?)
end
def filter_widget_params
params.delete(:widget_params)
end
def filter_contact_params(issuable)
return if params.slice(:add_contacts, :remove_contacts).empty?
return if can?(current_user, :set_issue_crm_contacts, issuable)

View File

@ -123,6 +123,8 @@ module QuickActions
end
def find_milestones(project, params = {})
return [] unless project
group_ids = project.group.self_and_ancestors.select(:id) if project.group
MilestonesFinder.new(params.merge(project_ids: [project.id], group_ids: group_ids)).execute

View File

@ -7,7 +7,6 @@ module WorkItems
def initialize(container:, current_user: nil, params: {}, perform_spam_check: false, widget_params: {})
@extra_params = params.delete(:extra_params) || {}
params[:widget_params] = true if widget_params.present?
super(container: container, current_user: current_user, params: params, perform_spam_check: perform_spam_check)

View File

@ -1,5 +1,11 @@
- ref = local_assigns.fetch(:ref) { current_ref }
- project = local_assigns.fetch(:project) { @project }
- web_ide_button_data = web_ide_button_data({ blob: nil })
- fork_options = fork_modal_options(@project, nil)
- archive_prefix = ref ? "#{project.path}-#{ref.tr('/', '-')}" : ''
- download_links = !project.empty_repo? ? download_links(project, ref, archive_prefix).to_json : ''
- pipeline = local_assigns.fetch(:pipeline, nil)
- download_artifacts = pipeline && previous_artifacts(project, ref, pipeline.latest_builds_with_artifacts).to_json || []
- add_page_startup_api_call logs_file_project_ref_path(@project, ref, @path, format: "json", offset: 0, ref_type: @ref_type)
- if readme_path = @project.repository.readme_path
- add_page_startup_api_call project_blob_path(@project, tree_join(@ref, readme_path), viewer: "rich", format: "json")
@ -10,7 +16,24 @@
#tree-holder.tree-holder.clearfix.js-per-page.gl-mt-5{ data: { blame_per_page: Gitlab::Git::BlamePagination::PAGINATION_PER_PAGE } }
- if params[:common_repository_blob_header_app] == 'true'
#js-repository-blob-header-app{ data: { project_id: @project.id, ref: ref, ref_type: @ref_type.to_s, breadcrumbs: breadcrumb_data_attributes, project_root_path: project_path(@project), project_path: project.full_path, compare_path: compare_path, escaped_ref: ActionDispatch::Journey::Router::Utils.escape_path(ref) } }
#js-repository-blob-header-app{ data: {
project_id: @project.id,
ref: ref,
ref_type: @ref_type.to_s,
breadcrumbs: breadcrumb_data_attributes,
project_root_path: project_path(@project),
project_path: project.full_path,
compare_path: compare_path,
web_ide_button_options: web_ide_button_data.merge(fork_options).to_json,
web_ide_button_default_branch: @project.default_branch_or_main,
ssh_url: ssh_enabled? ? ssh_clone_url_to_repo(@project) : '',
http_url: http_enabled? ? http_clone_url_to_repo(@project) : '',
xcode_url: show_xcode_link?(@project) ? xcode_uri_to_repo(@project) : '',
kerberos_url: alternative_kerberos_url? ? project.kerberos_url_to_repo : '',
download_links: download_links,
download_artifacts: download_artifacts,
escaped_ref: ActionDispatch::Journey::Router::Utils.escape_path(ref)
} }
- else
.nav-block.gl-flex.gl-flex-col.sm:gl-flex-row.gl-items-stretch

View File

@ -1,5 +1,7 @@
- ref = local_assigns.fetch(:ref) { current_ref }
- project = local_assigns.fetch(:project) { @project }
- web_ide_button_data = web_ide_button_data({ blob: nil })
- fork_options = fork_modal_options(@project, nil)
- add_page_specific_style 'page_bundles/projects'
- unless @ref.blank? || @repository&.root_ref == @ref
- compare_path = project_compare_index_path(@project, from: @repository&.root_ref, to: @ref)
@ -7,7 +9,18 @@
- if (readme = @repository.readme) && readme.rich_viewer
.tree-holder.gl-mt-5
- if params[:common_repository_blob_header_app] == 'true'
#js-repository-blob-header-app{ data: { project_id: @project.id, ref: ref, ref_type: @ref_type.to_s, breadcrumbs: breadcrumb_data_attributes, project_root_path: project_path(@project), project_path: project.full_path, compare_path: compare_path, escaped_ref: ActionDispatch::Journey::Router::Utils.escape_path(ref) } }
#js-repository-blob-header-app{ data: {
project_id: @project.id,
ref: ref,
ref_type: @ref_type.to_s,
breadcrumbs: breadcrumb_data_attributes,
project_root_path: project_path(@project),
project_path: project.full_path,
compare_path: compare_path,
web_ide_button_options: web_ide_button_data.merge(fork_options).to_json,
web_ide_button_default_branch: @project.default_branch_or_main,
escaped_ref: ActionDispatch::Journey::Router::Utils.escape_path(ref)
} }
- else
.nav-block.mt-0

View File

@ -1,4 +1,4 @@
= render layout: 'projects/protected_tags/shared/protected_tag', locals: { protected_tag: protected_tag } do
%td.create_access_levels-container
= render 'projects/protected_tags/protected_tag_create_access_levels', protected_tag: protected_tag, create_access_level: protected_tag.create_access_levels.for_role
= render 'projects/protected_tags/protected_tag_create_access_levels', protected_tag: protected_tag, create_access_level: protected_tag.create_access_levels
= render_if_exists 'projects/protected_tags/protected_tag_extra_create_access_levels', protected_tag: protected_tag

View File

@ -5,4 +5,4 @@
- fork_options = fork_modal_options(@project, blob)
- css_classes = false unless local_assigns[:css_classes]
.gl-inline-block{ data: { options: button_data.merge(fork_options).to_json, web_ide_promo_popover_img: image_path('web-ide-promo-popover.svg'), css_classes: css_classes, default_branch: @project.default_branch_or_main }, id: "js-#{type}-web-ide-link" }
.gl-inline-block{ data: { options: button_data.merge(fork_options).to_json, css_classes: css_classes, default_branch: @project.default_branch_or_main }, id: "js-#{type}-web-ide-link" }

View File

@ -0,0 +1,9 @@
---
name: ci_catalog_ranking_from_new_usage_table
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/work_items/495075
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/173081
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/505196
milestone: '17.7'
group: group::pipeline authoring
type: gitlab_com_derisk
default_enabled: false

View File

@ -6,7 +6,7 @@
# `16-9-deprecated-feature.yml`, where `16-9` is the announcement milestone,
# not the removal milestone.
#
# See the deprecation guidelines to confirm your understanding of GitLab's definitions:
# See the deprecation guidelines to confirm your understanding of the terminology:
# https://docs.gitlab.com/ee/development/deprecation_guidelines/#terminology
#
# If an End of Support period applies, see the OPTIONAL section below.

View File

@ -0,0 +1,8 @@
---
migration_job_name: BackfillIssuableSeveritiesNamespaceId
description: Backfills sharding key `issuable_severities.namespace_id` from `issues`.
feature_category: team_planning
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/173728
milestone: '17.7'
queued_migration_version: 20241125145009
finalized_by: # version of the migration that finalized this BBM

View File

@ -18,3 +18,4 @@ desired_sharding_key:
sharding_key: namespace_id
belongs_to: issue
table_size: small
desired_sharding_key_migration_job_name: BackfillIssuableSeveritiesNamespaceId

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddNamespaceIdToIssuableSeverities < Gitlab::Database::Migration[2.2]
milestone '17.7'
def change
add_column :issuable_severities, :namespace_id, :bigint
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class IndexIssuableSeveritiesOnNamespaceId < Gitlab::Database::Migration[2.2]
milestone '17.7'
disable_ddl_transaction!
INDEX_NAME = 'index_issuable_severities_on_namespace_id'
def up
add_concurrent_index :issuable_severities, :namespace_id, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :issuable_severities, INDEX_NAME
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddIssuableSeveritiesNamespaceIdFk < Gitlab::Database::Migration[2.2]
milestone '17.7'
disable_ddl_transaction!
def up
add_concurrent_foreign_key :issuable_severities, :namespaces, column: :namespace_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :issuable_severities, column: :namespace_id
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
class AddIssuableSeveritiesNamespaceIdTrigger < Gitlab::Database::Migration[2.2]
milestone '17.7'
def up
install_sharding_key_assignment_trigger(
table: :issuable_severities,
sharding_key: :namespace_id,
parent_table: :issues,
parent_sharding_key: :namespace_id,
foreign_key: :issue_id
)
end
def down
remove_sharding_key_assignment_trigger(
table: :issuable_severities,
sharding_key: :namespace_id,
parent_table: :issues,
parent_sharding_key: :namespace_id,
foreign_key: :issue_id
)
end
end

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
class QueueBackfillIssuableSeveritiesNamespaceId < Gitlab::Database::Migration[2.2]
milestone '17.7'
restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
MIGRATION = "BackfillIssuableSeveritiesNamespaceId"
DELAY_INTERVAL = 2.minutes
BATCH_SIZE = 1000
SUB_BATCH_SIZE = 100
def up
queue_batched_background_migration(
MIGRATION,
:issuable_severities,
:id,
:namespace_id,
:issues,
:namespace_id,
:issue_id,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
end
def down
delete_batched_background_migration(
MIGRATION,
:issuable_severities,
:id,
[
:namespace_id,
:issues,
:namespace_id,
:issue_id
]
)
end
end

View File

@ -0,0 +1 @@
0ab0371d24515c2fcb804e47fa7526edfb681204fed2137db4e9498f4b7fe116

View File

@ -0,0 +1 @@
a3632c565094b3a1672ff9c5c3f7ec04b993555ad3d1eb633b1b033c9741729f

View File

@ -0,0 +1 @@
c2ce13c3186404aec3981a847dd971c3a3a9be1cedd7aad0554821e83c8fe1b5

View File

@ -0,0 +1 @@
f27f4eacc953b45d95559b141ec42e6a312c44c9496a8283dc61dcb2443ddc47

View File

@ -0,0 +1 @@
98452a102d7cafe648ad028a41f2d71a42a9bd429b1df7356f66cd78bad370ed

View File

@ -2721,6 +2721,22 @@ RETURN NEW;
END
$$;
CREATE FUNCTION trigger_dfad97659d5f() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW."namespace_id" IS NULL THEN
SELECT "namespace_id"
INTO NEW."namespace_id"
FROM "issues"
WHERE "issues"."id" = NEW."issue_id";
END IF;
RETURN NEW;
END
$$;
CREATE FUNCTION trigger_e0864d1cff37() RETURNS trigger
LANGUAGE plpgsql
AS $$
@ -13514,7 +13530,8 @@ ALTER SEQUENCE issuable_resource_links_id_seq OWNED BY issuable_resource_links.i
CREATE TABLE issuable_severities (
id bigint NOT NULL,
issue_id bigint NOT NULL,
severity smallint DEFAULT 0 NOT NULL
severity smallint DEFAULT 0 NOT NULL,
namespace_id bigint
);
CREATE SEQUENCE issuable_severities_id_seq
@ -30616,6 +30633,8 @@ CREATE INDEX index_issuable_resource_links_on_issue_id ON issuable_resource_link
CREATE UNIQUE INDEX index_issuable_severities_on_issue_id ON issuable_severities USING btree (issue_id);
CREATE INDEX index_issuable_severities_on_namespace_id ON issuable_severities USING btree (namespace_id);
CREATE INDEX index_issuable_slas_on_due_at_id_label_applied_issuable_closed ON issuable_slas USING btree (due_at, id) WHERE ((label_applied = false) AND (issuable_closed = false));
CREATE UNIQUE INDEX index_issuable_slas_on_issue_id ON issuable_slas USING btree (issue_id);
@ -35428,6 +35447,8 @@ CREATE TRIGGER trigger_dc13168b8025 BEFORE INSERT OR UPDATE ON vulnerability_fla
CREATE TRIGGER trigger_delete_project_namespace_on_project_delete AFTER DELETE ON projects FOR EACH ROW WHEN ((old.project_namespace_id IS NOT NULL)) EXECUTE FUNCTION delete_associated_project_namespace();
CREATE TRIGGER trigger_dfad97659d5f BEFORE INSERT OR UPDATE ON issuable_severities FOR EACH ROW EXECUTE FUNCTION trigger_dfad97659d5f();
CREATE TRIGGER trigger_e0864d1cff37 BEFORE INSERT OR UPDATE ON packages_debian_group_architectures FOR EACH ROW EXECUTE FUNCTION trigger_e0864d1cff37();
CREATE TRIGGER trigger_e1da4a738230 BEFORE INSERT OR UPDATE ON vulnerability_external_issue_links FOR EACH ROW EXECUTE FUNCTION trigger_e1da4a738230();
@ -37127,6 +37148,9 @@ ALTER TABLE ONLY protected_tag_create_access_levels
ALTER TABLE ONLY application_settings
ADD CONSTRAINT fk_f9867b3540 FOREIGN KEY (web_ide_oauth_application_id) REFERENCES oauth_applications(id) ON DELETE SET NULL;
ALTER TABLE ONLY issuable_severities
ADD CONSTRAINT fk_f9df19ecb6 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE p_ci_stages
ADD CONSTRAINT fk_fb57e6cc56_p FOREIGN KEY (partition_id, pipeline_id) REFERENCES p_ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;

View File

@ -16,7 +16,7 @@ DETAILS:
> - `product_analytics_internal_preview` replaced with `product_analytics_dashboards` in GitLab 15.11.
> - `product_analytics_dashboards` [enabled](https://gitlab.com/gitlab-org/gitlab/-/issues/398653) by default in GitLab 16.11.
> - Feature flag `product_analytics_dashboards` [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/454059) in GitLab 17.1.
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/167296) to beta and feature flag `product_analytics_features` added in GitLab 17.5.
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/167296) to beta in GitLab 17.5 [with a flag](../administration/feature_flags.md) named `product_analytics_features`.
FLAG:
The availability of this feature is controlled by a feature flag.

View File

@ -7,7 +7,11 @@ info: Any user with at least the Maintainer role can merge updates to this conte
# Metrics Dictionary Guide
[Service Ping](../service_ping/index.md) metrics are defined in individual YAML files definitions from which the
[Metrics Dictionary](https://metrics.gitlab.com/) is built. Currently, the metrics dictionary is built automatically once a day. When a change to a metric is made in a YAML file, you can see the change in the dictionary within 24 hours.
[Metrics Dictionary](https://metrics.gitlab.com/) is built. Currently, the metrics dictionary is built automatically once an hour.
- When a change to a metric is made in a YAML file, you can see the change in the dictionary within 1 hour of the change getting deployed to production.
- When a change to an event is made in a YAML file, you can see the change in the dictionary within 1 hour of the change getting merged to the master branch.
This guide describes the dictionary and how it's implemented.
## Metrics Definition and validation
@ -29,7 +33,7 @@ All metrics are stored in YAML files:
WARNING:
Only metrics with a metric definition YAML and whose status is not `removed` are added to the Service Ping JSON payload.
Each metric is defined in a separate YAML file consisting of a number of fields:
Each metric is defined in a YAML file consisting of a number of fields:
| Field | Required | Additional information |
|------------------------------|----------|------------------------|
@ -38,7 +42,7 @@ Each metric is defined in a separate YAML file consisting of a number of fields:
| `product_group` | yes | The [group](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml) that owns the metric. |
| `value_type` | yes | `string`; one of [`string`, `number`, `boolean`, `object`](https://json-schema.org/understanding-json-schema/reference/type). |
| `status` | yes | `string`; [status](#metric-statuses) of the metric, may be set to `active`, `removed`, `broken`. |
| `time_frame` | yes | `string`; may be set to a value like `7d`, `28d`, `all`, `none`. |
| `time_frame` | yes | `string` or `array`; may be set to `7d`, `28d`, `all`, `none` or an array including any of these values except for `none`. |
| `data_source` | yes | `string`; may be set to a value like `database`, `redis`, `redis_hll`, `prometheus`, `system`, `license`, `internal_events`. |
| `data_category` | yes | `string`; [categories](#data-category) of the metric, may be set to `operational`, `optional`, `subscription`, `standard`. The default value is `optional`. |
| `instrumentation_class` | no | `string`; used for metrics with `data_source` other than `internal_events`. See [the class that implements the metric](metrics_instrumentation.md). |
@ -59,12 +63,15 @@ The `key_path` of the metric is the location in the JSON Service Ping payload.
The `key_path` could be composed from multiple parts separated by `.` and it must be unique.
We recommend to add the metric in one of the top-level keys:
If a metric definition has an array `time_frame`, the `key_path` defined in the YAML file will have a suffix automatically added for each of the included time frames:
- `settings`: for settings related metrics.
- `counts_weekly`: for counters that have data for the most recent 7 days.
- `counts_monthly`: for counters that have data for the most recent 28 days.
- `counts`: for counters that have data for all time.
| time_frame | `key_path` suffix|
|------------|------------------|
| `all` | no suffix |
| `7d` | `_weekly` |
| `28d` | `_monthly` |
The `key_path`s shown in the [Metrics Dictionary](https://metrics.gitlab.com/) include those suffixes.
### Metric statuses
@ -88,7 +95,7 @@ Metric definitions can have one of the following values for `value_type`:
### Metric `time_frame`
A metric's time frame is calculated based on the `time_frame` field and the `data_source` of the metric.
A metric's time frame is calculated based on the `time_frame` field and the `data_source` of the metric. When `time_frame` is an array, the metric's values are calculated for each of the included time frames.
| data_source | time_frame | Description |
|------------------------|------------|-------------------------------------------------|

View File

@ -1004,7 +1004,10 @@ _KaTeX only supports a [subset](https://katex.org/docs/supported.html) of LaTeX.
This syntax also works in AsciiDoc wikis and files using `:stem: latexmath`. For details, see
the [Asciidoctor user manual](https://asciidoctor.org/docs/user-manual/#activating-stem-support).
To prevent malicious activity, GitLab renders only the first 50 inline math instances.
To prevent malicious activity, GitLab renders only the first 50 inline math instances.
You can disable this limit [for a group](../api/graphql/reference/index.md#mutationgroupupdate)
or for the entire [self-managed instance](../administration/instance_limits.md#math-rendering-limits).
The number of math blocks is also limited based on render time. If the limit is exceeded,
GitLab renders the excess math instances as text. Wiki and repository files do not have
these limits.

View File

@ -138,12 +138,14 @@ have weight assigned, because issues with no weight don't show on the chart.
## Roll up weights
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/381879) in GitLab 16.11 [with a flag](../../../administration/feature_flags.md) named `rollup_timebox_chart`. Disabled by default.
DETAILS:
**Offering:** Self-managed
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/381879) in GitLab 17.1 [with a flag](../../../administration/feature_flags.md) named `rollup_timebox_chart`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, an administrator can [enable the feature flag](../../../administration/feature_flags.md) named `rollup_timebox_chart`.
On GitLab.com and GitLab Dedicated, this feature is not available.
This feature is not ready for production use.
On self-managed GitLab, by default this feature is not available. For more information, see the history.
This feature is available for testing, but not ready for production use.
With [tasks](../../tasks.md), a more granular planning is possible.
If this feature is enabled, the weight of issues that have tasks is derived from the tasks in the

View File

@ -135,6 +135,7 @@ module API
values: %w[running success failed canceled]
end
route_setting :authentication, job_token_allowed: true
route_setting :authorization, job_token_policies: [:admin_deployments, :admin_environments]
post ':id/deployments' do
authorize!(:create_deployment, user_project)
authorize!(:create_environment, user_project)

View File

@ -39,7 +39,7 @@ module API
mutually_exclusive :name, :search, message: 'cannot be used together'
end
route_setting :authentication, job_token_allowed: true
route_setting :authorization, job_token_policy: :read_environments
route_setting :authorization, job_token_policies: :read_environments
get ':id/environments' do
authorize! :read_environment, user_project

View File

@ -169,7 +169,7 @@ module API
return handle_job_token_failure!(project)
end
return forbidden!(job_token_policy_unauthorized_message(project)) unless job_token_policy_authorized?(project)
return forbidden!(job_token_policies_unauthorized_message(project)) unless job_token_policies_authorized?(project)
if project_moved?(id, project)
return not_allowed!('Non GET methods are not allowed for moved projects') unless request.get?
@ -1011,27 +1011,33 @@ module API
end
end
def job_token_policy_authorized?(project)
def job_token_policies_authorized?(project)
return true unless current_user&.from_ci_job_token?
return true unless Feature.enabled?(:enforce_job_token_policies, current_user)
current_user.ci_job_token_scope.policy_allowed?(project, job_token_policy)
current_user.ci_job_token_scope.policies_allowed?(project, job_token_policies)
end
def job_token_policy_unauthorized_message(project)
policy = job_token_policy
if policy.present?
def job_token_policies_unauthorized_message(project)
policies = job_token_policies
case policies.size
when 0
'This action is unauthorized for CI/CD job tokens.'
when 1
format("Insufficient permissions to access this resource in project %{project}. " \
"The following token permission is required: %{permission}.", project: project.path, permission: policy)
"The following token permission is required: %{policy}.",
project: project.path, policy: policies[0])
else
'This action is not authorized for CI/CD job tokens.'
format("Insufficient permissions to access this resource in project %{project}. " \
"The following token permissions are required: %{policies}.",
project: project.path, policies: policies.to_sentence)
end
end
def job_token_policy
return unless respond_to?(:route_setting)
def job_token_policies
return [] unless respond_to?(:route_setting)
route_setting(:authorization).try(:fetch, :job_token_policy, nil)
Array(route_setting(:authorization).try(:fetch, :job_token_policies, nil))
end
end
end

View File

@ -55,7 +55,7 @@ module Authn
payload, _header = ::JSONWebToken::RSAToken.decode(token, signing_public_key)
new(payload: payload, subject_type: subject_type)
rescue JWT::DecodeError, Gitlab::Graphql::Errors::ArgumentError
rescue JWT::DecodeError
# The token received is not a JWT
nil
end
@ -70,6 +70,9 @@ module Authn
return unless payload
GitlabSchema.parse_gid(payload['sub'], expected_type: subject_type)&.find
rescue Gitlab::Graphql::Errors::ArgumentError
# The subject doesn't exist anymore
nil
end
strong_memoize_attr :subject

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
class BackfillIssuableSeveritiesNamespaceId < BackfillDesiredShardingKeyJob
operation_name :backfill_issuable_severities_namespace_id
feature_category :team_planning
end
end
end

View File

@ -6,8 +6,8 @@ gemspec
group :test do
gem "climate_control", "~> 1.2.0"
gem "gitlab-styles", "~> 13.0.1"
gem "pry", "~> 0.14.2"
gem "gitlab-styles", "~> 13.0.2"
gem "pry", "~> 0.15.0"
gem "rspec", "~> 3.13"
gem "simplecov", "~> 0.22.0"
end

View File

@ -34,8 +34,8 @@ GEM
diff-lcs (1.5.1)
docile (1.4.0)
drb (2.2.1)
gitlab-styles (13.0.1)
rubocop (~> 1.67.0)
gitlab-styles (13.0.2)
rubocop (~> 1.68.0)
rubocop-capybara (~> 2.21.0)
rubocop-factory_bot (~> 2.26.1)
rubocop-graphql (~> 1.5.4)
@ -56,7 +56,7 @@ GEM
racc
pastel (0.8.0)
tty-color (~> 0.5)
pry (0.14.2)
pry (0.15.0)
coderay (~> 1.1)
method_source (~> 1.0)
racc (1.8.1)
@ -77,7 +77,7 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.1)
rubocop (1.67.0)
rubocop (1.68.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
@ -141,8 +141,8 @@ PLATFORMS
DEPENDENCIES
climate_control (~> 1.2.0)
gitlab-cng!
gitlab-styles (~> 13.0.1)
pry (~> 0.14.2)
gitlab-styles (~> 13.0.2)
pry (~> 0.15.0)
rspec (~> 3.13)
simplecov (~> 0.22.0)

View File

@ -33,7 +33,7 @@ module QA
element 'blob-viewer-file-content'
end
base.view 'app/assets/javascripts/vue_shared/components/code_dropdown/snippet_code_dropdown.vue' do
base.view 'app/assets/javascripts/vue_shared/components/code_dropdown/clone_code_dropdown.vue' do
element 'copy-http-url'
element 'copy-ssh-url'
end

View File

@ -15,6 +15,7 @@ require_relative './cli/flows/flow_advisor'
require_relative './cli/flows/metric_definer'
require_relative './cli/flows/usage_viewer'
require_relative './cli/global_state'
require_relative './cli/time_framed_key_path'
require_relative './cli/metric'
require_relative './cli/event'

View File

@ -139,6 +139,7 @@ module InternalEventsCli
per_page: 20,
&disabled_format_callback
)
@metrics = reduce_metrics_by_time_frame(@metrics)
assign_shared_attrs(:actions, :milestone) do
{
@ -221,10 +222,23 @@ module InternalEventsCli
def file_saved_context_message(attributes)
format_prefix " ", <<~TEXT.chomp
- Visit #{format_info('https://metrics.gitlab.com')} to find dashboard links for this metric
- Metric trend dashboard: #{format_info(metric_trend_path(attributes['key_path']))}
#{metric_dashboard_links(attributes)}
TEXT
end
def metric_dashboard_links(attributes)
time_frames = attributes['time_frame']
unless time_frames.is_a?(Array)
return "- Metric trend dashboard: #{format_info(metric_trend_path(attributes['key_path']))}"
end
dashboards = time_frames.map do |time_frame|
key_path = TimeFramedKeyPath.build(attributes['key_path'], time_frame)
" - #{format_info(metric_trend_path(key_path))}"
end
["- Metric trend dashboards:", *dashboards].join("\n")
end
# Check existing event files for attributes to copy over
def prompt_for_copying_event_properties
shared_values = collect_values_for_shared_event_properties
@ -389,6 +403,19 @@ module InternalEventsCli
end
end
def reduce_metrics_by_time_frame(metrics)
# MetricOptions class returns one metric per time_frame value,
# here we merge them into a singular metric including all the time_frame values
return metrics unless metrics.length > 1
time_frames = metrics.map do |metric|
metric.time_frame.value
end
attributes = metrics.first.to_h.merge(time_frame: time_frames)
[Metric.new(**attributes)]
end
# Helper for #prompt_for_event_filters
def print_event_filter_header(event, idx, total)
cli.say "\n"

View File

@ -12,11 +12,24 @@ module InternalEventsCli
end
def metrics
@metrics ||= load_definitions(
Metric,
InternalEventsCli::NEW_METRIC_FIELDS,
all_metric_paths
)
@metrics ||= begin
loaded_files = load_definitions(
Metric,
InternalEventsCli::NEW_METRIC_FIELDS,
all_metric_paths
)
loaded_files.flat_map do |metric|
# copy logic of Gitlab::Usage::MetricDefinition
next metric unless metric.time_frame.is_a?(Array)
metric.time_frame.map do |time_frame|
current_metric = metric.dup
current_metric.time_frame = time_frame
current_metric.key_path = TimeFramedKeyPath.build(current_metric.key_path, time_frame)
current_metric
end
end
end
end
def reload_definitions

View File

@ -203,7 +203,7 @@ module InternalEventsCli
# ex) "Monthly/Weekly"
def time_frame_phrase
phrase = metrics.map { |metric| metric.time_frame.description }.join('/')
phrase = metrics.map { |metric| metric.time_frame.description.capitalize }.join('/')
disabled ? phrase : format_info(phrase)
end

View File

@ -159,12 +159,15 @@ module InternalEventsCli
# Automatically prepended to all new descriptions
# ex) Total count of
# ex) Weekly/Monthly count of unique
# ex) Count of
def description_prefix
[
description_components = [
time_frame.description,
identifier.prefix,
*(identifier.plural if identifier.default?)
].join(' ')
].compact
description_components.join(' ').capitalize
end
# Provides simplified but technically accurate description
@ -172,8 +175,10 @@ module InternalEventsCli
def technical_description
event_name = actions.first if events.length == 1 && !filtered?
event_name ||= 'the selected events'
"#{time_frame.description} #{identifier.description % event_name}"
[
time_frame.description,
(identifier.description % event_name).to_s
].compact.join(' ').capitalize
end
def bulk_assign(key_value_pairs)
@ -185,21 +190,25 @@ module InternalEventsCli
TimeFrame = Struct.new(:value) do
def description
case value
when Array
nil # array time_frame metrics have no description prefix
when '7d'
'Weekly'
'weekly'
when '28d'
'Monthly'
'monthly'
when 'all'
'Total'
'total'
end
end
def directory_name
return "counts_all" if value.is_a? Array
"counts_#{value}"
end
def key_path
description&.downcase if value != 'all'
description&.downcase if %w[7d 28d].include?(value)
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
# Helpers for shared & state across all CLI flows
module InternalEventsCli
class TimeFramedKeyPath
METRIC_TIME_FRAME_SUFFIX = {
'7d' => '_weekly',
'28d' => '_monthly',
'all' => ''
}.freeze
def self.build(base_key_path, time_frame)
# copy logic of Gitlab::Usage::MetricDefinition
"#{base_key_path}#{METRIC_TIME_FRAME_SUFFIX[time_frame]}"
end
end
end

View File

@ -121,10 +121,8 @@
files:
- path: config/events/random_name.yml
content: spec/fixtures/scripts/internal_events/events/keyboard_smashed_event.yml
- path: config/metrics/counts_28d/count_distinct_user_id_from_random_name_monthly.yml
content: spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_28d.yml
- path: config/metrics/counts_7d/count_distinct_user_id_from_random_name_weekly.yml
content: spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric_7d.yml
- path: config/metrics/counts_all/count_distinct_user_id_from_random_name.yml
content: spec/fixtures/scripts/internal_events/metrics/keyboard_smashed_metric.yml
- description: Creates an event after helping the user figure out next steps
inputs:

View File

@ -10,17 +10,13 @@
- "\n" # Select: config/events/internal_events_cli_used.yml
- "\n" # Select: Weekly count of unique users
- "who defined an internal event using the CLI\n" # Input description
- "\n" # Submit weekly description for monthly
- "1\n" # Enum-select: Copy & continue
- "y\n" # Create file
- "y\n" # Create file
- "5\n" # Exit
outputs:
files:
- path: config/metrics/counts_28d/count_distinct_user_id_from_internal_events_cli_used_monthly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml
- path: config/metrics/counts_7d/count_distinct_user_id_from_internal_events_cli_used_weekly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml
- path: config/metrics/counts_all/count_distinct_user_id_from_internal_events_cli_used.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_single_event.yml
- description: Create a weekly/monthly metric for a multiple events, but select only one event
inputs:
@ -35,17 +31,13 @@
- "\n" # Submit selections
- "\n" # Select: Weekly count of unique projects
- "who defined an internal event using the CLI\n" # Input description
- "\n" # Submit weekly description for monthly
- "1\n" # Enum-select: Copy & continue
- "y\n" # Create file
- "y\n" # Create file
- "5\n" # Exit
outputs:
files:
- path: config/metrics/counts_28d/count_distinct_user_id_from_internal_events_cli_used_monthly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml
- path: config/metrics/counts_7d/count_distinct_user_id_from_internal_events_cli_used_weekly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml
- path: config/metrics/counts_all/count_distinct_user_id_from_internal_events_cli_used.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_single_event.yml
- description: Create a weekly/monthly metric for multiple events
inputs:
@ -65,17 +57,13 @@
- "\e[B" # Arrow down to: Weekly count of unique projects
- "\n" # Select: Weekly count of unique projects
- "where a defition file was created with the CLI\n" # Input description
- "\n" # Submit weekly description for monthly
- "1\n" # Select: Copy & continue
- "y\n" # Create file
- "y\n" # Create file
- "5\n" # Exit
outputs:
files:
- path: config/metrics/counts_28d/count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_monthly.yml
content: spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml
- path: config/metrics/counts_7d/count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_weekly.yml
content: spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml
- path: config/metrics/counts_all/count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used.yml
content: spec/fixtures/scripts/internal_events/metrics/project_id_multiple_events.yml
- description: Create an all time total metric for a single event
inputs:
@ -129,6 +117,33 @@
- path: ee/config/metrics/counts_all/count_total_internal_events_cli_used.yml
content: spec/fixtures/scripts/internal_events/metrics/ee_total_single_event.yml
- description: Create a weekly metric for a single event that already has a monthly metric
inputs:
files:
- path: config/events/internal_events_cli_used.yml
content: spec/fixtures/scripts/internal_events/events/event_with_identifiers.yml
- path: config/metrics/counts_28d/count_internal_events_cli_used_monthly.yml
content: spec/fixtures/scripts/internal_events/metrics/ee_total_28d_single_event.yml
keystrokes:
- "2\n" # Enum-select: New Metric -- calculate how often one or more existing events occur over time
- "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction
- 'internal_events_cli_used' # Filters to this event
- "\n" # Select: config/events/internal_events_cli_used.yml
- "\e[A\e[A" # Arrow up to: Monthly/Weekly count of events
- "\n" # Select: Monthly/Weekly count of events
- "when an event was defined using the CLI\n" # Input description
- "2\n" # Enum-select: Modify attributes
- "\n" # Accept group from event definition
- "\n" # Accept product categories from event definition
- "\n" # Accept URL from event definition
- "2\n" # Override tier -> Select: [premium, ultimate]
- "y\n" # Create file
- "5\n" # Exit
outputs:
files:
- path: ee/config/metrics/counts_7d/count_total_internal_events_cli_used_weekly.yml
content: spec/fixtures/scripts/internal_events/metrics/ee_total_7d_single_event.yml
- description: Create a metric after helping the user figure out next steps
inputs:
files:
@ -153,6 +168,7 @@
files:
- path: config/metrics/counts_all/count_total_internal_events_cli_used.yml
content: spec/fixtures/scripts/internal_events/metrics/total_single_event.yml
- description: Create a metric and then another metric with copied events
inputs:
files:
@ -165,10 +181,8 @@
- "\n" # Select: config/events/internal_events_cli_used.yml
- "\n" # Select: Weekly count of unique users
- "who defined an internal event using the CLI\n" # Input description
- "\n" # Submit weekly description for monthly
- "1\n" # Enum-select: Copy & continue
- "y\n" # Create file
- "y\n" # Create file
- "2\n" # Create another metric with the same events
- "\e[A" # Arrow up to: Total count of events
- "\n" # Select: Total count of events
@ -178,10 +192,8 @@
- "5\n" # Exit
outputs:
files:
- path: config/metrics/counts_28d/count_distinct_user_id_from_internal_events_cli_used_monthly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml
- path: config/metrics/counts_7d/count_distinct_user_id_from_internal_events_cli_used_weekly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml
- path: config/metrics/counts_all/count_distinct_user_id_from_internal_events_cli_used.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_single_event.yml
- path: config/metrics/counts_all/count_total_internal_events_cli_used.yml
content: spec/fixtures/scripts/internal_events/metrics/total_single_event.yml
@ -197,10 +209,8 @@
- "\n" # Select: config/events/internal_events_cli_used.yml
- "\n" # Select: Weekly count of unique users
- "who defined an internal event using the CLI\n" # Input description
- "\n" # Submit weekly description for monthly
- "1\n" # Enum-select: Copy & continue
- "y\n" # Create file
- "y\n" # Create file
- "3\n" # Create another metric
- "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction
- 'internal_events_cli_used' # Filters to this event
@ -213,10 +223,8 @@
- "5\n" # Exit
outputs:
files:
- path: config/metrics/counts_28d/count_distinct_user_id_from_internal_events_cli_used_monthly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml
- path: config/metrics/counts_7d/count_distinct_user_id_from_internal_events_cli_used_weekly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml
- path: config/metrics/counts_all/count_distinct_user_id_from_internal_events_cli_used.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_single_event.yml
- path: config/metrics/counts_all/count_total_internal_events_cli_used.yml
content: spec/fixtures/scripts/internal_events/metrics/total_single_event.yml
@ -232,10 +240,8 @@
- "\n" # Select: config/events/internal_events_cli_used.yml
- "\n" # Select: Weekly count of unique users
- "who defined an internal event using the CLI\n" # Input description
- "\n" # Submit weekly description for monthly
- "1\n" # Enum-select: Copy & continue
- "y\n" # Create file
- "y\n" # Create file
- "2\n" # Create another metric
- "\e[A" # Arrow up to: Total count of events
- "\n" # Select: Total count of events
@ -256,10 +262,8 @@
- "4\n" # Exit
outputs:
files:
- path: config/metrics/counts_28d/count_distinct_user_id_from_internal_events_cli_used_monthly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event.yml
- path: config/metrics/counts_7d/count_distinct_user_id_from_internal_events_cli_used_weekly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event.yml
- path: config/metrics/counts_all/count_distinct_user_id_from_internal_events_cli_used.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_single_event.yml
- path: config/metrics/counts_all/count_total_internal_events_cli_used.yml
content: spec/fixtures/scripts/internal_events/metrics/total_single_event.yml
- path: ee/config/events/internal_events_cli_opened.yml
@ -324,18 +328,13 @@
- "\n" # Skip "Add extra property" filter
- "who tried and failed to define an internal event using the CLI\n" # Input description
- "failed_usage_attempts\n" # Input metric key path
- "\n" # Submit monthly description for weekly
- "\n" # Submit monthly name for weekly
- "1\n" # Enum-select: Copy & continue
- "y\n" # Create file
- "y\n" # Create file
- "5\n" # Exit
outputs:
files:
- path: config/metrics/counts_28d/count_distinct_user_id_from_failed_usage_attempts_monthly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_additional_props.yml
- path: config/metrics/counts_7d/count_distinct_user_id_from_failed_usage_attempts_weekly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_additional_props.yml
- path: config/metrics/counts_all/count_distinct_user_id_from_failed_usage_attempts.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_single_event_additional_props.yml
- description: Create a weekly/monthly metric for a single event with all additional properties
inputs:
@ -355,18 +354,13 @@
- "60\n" # Input valid "value"
- "who tried and failed to define an internal event using the CLI\n" # Input description
- "failed_usage_attempts_under_60s\n" # Input metric key path
- "\n" # Submit weekly description for monthly
- "\n" # Submit weekly name for monthly
- "1\n" # Enum-select: Copy & continue
- "y\n" # Create file
- "y\n" # Create file
- "5\n" # Exit
outputs:
files:
- path: config/metrics/counts_28d/count_distinct_user_id_from_failed_usage_attempts_under_60s_monthly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_all_additional_props.yml
- path: config/metrics/counts_7d/count_distinct_user_id_from_failed_usage_attempts_under_60s_weekly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_all_additional_props.yml
- path: config/metrics/counts_all/count_distinct_user_id_from_failed_usage_attempts_under_60s.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_single_event_all_additional_props.yml
- description: Create a weekly/monthly metric for a single event with custom additional properties filters
inputs:
@ -388,18 +382,13 @@
- "12.4\n" # Input value for "custom_key4" filter
- "who tried and failed to define an internal event using the CLI\n" # Input description
- "failed_usage_attempts_under_60s\n" # Input metric key path
- "\n" # Submit weekly description for monthly
- "\n" # Submit weekly name for monthly
- "1\n" # Enum-select: Copy & continue
- "y\n" # Create file
- "y\n" # Create file
- "5\n" # Exit
outputs:
files:
- path: config/metrics/counts_28d/count_distinct_user_id_from_failed_usage_attempts_under_60s_monthly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_custom_key_filter.yml
- path: config/metrics/counts_7d/count_distinct_user_id_from_failed_usage_attempts_under_60s_weekly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_custom_key_filter.yml
- path: config/metrics/counts_all/count_distinct_user_id_from_failed_usage_attempts_under_60s.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_single_event_custom_key_filter.yml
- description: Create a weekly/monthly metric for multiple events with and without additional properties
inputs:
@ -479,17 +468,13 @@
- "\e[A\e[A\e[A\e[A" # Arrow up to: Weekly count of unique values for label
- "\n" # Select: Weekly count of unique values for label
- "values provided for label\n" # Input description
- "\n" # Submit monthly description for weekly
- "1\n" # Enum-select: Copy & continue
- "y\n" # Create file
- "y\n" # Create file
- "5\n" # Exit
outputs:
files:
- path: config/metrics/counts_28d/count_distinct_label_from_internal_events_cli_used_monthly.yml
content: spec/fixtures/scripts/internal_events/metrics/label_28d_single_event_additional_props.yml
- path: config/metrics/counts_7d/count_distinct_label_from_internal_events_cli_used_weekly.yml
content: spec/fixtures/scripts/internal_events/metrics/label_7d_single_event_additional_props.yml
- path: config/metrics/counts_all/count_distinct_label_from_internal_events_cli_used.yml
content: spec/fixtures/scripts/internal_events/metrics/label_single_event_additional_props.yml
- description: Creates metrics with explicitly no product category
inputs:

View File

@ -1,6 +1,6 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_random_name_weekly
description: Weekly count of unique users random metric string
key_path: redis_hll_counters.count_distinct_user_id_from_random_name
description: Count of unique users random metric string
product_group: import_and_integrate
product_categories:
- acquisition
@ -9,7 +9,9 @@ value_type: number
status: active
milestone: '16.6'
introduced_by_url: TODO
time_frame: 7d
time_frame:
- 28d
- 7d
data_source: internal_events
data_category: optional
distribution:

View File

@ -1,24 +0,0 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_random_name_monthly
description: Monthly count of unique users random metric string
product_group: import_and_integrate
product_categories:
- acquisition
performance_indicator_type: []
value_type: number
status: active
milestone: '16.6'
introduced_by_url: TODO
time_frame: 28d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tiers:
- free
- premium
- ultimate
events:
- name: random_name
unique: user.id

View File

@ -1,24 +0,0 @@
---
key_path: redis_hll_counters.count_distinct_label_from_internal_events_cli_used_monthly
description: Monthly count of unique values provided for label
product_group: analytics_instrumentation
product_categories:
- service_ping
performance_indicator_type: []
value_type: number
status: active
milestone: '16.6'
introduced_by_url: TODO
time_frame: 28d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tiers:
- free
- premium
- ultimate
events:
- name: internal_events_cli_used
unique: label

View File

@ -1,6 +1,6 @@
---
key_path: redis_hll_counters.count_distinct_label_from_internal_events_cli_used_weekly
description: Weekly count of unique values provided for label
key_path: redis_hll_counters.count_distinct_label_from_internal_events_cli_used
description: Count of unique values provided for label
product_group: analytics_instrumentation
product_categories:
- service_ping
@ -9,7 +9,9 @@ value_type: number
status: active
milestone: '16.6'
introduced_by_url: TODO
time_frame: 7d
time_frame:
- 28d
- 7d
data_source: internal_events
data_category: optional
distribution:

View File

@ -1,26 +0,0 @@
---
key_path: redis_hll_counters.count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_monthly
description: Monthly count of unique projects where a defition file was created with the CLI
product_group: analytics_instrumentation
product_categories:
- service_ping
performance_indicator_type: []
value_type: number
status: active
milestone: '16.6'
introduced_by_url: TODO
time_frame: 28d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tiers:
- free
- premium
- ultimate
events:
- name: internal_events_cli_closed
unique: project.id
- name: internal_events_cli_used
unique: project.id

View File

@ -1,6 +1,6 @@
---
key_path: redis_hll_counters.count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_weekly
description: Weekly count of unique projects where a defition file was created with the CLI
key_path: redis_hll_counters.count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used
description: Count of unique projects where a defition file was created with the CLI
product_group: analytics_instrumentation
product_categories:
- service_ping
@ -9,7 +9,9 @@ value_type: number
status: active
milestone: '16.6'
introduced_by_url: TODO
time_frame: 7d
time_frame:
- 28d
- 7d
data_source: internal_events
data_category: optional
distribution:

View File

@ -1,30 +0,0 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_failed_usage_attempts_monthly
description: Monthly count of unique users who tried and failed to define an internal event using the CLI
product_group: analytics_instrumentation
product_categories:
- service_ping
performance_indicator_type: []
value_type: number
status: active
milestone: '16.6'
introduced_by_url: TODO
time_frame: 28d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tiers:
- free
- premium
- ultimate
events:
- name: internal_events_cli_used
unique: user.id
filter:
label: failure
- name: internal_events_cli_used
unique: user.id
filter:
label: incomplete

View File

@ -1,46 +0,0 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_failed_usage_attempts_under_60s_monthly
description: Monthly count of unique users who tried and failed to define an internal event using the CLI
product_group: analytics_instrumentation
product_categories:
- service_ping
performance_indicator_type: []
value_type: number
status: active
milestone: '16.6'
introduced_by_url: TODO
time_frame: 28d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tiers:
- free
- premium
- ultimate
events:
- name: internal_events_cli_used
unique: user.id
filter:
label: failure
property: metrics
value: 60
- name: internal_events_cli_used
unique: user.id
filter:
label: failure
property: events
value: 60
- name: internal_events_cli_used
unique: user.id
filter:
label: incomplete
property: metrics
value: 60
- name: internal_events_cli_used
unique: user.id
filter:
label: incomplete
property: events
value: 60

View File

@ -1,36 +0,0 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_failed_usage_attempts_under_60s_monthly
description: Monthly count of unique users who tried and failed to define an internal event using the CLI
product_group: analytics_instrumentation
product_categories:
- service_ping
performance_indicator_type: []
value_type: number
status: active
milestone: '16.6'
introduced_by_url: TODO
time_frame: 28d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tiers:
- free
- premium
- ultimate
events:
- name: internal_events_cli_used
unique: user.id
filter:
label: failure
custom_key1: metrics
custom_key3: 30
custom_key4: 12.4
- name: internal_events_cli_used
unique: user.id
filter:
label: failure
custom_key1: metrics
custom_key3: 13
custom_key4: 12.4

View File

@ -1,6 +1,6 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_internal_events_cli_used_monthly
description: Monthly count of unique users who defined an internal event using the CLI
key_path: redis_hll_counters.count_distinct_user_id_from_internal_events_cli_used
description: Count of unique users who defined an internal event using the CLI
product_group: analytics_instrumentation
product_categories:
- service_ping
@ -9,7 +9,9 @@ value_type: number
status: active
milestone: '16.6'
introduced_by_url: TODO
time_frame: 28d
time_frame:
- 28d
- 7d
data_source: internal_events
data_category: optional
distribution:

View File

@ -1,6 +1,6 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_failed_usage_attempts_weekly
description: Weekly count of unique users who tried and failed to define an internal event using the CLI
key_path: redis_hll_counters.count_distinct_user_id_from_failed_usage_attempts
description: Count of unique users who tried and failed to define an internal event using the CLI
product_group: analytics_instrumentation
product_categories:
- service_ping
@ -9,7 +9,9 @@ value_type: number
status: active
milestone: '16.6'
introduced_by_url: TODO
time_frame: 7d
time_frame:
- 28d
- 7d
data_source: internal_events
data_category: optional
distribution:

View File

@ -1,6 +1,6 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_failed_usage_attempts_under_60s_weekly
description: Weekly count of unique users who tried and failed to define an internal event using the CLI
key_path: redis_hll_counters.count_distinct_user_id_from_failed_usage_attempts_under_60s
description: Count of unique users who tried and failed to define an internal event using the CLI
product_group: analytics_instrumentation
product_categories:
- service_ping
@ -9,7 +9,9 @@ value_type: number
status: active
milestone: '16.6'
introduced_by_url: TODO
time_frame: 7d
time_frame:
- 28d
- 7d
data_source: internal_events
data_category: optional
distribution:

View File

@ -1,6 +1,6 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_failed_usage_attempts_under_60s_weekly
description: Weekly count of unique users who tried and failed to define an internal event using the CLI
key_path: redis_hll_counters.count_distinct_user_id_from_failed_usage_attempts_under_60s
description: Count of unique users who tried and failed to define an internal event using the CLI
product_group: analytics_instrumentation
product_categories:
- service_ping
@ -9,7 +9,9 @@ value_type: number
status: active
milestone: '16.6'
introduced_by_url: TODO
time_frame: 7d
time_frame:
- 28d
- 7d
data_source: internal_events
data_category: optional
distribution:

View File

@ -81,6 +81,7 @@ describe('Protected Tag Edit', () => {
{ user_id: 1, id: 1 },
{ group_id: 1, id: 2 },
{ access_level: 3, id: 3 },
{ deploy_key_id: 1, id: 4 },
];
mockAxios.onPatch().replyOnce(HTTP_STATUS_OK, { [ACCESS_LEVELS.CREATE]: updatedPermissions });
findAccessDropdown().vm.$emit('hidden', [{ user_id: 1 }]);
@ -92,6 +93,7 @@ describe('Protected Tag Edit', () => {
{ user_id: 1, id: 1, type: LEVEL_TYPES.USER },
{ group_id: 1, id: 2, type: LEVEL_TYPES.GROUP },
{ access_level: 3, id: 3, type: LEVEL_TYPES.ROLE },
{ deploy_key_id: 1, id: 4, type: LEVEL_TYPES.DEPLOY_KEY },
];
expect(findAccessDropdown().props('preselectedItems')).toEqual(newPreselected);
});

View File

@ -3,6 +3,9 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RefSelector from '~/ref/components/ref_selector.vue';
import HeaderArea from '~/repository/components/header_area.vue';
import Breadcrumbs from '~/repository/components/header_area/breadcrumbs.vue';
import CodeDropdown from '~/vue_shared/components/code_dropdown/code_dropdown.vue';
import SourceCodeDownloadDropdown from '~/vue_shared/components/download_dropdown/download_dropdown.vue';
import CloneCodeDropdown from '~/vue_shared/components/code_dropdown/clone_code_dropdown.vue';
import BlobControls from '~/repository/components/header_area/blob_controls.vue';
import Shortcuts from '~/behaviors/shortcuts/shortcuts';
import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';
@ -36,6 +39,33 @@ const defaultProvided = {
projectRootPath: '/project/root/path',
comparePath: undefined,
isReadmeView: false,
isFork: false,
needsToFork: true,
gitpodEnabled: false,
isBlob: true,
showEditButton: true,
showWebIdeButton: true,
showGitpodButton: false,
showPipelineEditorUrl: true,
webIdeUrl: 'https://gitlab.com/project/-/ide/',
editUrl: 'https://gitlab.com/project/-/edit/main/',
pipelineEditorUrl: 'https://gitlab.com/project/-/ci/editor',
gitpodUrl: 'https://gitpod.io/#https://gitlab.com/project',
userPreferencesGitpodPath: '/profile/preferences#gitpod',
userProfileEnableGitpodPath: '/profile/preferences?enable_gitpod=true',
httpUrl: 'https://gitlab.com/example-group/example-project.git',
xcodeUrl: 'xcode://clone?repo=https://gitlab.com/example-group/example-project.git',
sshUrl: 'git@gitlab.com:example-group/example-project.git',
kerberosUrl: 'https://kerberos@gitlab.com/example-group/example-project.git',
downloadLinks: [
'https://gitlab.com/example-group/example-project/-/archive/main/example-project-main.zip',
'https://gitlab.com/example-group/example-project/-/archive/main/example-project-main.tar.gz',
'https://gitlab.com/example-group/example-project/-/archive/main/example-project-main.tar.bz2',
'https://gitlab.com/example-group/example-project/-/releases',
],
downloadArtifacts: [
'https://gitlab.com/example-group/example-project/-/jobs/artifacts/main/download?job=build',
],
};
describe('HeaderArea', () => {
@ -45,6 +75,11 @@ describe('HeaderArea', () => {
const findRefSelector = () => wrapper.findComponent(RefSelector);
const findFindFileButton = () => wrapper.findByTestId('tree-find-file-control');
const findCompareButton = () => wrapper.findByTestId('tree-compare-control');
const findWebIdeButton = () => wrapper.findByTestId('js-tree-web-ide-link');
const findCodeDropdown = () => wrapper.findComponent(CodeDropdown);
const findSourceCodeDownloadDropdown = () => wrapper.findComponent(SourceCodeDownloadDropdown);
const findCloneCodeDropdown = () => wrapper.findComponent(CloneCodeDropdown);
const { bindInternalEventDocument } = useMockInternalEventsTracking();
const createComponent = (props = {}, routeName = 'blobPathDecoded', provided = {}) => {
@ -126,6 +161,35 @@ describe('HeaderArea', () => {
expect(findCompareButton().exists()).toBe(true);
});
});
describe('Edit button', () => {
it('renders WebIdeLink component', () => {
expect(findWebIdeButton().exists()).toBe(true);
});
});
describe('CodeDropdown', () => {
it('renders CodeDropdown component with correct props for desktop layout', () => {
expect(findCodeDropdown().exists()).toBe(true);
expect(findCodeDropdown().props('sshUrl')).toBe(defaultProvided.sshUrl);
expect(findCodeDropdown().props('httpUrl')).toBe(defaultProvided.httpUrl);
});
});
describe('SourceCodeDownloadDropdown', () => {
it('renders SourceCodeDownloadDropdown and CloneCodeDropdown component with correct props for mobile layout', () => {
expect(findSourceCodeDownloadDropdown().exists()).toBe(true);
expect(findSourceCodeDownloadDropdown().props('downloadLinks')).toEqual(
defaultProvided.downloadLinks,
);
expect(findSourceCodeDownloadDropdown().props('downloadArtifacts')).toEqual(
defaultProvided.downloadArtifacts,
);
expect(findCloneCodeDropdown().exists()).toBe(true);
expect(findCloneCodeDropdown().props('sshUrl')).toBe(defaultProvided.sshUrl);
expect(findCloneCodeDropdown().props('httpUrl')).toBe(defaultProvided.httpUrl);
});
});
});
describe('when rendered for blob view', () => {
@ -136,6 +200,11 @@ describe('HeaderArea', () => {
expect(blobControls.props('projectPath')).toBe('test/project');
expect(blobControls.props('refType')).toBe('');
});
it('does not render CodeDropdown and SourceCodeDownloadDropdown', () => {
expect(findCodeDropdown().exists()).toBe(false);
expect(findSourceCodeDownloadDropdown().exists()).toBe(false);
});
});
describe('when isReadmeView is true', () => {
@ -147,5 +216,10 @@ describe('HeaderArea', () => {
expect(findRefSelector().exists()).toBe(false);
expect(findBreadcrumbs().exists()).toBe(false);
});
it('does not render CodeDropdown and SourceCodeDownloadDropdown', () => {
expect(findCodeDropdown().exists()).toBe(false);
expect(findSourceCodeDownloadDropdown().exists()).toBe(false);
});
});
});

View File

@ -22,7 +22,7 @@ import {
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
import SnippetHeader, { i18n } from '~/snippets/components/snippet_header.vue';
import SnippetCodeDropdown from '~/vue_shared/components/code_dropdown/snippet_code_dropdown.vue';
import CloneCodeDropdown from '~/vue_shared/components/code_dropdown/clone_code_dropdown.vue';
import ImportedBadge from '~/vue_shared/components/imported_badge.vue';
import DeleteSnippetMutation from '~/snippets/mutations/delete_snippet.mutation.graphql';
import axios from '~/lib/utils/axios_utils';
@ -86,7 +86,7 @@ describe('Snippet header component', () => {
},
},
stubs: {
SnippetCodeDropdown,
CloneCodeDropdown,
GlButton,
GlDisclosureDropdown,
GlDisclosureDropdownGroup,
@ -113,7 +113,7 @@ describe('Snippet header component', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
const findTooltip = () => getBinding(findIcon().element, 'gl-tooltip');
const findSpamIcon = () => wrapper.findByTestId('snippets-spam-icon');
const findCodeDropdown = () => wrapper.findComponent(SnippetCodeDropdown);
const findCodeDropdown = () => wrapper.findComponent(CloneCodeDropdown);
const findImportedBadge = () => wrapper.findComponent(ImportedBadge);
const webUrl = 'http://foo.bar';

View File

@ -1,19 +1,19 @@
import { GlFormInputGroup } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import SnippetCodeDropdown from '~/vue_shared/components/code_dropdown/snippet_code_dropdown.vue';
import CloneCodeDropdown from '~/vue_shared/components/code_dropdown/clone_code_dropdown.vue';
import CodeDropdownItem from '~/vue_shared/components/code_dropdown/code_dropdown_item.vue';
describe('SnippetCodeDropdown', () => {
describe('CloneCodeDropdown', () => {
let wrapper;
const sshLink = 'ssh://foo.bar';
const httpLink = 'http://foo.bar';
const httpsLink = 'https://foo.bar';
const sshUrl = 'ssh://foo.bar';
const httpUrl = 'http://foo.bar';
const httpsUrl = 'https://foo.bar';
const embedUrl = 'http://link.to.snippet';
const embedCode = `<script src="${embedUrl}.js"></script>`;
const defaultPropsData = {
sshLink,
httpLink,
sshUrl,
httpUrl,
url: embedUrl,
embeddable: true,
};
@ -24,13 +24,14 @@ describe('SnippetCodeDropdown', () => {
const findCopyHttpUrlButton = () => wrapper.findComponentByTestId('copy-http-url');
const findCopyEmbeddedCodeButton = () => wrapper.findComponentByTestId('copy-embedded-code');
const findCopyShareUrlButton = () => wrapper.findComponentByTestId('copy-share-url');
const findCopyKRB5UrlButton = () => wrapper.findComponentByTestId('copy-kerberos-url');
const createComponent = (propsData = defaultPropsData) => {
wrapper = shallowMountExtended(SnippetCodeDropdown, {
wrapper = shallowMountExtended(CloneCodeDropdown, {
propsData,
stubs: {
GlFormInputGroup,
SnippetCodeDropdown: stubComponent(SnippetCodeDropdown),
CloneCodeDropdown: stubComponent(CloneCodeDropdown),
},
});
};
@ -38,8 +39,8 @@ describe('SnippetCodeDropdown', () => {
describe('rendering', () => {
it.each`
name | index | link
${'SSH'} | ${0} | ${sshLink}
${'HTTP'} | ${1} | ${httpLink}
${'SSH'} | ${0} | ${sshUrl}
${'HTTP'} | ${1} | ${httpUrl}
${'Embed'} | ${2} | ${embedCode}
${'Share'} | ${3} | ${embedUrl}
`('renders correct link and a copy-button for $name', ({ index, link }) => {
@ -50,9 +51,9 @@ describe('SnippetCodeDropdown', () => {
});
it.each`
name | finder | value
${'sshLink'} | ${findCopySshUrlButton} | ${sshLink}
${'httpLink'} | ${findCopyHttpUrlButton} | ${httpLink}
name | finder | value
${'sshUrl'} | ${findCopySshUrlButton} | ${sshUrl}
${'httpUrl'} | ${findCopyHttpUrlButton} | ${httpUrl}
`('does not fail if only $name is set', ({ name, finder, value }) => {
createComponent({ [name]: value, url: embedCode });
@ -67,11 +68,30 @@ describe('SnippetCodeDropdown', () => {
expect(findCopyEmbeddedCodeButton().exists()).toBe(false);
expect(findCopyShareUrlButton().exists()).toBe(false);
});
it('does not render Embed and Share items, if url is not provided', () => {
createComponent({ ...defaultPropsData, url: '' });
expect(findCopyEmbeddedCodeButton().exists()).toBe(false);
expect(findCopyShareUrlButton().exists()).toBe(false);
});
it('does not render KRB5 link, if kerberosUrl is not provided', () => {
createComponent();
expect(findCopyKRB5UrlButton().exists()).toBe(false);
});
it('renders KRB5 link, if kerberosUrl is provided', () => {
createComponent({ ...defaultPropsData, kerberosUrl: 'http://:@gitlab.com/project-2.git' });
expect(findCopyKRB5UrlButton().exists()).toBe(true);
});
});
describe('functionality', () => {
it('correctly calculates httpLabel for HTTPS protocol', () => {
createComponent({ ...defaultPropsData, httpLink: httpsLink });
createComponent({ ...defaultPropsData, httpUrl: httpsUrl });
expect(findCopyHttpUrlButton().props('label')).toContain('HTTPS');
});

View File

@ -46,6 +46,8 @@ describe('autocompleteDataSources', () => {
mergeRequests:
'/foobar/project/group/-/autocomplete_sources/merge_requests?type=WorkItem&work_item_type_id=2',
epics: '/foobar/project/group/-/autocomplete_sources/epics?type=WorkItem&work_item_type_id=2',
milestones:
'/foobar/project/group/-/autocomplete_sources/milestones?type=WorkItem&work_item_type_id=2',
});
});
@ -58,6 +60,7 @@ describe('autocompleteDataSources', () => {
mergeRequests:
'/foobar/project/group/-/autocomplete_sources/merge_requests?type=WorkItem&type_id=2',
epics: '/foobar/project/group/-/autocomplete_sources/epics?type=WorkItem&type_id=2',
milestones: '/foobar/project/group/-/autocomplete_sources/milestones?type=WorkItem&type_id=2',
});
});
@ -81,6 +84,8 @@ describe('autocompleteDataSources', () => {
mergeRequests:
'/foobar/groups/group/-/autocomplete_sources/merge_requests?type=WorkItem&work_item_type_id=2',
epics: '/foobar/groups/group/-/autocomplete_sources/epics?type=WorkItem&work_item_type_id=2',
milestones:
'/foobar/groups/group/-/autocomplete_sources/milestones?type=WorkItem&work_item_type_id=2',
});
});
@ -99,6 +104,7 @@ describe('autocompleteDataSources', () => {
mergeRequests:
'/foobar/groups/group/-/autocomplete_sources/merge_requests?type=WorkItem&type_id=2',
epics: '/foobar/groups/group/-/autocomplete_sources/epics?type=WorkItem&type_id=2',
milestones: '/foobar/groups/group/-/autocomplete_sources/milestones?type=WorkItem&type_id=2',
});
});
});

View File

@ -286,7 +286,7 @@ RSpec.describe API::Helpers, feature_category: :shared do
)
allow(helper).to receive(:route_authentication_setting).and_return({})
allow(helper).to receive(:route_setting).with(:authorization).and_return(job_token_policy: job_token_policy)
allow(helper).to receive(:route_setting).with(:authorization).and_return(job_token_policies: job_token_policy)
allow(user).to receive(:ci_job_token_scope).and_return(user.set_ci_job_token_scope!(job))
end
@ -301,11 +301,24 @@ RSpec.describe API::Helpers, feature_category: :shared do
expect(helper)
.to receive(:forbidden!)
.with("Insufficient permissions to access this resource in project #{project.path}. " \
"The following token permission is required: #{job_token_policy}.")
'The following token permission is required: not_allowed_policy.')
find_project!
end
context 'when multiple policies are required' do
let_it_be(:job_token_policy) { [:policy_1, :policy_2] }
it 'returns forbidden' do
expect(helper)
.to receive(:forbidden!)
.with("Insufficient permissions to access this resource in project #{project.path}. " \
'The following token permissions are required: policy_1 and policy_2.')
find_project!
end
end
context 'when the `enforce_job_token_policies` feature flag is disabled' do
before do
stub_feature_flags(enforce_job_token_policies: false)
@ -319,7 +332,7 @@ RSpec.describe API::Helpers, feature_category: :shared do
let_it_be(:job_token_policy) { nil }
it 'returns forbidden' do
expect(helper).to receive(:forbidden!).with('This action is not authorized for CI/CD job tokens.')
expect(helper).to receive(:forbidden!).with('This action is unauthorized for CI/CD job tokens.')
find_project!
end
@ -332,6 +345,18 @@ RSpec.describe API::Helpers, feature_category: :shared do
it { is_expected.to eq project }
end
end
context "when route settings don't exist" do
before do
allow(helper).to receive(:respond_to?).with(:route_setting).and_return(false)
end
it 'returns forbidden' do
expect(helper).to receive(:forbidden!).with('This action is unauthorized for CI/CD job tokens.')
find_project!
end
end
end
end

View File

@ -113,9 +113,7 @@ RSpec.describe Authn::Tokens::Jwt, feature_category: :system_access do
context 'with wrong subject type' do
let(:subject_type) { Project }
it 'raises a Gitlab::Graphql::Errors::ArgumentError' do
expect { jwt_subject }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end
it { is_expected.to be_nil }
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillIssuableSeveritiesNamespaceId,
feature_category: :team_planning,
schema: 20241125145005 do
include_examples 'desired sharding key backfill job' do
let(:batch_table) { :issuable_severities }
let(:backfill_column) { :namespace_id }
let(:backfill_via_table) { :issues }
let(:backfill_via_column) { :namespace_id }
let(:backfill_via_foreign_key) { :issue_id }
end
end

View File

@ -903,6 +903,7 @@ project:
- observability_logs
- security_exclusions
- security_statistics
- vulnerability_management_policies
award_emoji:
- awardable
- user

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe QueueBackfillIssuableSeveritiesNamespaceId, feature_category: :team_planning do
let!(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do
reversible_migration do |migration|
migration.before -> {
expect(batched_migration).not_to have_scheduled_batched_migration
}
migration.after -> {
expect(batched_migration).to have_scheduled_batched_migration(
table_name: :issuable_severities,
column_name: :id,
interval: described_class::DELAY_INTERVAL,
batch_size: described_class::BATCH_SIZE,
sub_batch_size: described_class::SUB_BATCH_SIZE,
gitlab_schema: :gitlab_main_cell,
job_arguments: [
:namespace_id,
:issues,
:namespace_id,
:issue_id
]
)
}
end
end
end

View File

@ -229,32 +229,39 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f
end
end
describe '#policy_allowed?' do
subject { scope.policy_allowed?(accessed_project, policy) }
describe '#policies_allowed?' do
subject { scope.policies_allowed?(accessed_project, policies) }
let(:scope) { described_class.new(target_project) }
let_it_be(:target_project) { create(:project) }
let_it_be(:allowed_policy) { ::Ci::JobToken::Policies.all_policies.pick(:value) }
let_it_be(:policy) { allowed_policy }
let_it_be(:accessed_project) { create_inbound_accessible_project_for_policies(target_project, [allowed_policy]) }
let(:accessed_project) { create_inbound_accessible_project_for_policies(target_project, [allowed_policy]) }
context 'when no policies are given' do
let_it_be(:policies) { [] }
it { is_expected.to be(false) }
end
context 'when the policies are defined in the scope' do
let_it_be(:policies) { [allowed_policy] }
context 'when the policy is defined in the scope' do
it { is_expected.to be(true) }
context 'when the accessed project is not inbound accessible' do
let_it_be(:accessed_project) { create(:project) }
let(:accessed_project) { create(:project) }
it { is_expected.to be(false) }
end
end
context 'when the policy is not defined in the scope' do
let_it_be(:policy) { :not_allowed_policy }
context 'when the policy are not defined in the scope' do
let_it_be(:policies) { [:not_allowed_policy] }
it { is_expected.to be(false) }
context 'when the accessed project is the target project' do
let_it_be(:accessed_project) { target_project }
let(:accessed_project) { target_project }
it { is_expected.to be(true) }
end
@ -268,7 +275,7 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f
end
context 'when the accessed project has not enabled fine grained permissions' do
let_it_be(:accessed_project) { create_inbound_accessible_project(target_project) }
let(:accessed_project) { create_inbound_accessible_project(target_project) }
it { is_expected.to be(true) }
end

View File

@ -1607,6 +1607,27 @@ RSpec.describe User, feature_category: :user_profile do
expect(described_class.by_ids(user_ids)).to contain_exactly(first_user, second_user)
end
end
describe '.by_bot_namespace_ids' do
let_it_be(:group) { create(:group) }
let_it_be(:project_namespace) { create(:project_namespace) }
let_it_be(:other_group) { create(:group) }
let_it_be(:other_user) { create(:user, user_type: :project_bot) }
let_it_be(:user_with_group) { create(:user, user_type: :project_bot) }
let_it_be(:user_with_project) { create(:user, user_type: :project_bot) }
before do
user_with_group.update!(bot_namespace: group)
user_with_project.update!(bot_namespace: project_namespace)
other_user.update!(bot_namespace: other_group)
end
it 'returns users for the given bot_namespace_ids' do
expect(described_class.by_bot_namespace_ids([group, project_namespace]))
.to contain_exactly(user_with_group, user_with_project)
end
end
end
context 'strip attributes' do

View File

@ -24,4 +24,8 @@ RSpec.describe WorkItems::Widgets::Milestone do
it { is_expected.to eq(work_item.milestone) }
end
describe '.quick_action_commands' do
it { expect(described_class.quick_action_commands).to match_array([:milestone, :remove_milestone]) }
end
end

View File

@ -241,6 +241,23 @@ RSpec.describe API::Deployments, feature_category: :continuous_delivery do
)
end
it_behaves_like 'enforcing job token policies', [:admin_deployments, :admin_environments] do
# let(:accessed_project) { project }
let(:request) do
post(
api("/projects/#{project.id}/deployments"),
params: {
job_token: job.token,
environment: 'production',
sha: sha,
ref: 'master',
tag: false,
status: 'success'
}
)
end
end
context 'as a maintainer' do
it 'creates a new deployment' do
post(

View File

@ -125,7 +125,7 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do
it_behaves_like 'enforcing job token policies', :read_environments do
let(:request) do
get api("/projects/#{accessed_project.id}/environments"), params: { job_token: job.token }
get api("/projects/#{project.id}/environments"), params: { job_token: job.token }
end
end
end

View File

@ -48,8 +48,7 @@ RSpec.describe 'InternalEventsCli::Flows::MetricDefiner', :aggregate_failures, f
" ", # Multi-select: __event2
"\n", # Submit selections
"\n", # Select: Weekly/Monthly count of unique users
"aggregate metric description\n", # Submit description
"\n" # Accept description for weekly
"aggregate metric description\n" # Submit description
])
# Filter down to "dev" options
@ -81,8 +80,7 @@ RSpec.describe 'InternalEventsCli::Flows::MetricDefiner', :aggregate_failures, f
" ", # Multi-select: __event3
"\n", # Submit selections
"\n", # Select: Weekly/Monthly count of unique users
"aggregate metric description\n", # Submit description
"\n" # Accept description for weekly
"aggregate metric description\n" # Submit description
])
# Filter down to "dev:create" options
@ -118,7 +116,6 @@ RSpec.describe 'InternalEventsCli::Flows::MetricDefiner', :aggregate_failures, f
"\n", # Select: 00__event1
"\n", # Select: Weekly/Monthly count of unique users
"aggregate metric description\n", # Submit description
"\n", # Accept description for weekly
"2\n" # Modify attributes
])
@ -220,14 +217,9 @@ RSpec.describe 'InternalEventsCli::Flows::MetricDefiner', :aggregate_failures, f
# existing metrics which use both events
File.write(
'config/metrics/counts_7d/' \
'count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_weekly.yml',
File.read('spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml')
)
File.write(
'config/metrics/counts_28d/' \
'count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_monthly.yml',
File.read('spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml')
'config/metrics/counts_all/' \
'count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used.yml',
File.read('spec/fixtures/scripts/internal_events/metrics/project_id_multiple_events.yml')
)
# Non-conflicting metric which uses only one of the events
@ -434,4 +426,77 @@ RSpec.describe 'InternalEventsCli::Flows::MetricDefiner', :aggregate_failures, f
end
end
end
context 'when succeeded in saving the file' do
let(:events) do
[{
action: 'internal_events_cli_closed', internal_events: true, product_group: 'optimize', tiers: ['ultimate']
}, {
action: 'internal_events_cli_used', internal_events: true, product_group: 'optimize', tiers: ['ultimate']
}]
end
before do
events.each do |event|
File.write("config/events/#{event[:action]}.yml", event.transform_keys(&:to_s).to_yaml)
end
end
context "when creating a single metric" do
it 'shows link to the metric dashboard' do
queue_cli_inputs([
"2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
"2\n", # Enum-select: Multiple events -- count occurrences of several separate events or interactions
'internal_events_cli', # Filters to the relevant events
' ', # Multi-select: internal_events_cli_closed
"\e[B", # Arrow down to: internal_events_cli_used
' ', # Multi-select: internal_events_cli_used
"\n", # Submit selections
"\e[B", # Arrow down to: Total count
"\n", # Select: Total count
"where a definition file was created with the CLI\n", # Input description
"1\n", # Select: Copy & continue
"\e[B \n", # Skip product categories
"y\n" # Create file
])
expected_output = <<~TEXT.chomp
- Metric trend dashboard: https://10az.online.tableau.com/#/site/gitlab/views/PDServicePingExplorationDashboard/MetricTrend?Metrics%20Path=counts.count_total_internal_events_cli_closed_and_internal_events_cli_used
TEXT
with_cli_thread do
expect { plain_last_lines }.to eventually_include_cli_text(expected_output)
end
end
end
context "when creating a multiple metrics" do
it 'shows link to the metric dashboard' do
queue_cli_inputs([
"2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
"2\n", # Enum-select: Multiple events -- count occurrences of several separate events or interactions
'internal_events_cli', # Filters to the relevant events
' ', # Multi-select: internal_events_cli_closed
"\e[B", # Arrow down to: internal_events_cli_used
' ', # Multi-select: internal_events_cli_used
"\n", # Submit selections
"\n", # Select: Weekly/Monthly count
"where a definition file was created with the CLI\n", # Input description
"1\n", # Select: Copy & continue
"\e[B \n", # Skip product categories
"y\n" # Create file
])
expected_output = <<-TEXT.chomp # <<- used instead of <<~ to save indentation
- Metric trend dashboards:
- https://10az.online.tableau.com/#/site/gitlab/views/PDServicePingExplorationDashboard/MetricTrend?Metrics%20Path=counts.count_total_internal_events_cli_closed_and_internal_events_cli_used_monthly
- https://10az.online.tableau.com/#/site/gitlab/views/PDServicePingExplorationDashboard/MetricTrend?Metrics%20Path=counts.count_total_internal_events_cli_closed_and_internal_events_cli_used_weekly
TEXT
with_cli_thread do
expect { plain_last_lines }.to eventually_include_cli_text(expected_output)
end
end
end
end
end

View File

@ -131,6 +131,111 @@ RSpec.describe 'InternalEventsCli::Flows::UsageViewer', :aggregate_failures, fea
end
end
context 'for an event with multiple metrics' do
let(:expected_rails_example) do
<<~TEXT.chomp
--------------------------------------------------
# RAILS
include Gitlab::InternalEventsTracking
track_internal_event(
'internal_events_cli_used',
project: project,
user: user
)
--------------------------------------------------
TEXT
end
let(:expected_rspec_example) do
<<~TEXT.chomp
--------------------------------------------------
# RSPEC
it_behaves_like 'internal event tracking' do
let(:event) { 'internal_events_cli_used' }
let(:project) { create(:project) }
let(:user) { create(:user) }
end
--------------------------------------------------
TEXT
end
let(:expected_gdk_example) do
<<~TEXT.chomp
--------------------------------------------------
# RAILS CONSOLE -- generate service ping payload, including most recent usage data
require_relative 'spec/support/helpers/service_ping_helpers.rb'
# Get current value of a metric
ServicePingHelpers.get_current_usage_metric_value('redis_hll_counters.count_distinct_user_id_from_internal_events_cli_used_monthly')
ServicePingHelpers.get_current_usage_metric_value('redis_hll_counters.count_distinct_user_id_from_internal_events_cli_used_weekly')
# View entire service ping payload
ServicePingHelpers.get_current_service_ping_payload
--------------------------------------------------
TEXT
end
let(:expected_tableau_example) do
<<~TEXT.chomp
--------------------------------------------------
# GROUP DASHBOARDS -- view all service ping metrics for a specific group
analytics_instrumentation: https://10az.online.tableau.com/#/site/gitlab/views/PDServicePingExplorationDashboard/MetricExplorationbyGroup?Group%20Name=analytics_instrumentation&Stage%20Name=monitor
--------------------------------------------------
# METRIC TRENDS -- view data for a service ping metric for internal_events_cli_used
redis_hll_counters.count_distinct_user_id_from_internal_events_cli_used_monthly: https://10az.online.tableau.com/#/site/gitlab/views/PDServicePingExplorationDashboard/MetricTrend?Metrics%20Path=redis_hll_counters.count_distinct_user_id_from_internal_events_cli_used_monthly
redis_hll_counters.count_distinct_user_id_from_internal_events_cli_used_weekly: https://10az.online.tableau.com/#/site/gitlab/views/PDServicePingExplorationDashboard/MetricTrend?Metrics%20Path=redis_hll_counters.count_distinct_user_id_from_internal_events_cli_used_weekly
--------------------------------------------------
Note: The metric dashboard links can also be accessed from https://metrics.gitlab.com/
Not what you're looking for? Check this doc:
- https://docs.gitlab.com/ee/development/internal_analytics/#data-discovery
TEXT
end
before do
File.write(event1_filepath, File.read(event1_content))
File.write(
'config/metrics/counts_all/count_distinct_user_id_from_internal_events_cli_used.yml',
File.read('spec/fixtures/scripts/internal_events/metrics/user_id_single_event.yml')
)
end
it 'shows backend examples for all metrics' do
queue_cli_inputs([
"3\n", # Enum-select: View Usage -- look at code examples for an existing event
'internal_events_cli_used', # Filters to this event
"\n", # Select: config/events/internal_events_cli_used.yml
"\n", # Select: ruby/rails
"\e[B", # Arrow down to: rspec
"\n", # Select: rspec
"7\n", # Select: Manual testing: check current values of metrics from rails console (any data source)
"8\n", # Select: Data verification in Tableau
"Exit", # Filters to this item
"\n" # select: Exit
])
with_cli_thread do
expect { plain_last_lines(200) }.to eventually_include_cli_text(
expected_example_prompt,
expected_rails_example,
expected_rspec_example,
expected_gdk_example,
expected_tableau_example
)
end
end
end
context 'for an event without identifiers' do
let(:expected_rails_example) do
<<~TEXT.chomp

View File

@ -41,7 +41,7 @@ RSpec.describe InternalEventsCli::Helpers::MetricOptions::Option, feature_catego
context 'when option is for a supported and not yet defined metric' do
it 'highlights key words in the name' do
expect(option.formatted).to eq({
name: "<cyan>time frame</cyan> count of <cyan>unique users</cyan> who triggered a list of events",
name: "<cyan>Time frame</cyan> count of <cyan>unique users</cyan> who triggered a list of events",
value: metrics
})
end
@ -51,7 +51,7 @@ RSpec.describe InternalEventsCli::Helpers::MetricOptions::Option, feature_catego
it 'highlights key words in the name' do
expect(option.formatted).to eq({
name: "<cyan>time frame</cyan> count of <cyan>unique users</cyan> who triggered a list of events " \
name: "<cyan>Time frame</cyan> count of <cyan>unique users</cyan> who triggered a list of events " \
"<cyan>where label/prop/anything</cyan> is...",
value: metrics
})
@ -64,7 +64,7 @@ RSpec.describe InternalEventsCli::Helpers::MetricOptions::Option, feature_catego
it 'formats the option as disabled' do
expect(option.formatted).to eq({
name: "<bright_black>time frame count of unique users who triggered a list of events</bright_black>",
name: "<bright_black>Time frame count of unique users who triggered a list of events</bright_black>",
value: metrics,
disabled: "<bold><bright_black>(already defined)</bright_black></bold>"
})
@ -75,7 +75,7 @@ RSpec.describe InternalEventsCli::Helpers::MetricOptions::Option, feature_catego
it 'highlights key words in the name' do
expect(option.formatted).to eq({
name: "<bright_black>time frame count of unique users who triggered " \
name: "<bright_black>Time frame count of unique users who triggered " \
"a list of events where filtered</bright_black>",
value: metrics,
disabled: "<bold><bright_black>(already defined)</bright_black></bold>"
@ -89,7 +89,7 @@ RSpec.describe InternalEventsCli::Helpers::MetricOptions::Option, feature_catego
it 'formats the option as disabled' do
expect(option.formatted).to eq({
name: "<bright_black>time frame count of unique users who triggered a list of events</bright_black>",
name: "<bright_black>Time frame count of unique users who triggered a list of events</bright_black>",
value: metrics,
disabled: "<bold><bright_black>(user unavailable)</bright_black></bold>"
})
@ -100,7 +100,7 @@ RSpec.describe InternalEventsCli::Helpers::MetricOptions::Option, feature_catego
it 'highlights key words in the name' do
expect(option.formatted).to eq({
name: "<bright_black>time frame count of unique users who triggered " \
name: "<bright_black>Time frame count of unique users who triggered " \
"a list of events where filtered</bright_black>",
value: metrics,
disabled: "<bold><bright_black>(user unavailable)</bright_black></bold>"
@ -114,7 +114,7 @@ RSpec.describe InternalEventsCli::Helpers::MetricOptions::Option, feature_catego
it 'highlights key words in the name' do
expect(option.formatted).to eq({
name: "<cyan>time frame</cyan> count of <cyan>unique values for 'label'</cyan> " \
name: "<cyan>Time frame</cyan> count of <cyan>unique values for 'label'</cyan> " \
"from a list of events occurrences",
value: metrics
})
@ -126,7 +126,7 @@ RSpec.describe InternalEventsCli::Helpers::MetricOptions::Option, feature_catego
it 'highlights key words in the name' do
expect(option.formatted).to eq({
name: "<cyan>time frame</cyan> count of a list of events occurrences",
name: "<cyan>Time frame</cyan> count of a list of events occurrences",
value: metrics
})
end
@ -144,7 +144,7 @@ RSpec.describe InternalEventsCli::Helpers::MetricOptions::Option, feature_catego
it 'highlights key words in the name' do
expect(option.formatted).to eq({
name: "<cyan>time frame 1/time frame 2</cyan> count of <cyan>unique users</cyan> " \
name: "<cyan>Time frame 1/Time frame 2</cyan> count of <cyan>unique users</cyan> " \
"who triggered a list of events",
value: metrics
})

View File

@ -82,14 +82,12 @@ RSpec.describe Cli, feature_category: :service_ping do
"\e[B", # Arrow down to: Weekly count of unique projects
"\n", # Select: Weekly count of unique projects
"where a defition file was created with the CLI\n", # Input description
"\n", # Submit weekly description for monthly
"2\n", # Select: Modify attributes
"\n", # Accept group
"\n", # Accept product categories
"\n", # Skip URL
"1\n", # Select: [free, premium, ultimate]
"y\n", # Create file
"y\n", # Create file
"5\n" # Exit
]
end
@ -104,11 +102,8 @@ RSpec.describe Cli, feature_category: :service_ping do
let(:output_files) do
# rubocop:disable Layout/LineLength -- Long filepaths read better unbroken
[{
'path' => 'config/metrics/counts_28d/count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_monthly.yml',
'content' => 'spec/fixtures/scripts/internal_events/metrics/project_id_28d_multiple_events.yml'
}, {
'path' => 'config/metrics/counts_7d/count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used_weekly.yml',
'content' => 'spec/fixtures/scripts/internal_events/metrics/project_id_7d_multiple_events.yml'
'path' => 'config/metrics/counts_all/count_distinct_project_id_from_internal_events_cli_closed_and_internal_events_cli_used.yml',
'content' => 'spec/fixtures/scripts/internal_events/metrics/project_id_multiple_events.yml'
}]
# rubocop:enable Layout/LineLength
end

View File

@ -34,13 +34,18 @@ RSpec.describe Ci::Catalog::Resources::AggregateLast30DayUsageService, :clean_gi
(1..i + 1).each do |j|
component = create(:ci_catalog_resource_component, version: version, name: "component#{j}")
(1..usage_count).each do |k|
(1..usage_count).each do |mock_used_by_project_id|
# Inside the usage window
create(:ci_catalog_resource_component_usage,
component: component, used_date: usage_start_date, used_by_project_id: k)
component: component, used_date: usage_start_date, used_by_project_id: mock_used_by_project_id)
# Outside the usage window
create(:ci_catalog_resource_component_usage,
component: component, used_date: usage_start_date - k.days, used_by_project_id: k)
component: component, used_date: usage_start_date - mock_used_by_project_id.days,
used_by_project_id: mock_used_by_project_id)
# create new usage records in the window
create(:catalog_resource_component_last_usage, component: component, last_used_date: usage_start_date,
used_by_project_id: mock_used_by_project_id)
end
end
end
@ -49,56 +54,25 @@ RSpec.describe Ci::Catalog::Resources::AggregateLast30DayUsageService, :clean_gi
Ci::Catalog::Resource.update_all(last_30_day_usage_count_updated_at: initial_usage_count_updated_at)
end
describe '#execute' do
context 'when the aggregator is not interrupted' do
shared_examples 'aggregates usage data for all catalog resources' do
it 'returns a success response' do
response = service.execute
context 'when storing usage data in p_catalog_resource_component_usages' do
before do
stub_feature_flags(ci_catalog_ranking_from_new_usage_table: false)
end
expect(response).to be_success
expect(response.payload).to eq({
total_targets_completed: 4,
cursor_attributes: expected_cursor_attributes
})
end
end
describe '#execute' do
context 'when the aggregator is not interrupted' do
shared_examples 'aggregates usage data for all catalog resources' do
it 'returns a success response' do
response = service.execute
it_behaves_like 'aggregates usage data for all catalog resources'
it 'calls BulkUpdate once and updates usage counts for all catalog resources' do
expect(Gitlab::Database::BulkUpdate).to receive(:execute).once.and_call_original
service.execute
expect(ordered_usage_counts).to eq(expected_ordered_usage_counts)
expect(ordered_usage_counts_updated_at).to match_array([Time.current] * 4)
end
context 'when there are two batches of usage counts' do
before do
stub_const('Gitlab::Ci::Components::Usages::Aggregator::TARGET_BATCH_SIZE', 2)
end
it_behaves_like 'aggregates usage data for all catalog resources'
it 'calls BulkUpdate twice and updates usage counts for all catalog resources' do
expect(Gitlab::Database::BulkUpdate).to receive(:execute).twice.and_call_original
service.execute
expect(ordered_usage_counts).to eq(expected_ordered_usage_counts)
expect(ordered_usage_counts_updated_at).to match_array([Time.current] * 4)
end
end
context 'when some catalog resources have already been processed today' do
before_all do
resources.first(2).each do |resource|
resource.update!(last_30_day_usage_count_updated_at: Date.today.to_time)
expect(response).to be_success
expect(response.payload).to eq({
total_targets_completed: 4,
cursor_attributes: expected_cursor_attributes
})
end
end
# The cursor has not advanced so it still processes all targets
it_behaves_like 'aggregates usage data for all catalog resources'
it 'calls BulkUpdate once and updates usage counts for all catalog resources' do
@ -109,26 +83,50 @@ RSpec.describe Ci::Catalog::Resources::AggregateLast30DayUsageService, :clean_gi
expect(ordered_usage_counts).to eq(expected_ordered_usage_counts)
expect(ordered_usage_counts_updated_at).to match_array([Time.current] * 4)
end
end
context 'when all catalog resources have already been processed today' do
before_all do
Ci::Catalog::Resource.update_all(last_30_day_usage_count_updated_at: Date.today.to_time)
context 'when there are two batches of usage counts' do
before do
stub_const('Gitlab::Ci::Components::Usages::Aggregator::TARGET_BATCH_SIZE', 2)
end
it_behaves_like 'aggregates usage data for all catalog resources'
it 'calls BulkUpdate twice and updates usage counts for all catalog resources' do
expect(Gitlab::Database::BulkUpdate).to receive(:execute).twice.and_call_original
service.execute
expect(ordered_usage_counts).to eq(expected_ordered_usage_counts)
expect(ordered_usage_counts_updated_at).to match_array([Time.current] * 4)
end
end
it 'does not aggregate usage data' do
expect(Gitlab::Ci::Components::Usages::Aggregator).not_to receive(:new)
context 'when some catalog resources have already been processed today' do
before_all do
resources.first(2).each do |resource|
resource.update!(last_30_day_usage_count_updated_at: Date.today.to_time)
end
end
response = service.execute
# The cursor has not advanced so it still processes all targets
it_behaves_like 'aggregates usage data for all catalog resources'
expect(response).to be_success
expect(response.message).to eq("Processing complete for #{Date.today}")
expect(response.payload).to eq({})
it 'calls BulkUpdate once and updates usage counts for all catalog resources' do
expect(Gitlab::Database::BulkUpdate).to receive(:execute).once.and_call_original
service.execute
expect(ordered_usage_counts).to eq(expected_ordered_usage_counts)
expect(ordered_usage_counts_updated_at).to match_array([Time.current] * 4)
end
end
context 'when a new catalog resource is added today' do
context 'when all catalog resources have already been processed today' do
before_all do
Ci::Catalog::Resource.update_all(last_30_day_usage_count_updated_at: Date.today.to_time)
end
it 'does not aggregate usage data' do
create(:ci_catalog_resource)
expect(Gitlab::Ci::Components::Usages::Aggregator).not_to receive(:new)
response = service.execute
@ -137,49 +135,108 @@ RSpec.describe Ci::Catalog::Resources::AggregateLast30DayUsageService, :clean_gi
expect(response.message).to eq("Processing complete for #{Date.today}")
expect(response.payload).to eq({})
end
context 'when a new catalog resource is added today' do
it 'does not aggregate usage data' do
create(:ci_catalog_resource)
expect(Gitlab::Ci::Components::Usages::Aggregator).not_to receive(:new)
response = service.execute
expect(response).to be_success
expect(response.message).to eq("Processing complete for #{Date.today}")
expect(response.payload).to eq({})
end
end
end
end
context 'when the aggregator is interrupted' do
before do
# Sets the aggregator to break after the first iteration on each run
stub_const('Gitlab::Ci::Components::Usages::Aggregator::MAX_RUNTIME', 0)
stub_const('Gitlab::Ci::Components::Usages::Aggregator::DISTINCT_USAGE_BATCH_SIZE', 2)
end
it 'updates the expected usage counts for each run' do
# On 1st run, we get an incomplete usage count for the first catalog resource so it is not saved
expect { service.execute }
.to not_change { ordered_usage_counts }
.and not_change { ordered_usage_counts_updated_at }
# On 2nd run, we get the complete usage count for the first catalog resource and save it
service.execute
expect(ordered_usage_counts).to eq([expected_ordered_usage_counts.first, 0, 0, 0])
expect(ordered_usage_counts_updated_at).to eq([Time.current, [initial_usage_count_updated_at] * 3].flatten)
# Execute service repeatedly until done
30.times do
response = service.execute
break if response.payload[:cursor_attributes][:target_id] == 0
end
expect(ordered_usage_counts).to eq(expected_ordered_usage_counts)
expect(ordered_usage_counts_updated_at).to match_array([Time.current] * 4)
end
end
context 'when another instance is running with the same lease key' do
it 'returns a success response with the lease key' do
lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute).tap(&:try_obtain)
response = service.execute
expect(response).to be_success
expect(response.message).to eq('Lease taken')
expect(response.payload).to eq({ lease_key: lease_key })
lease.cancel
end
end
end
end
context 'when the aggregator is interrupted' do
before do
# Sets the aggregator to break after the first iteration on each run
stub_const('Gitlab::Ci::Components::Usages::Aggregator::MAX_RUNTIME', 0)
stub_const('Gitlab::Ci::Components::Usages::Aggregator::DISTINCT_USAGE_BATCH_SIZE', 2)
end
it 'updates the expected usage counts for each run' do
# On 1st run, we get an incomplete usage count for the first catalog resource so it is not saved
expect { service.execute }
.to not_change { ordered_usage_counts }
.and not_change { ordered_usage_counts_updated_at }
# On 2nd run, we get the complete usage count for the first catalog resource and save it
context 'when storing usage data in catalog_resource_component_last_usages' do
describe '#execute' do
it 'updates component usage counts' do
service.execute
expect(ordered_usage_counts).to eq([expected_ordered_usage_counts.first, 0, 0, 0])
expect(ordered_usage_counts_updated_at).to eq([Time.current, [initial_usage_count_updated_at] * 3].flatten)
# Execute service repeatedly until done
30.times do
response = service.execute
break if response.payload[:cursor_attributes][:target_id] == 0
resources.each do |resource|
resource.components.each do |component|
expect(component.reload.last_30_day_usage_count).to eq(
component.last_usages.select(:used_by_project_id).distinct.count
)
end
end
expect(ordered_usage_counts).to eq(expected_ordered_usage_counts)
expect(ordered_usage_counts_updated_at).to match_array([Time.current] * 4)
end
end
context 'when another instance is running with the same lease key' do
it 'returns a success response with the lease key' do
lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute).tap(&:try_obtain)
it 'updates resource usage counts' do
service.execute
resources.each do |resource|
expected_count = resource.components.sum(:last_30_day_usage_count)
expect(resource.reload.last_30_day_usage_count).to eq(expected_count)
end
end
it 'returns a success response' do
response = service.execute
expect(response).to be_success
expect(response.message).to eq('Lease taken')
expect(response.payload).to eq({ lease_key: lease_key })
lease.cancel
expect(response.message).to eq('Usage counts updated for components and resources')
end
context 'when there are no components' do
before do
Ci::Catalog::Resources::Component.delete_all
end
it 'updates resource counts to zero' do
service.execute
resources.each do |resource|
expect(resource.reload.last_30_day_usage_count).to eq(0)
end
end
end
end
end

View File

@ -143,11 +143,12 @@ RSpec.describe WorkItems::UpdateService, feature_category: :team_planning do
end
end
context 'when work item labels widget is disabled' do
context 'when work item labels, assignees & milestone widgets are disabled' do
before do
widget_definitions = WorkItems::Type.default_by_type(:issue).widget_definitions
widget_definitions.find_by_widget_type(:labels).update!(disabled: true)
widget_definitions.find_by_widget_type(:assignees).update!(disabled: true)
widget_definitions.find_by_widget_type(:milestone).update!(disabled: true)
end
it_behaves_like 'issuable record that does not supports quick actions' do

View File

@ -1,16 +1,14 @@
# frozen_string_literal: true
RSpec.shared_examples 'enforcing job token policies' do |policy|
RSpec.shared_examples 'enforcing job token policies' do |policies|
context 'when authenticating with a CI Job Token from another project' do
let_it_be(:user) { create(:user) }
let_it_be(:job) { create(:ci_build, :running, user: user) }
let_it_be(:accessed_project) { create(:project, developers: user) }
let_it_be(:allowed_policies) { [policy] }
let_it_be(:allowed_policies) { Array(policies) }
let_it_be(:default_permissions) { false }
before do
create(:ci_job_token_project_scope_link,
source_project: accessed_project,
source_project: project,
target_project: job.project,
direction: :inbound,
job_token_policies: allowed_policies,
@ -23,23 +21,32 @@ RSpec.shared_examples 'enforcing job token policies' do |policy|
response
end
it { is_expected.to have_gitlab_http_status(:ok) }
it { is_expected.to have_gitlab_http_status(:success) }
context 'when the policy is not allowed' do
context 'when the policies are not allowed' do
let(:allowed_policies) { [] }
it { is_expected.to have_gitlab_http_status(:forbidden) }
it 'returns an error message containing the disallowed policy' do
do_request
expect(json_response['message']).to eq("403 Forbidden - Insufficient permissions to access this resource " \
"in project #{accessed_project.path}. The following token permission is required: #{policy}.")
expected_message = '403 Forbidden - Insufficient permissions to access this resource ' \
"in project #{project.path}. "
expected_message << if Array(policies).size == 1
"The following token permission is required: #{policies}."
else
"The following token permissions are required: #{Array(policies).to_sentence}."
end
expect(json_response['message']).to eq(expected_message)
end
context 'when fine grained permissions are disabled' do
let_it_be(:default_permissions) { true }
it { is_expected.to have_gitlab_http_status(:ok) }
it { is_expected.to have_gitlab_http_status(:success) }
end
context 'when the `enforce_job_token_policies` feature flag is disabled' do
@ -47,7 +54,7 @@ RSpec.shared_examples 'enforcing job token policies' do |policy|
stub_feature_flags(enforce_job_token_policies: false)
end
it { is_expected.to have_gitlab_http_status(:ok) }
it { is_expected.to have_gitlab_http_status(:success) }
end
end
end

View File

@ -60,5 +60,28 @@ RSpec.shared_examples 'Deploy keys with protected tags' do
end
end
end
context 'when deploy key is already selected for protected branch' do
let(:protected_tag) { create(:protected_tag, :no_one_can_create, project: project, name: 'v1.0.0') }
let(:write_access_key) { create(:deploy_key, user: user, write_access_to: project) }
before do
create(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: write_access_key)
end
it 'displays a preselected deploy key' do
visit project_protected_tags_path(project)
within(".js-protected-tag-edit-form") do
find(".js-allowed-to-create").click
wait_for_requests
within('[data-testid="deploy_key-dropdown-item"]') do
deploy_key_checkbox = find('[data-testid="dropdown-item-checkbox"]')
expect(deploy_key_checkbox).to have_no_css("gl-invisible")
end
end
end
end
end
end

View File

@ -118,9 +118,7 @@ RSpec.shared_examples 'issuable record that supports quick actions' do |with_wid
it 'assigns and sets milestone to issuable' do
expect(issuable.assignees).to eq([assignee])
expect(issuable.description).to eq new_descr
# WorkItem milestone widget does not support quick the action yet
expect(issuable.milestone).to eq(milestone) unless issuable.is_a?(WorkItem)
expect(issuable.milestone).to be_nil if issuable.is_a?(WorkItem)
expect(issuable.milestone).to eq(milestone)
end
end
end

View File

@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe Ci::Catalog::Resources::AggregateLast30DayUsageWorker, feature_category: :pipeline_composition do
subject(:worker) { described_class.new }
before do
stub_feature_flags(ci_catalog_ranking_from_new_usage_table: false)
end
include_examples 'an idempotent worker'
it 'has the `until_executed` deduplicate strategy' do