diff options
Diffstat (limited to 'app/assets/javascripts/ci')
66 files changed, 1451 insertions, 688 deletions
diff --git a/app/assets/javascripts/ci/artifacts/components/artifact_row.vue b/app/assets/javascripts/ci/artifacts/components/artifact_row.vue index 5b1c322f07a..d4de42b10a8 100644 --- a/app/assets/javascripts/ci/artifacts/components/artifact_row.vue +++ b/app/assets/javascripts/ci/artifacts/components/artifact_row.vue @@ -8,12 +8,10 @@ import { GlTooltipDirective, } from '@gitlab/ui'; import { numberToHumanSize } from '~/lib/utils/number_utils'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { I18N_EXPIRED, I18N_DOWNLOAD, I18N_DELETE, - BULK_DELETE_FEATURE_FLAG, I18N_BULK_DELETE_MAX_SELECTED, } from '../constants'; @@ -29,7 +27,6 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - mixins: [glFeatureFlagsMixin()], inject: ['canDestroyArtifacts'], props: { artifact: { @@ -66,7 +63,7 @@ export default { return numberToHumanSize(this.artifact.size); }, canBulkDestroyArtifacts() { - return this.glFeatures[BULK_DELETE_FEATURE_FLAG] && this.canDestroyArtifacts; + return this.canDestroyArtifacts; }, }, methods: { diff --git a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue index 3f6ea56382f..88334488fdd 100644 --- a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue +++ b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue @@ -9,12 +9,12 @@ import { GlIcon, GlPagination, GlFormCheckbox, + GlTooltipDirective, } from '@gitlab/ui'; import { createAlert } from '~/alert'; import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { TYPENAME_PROJECT } from '~/graphql_shared/constants'; import getJobArtifactsQuery from '../graphql/queries/get_job_artifacts.query.graphql'; import { totalArtifactsSizeForJob, mapArchivesToJobNodes, mapBooleansToJobNodes } from '../utils'; @@ -38,11 +38,11 @@ import { INITIAL_NEXT_PAGE_CURSOR, JOBS_PER_PAGE, INITIAL_LAST_PAGE_SIZE, - BULK_DELETE_FEATURE_FLAG, I18N_BULK_DELETE_ERROR, I18N_BULK_DELETE_PARTIAL_ERROR, I18N_BULK_DELETE_CONFIRMATION_TOAST, SELECTED_ARTIFACTS_MAX_COUNT, + I18N_BULK_DELETE_MAX_SELECTED, } from '../constants'; import JobCheckbox from './job_checkbox.vue'; import ArtifactsBulkDelete from './artifacts_bulk_delete.vue'; @@ -78,7 +78,9 @@ export default { ArtifactsTableRowDetails, FeedbackBanner, }, - mixins: [glFeatureFlagsMixin()], + directives: { + GlTooltip: GlTooltipDirective, + }, inject: ['projectId', 'projectPath', 'canDestroyArtifacts'], apollo: { jobArtifacts: { @@ -156,7 +158,7 @@ export default { return this.selectedArtifacts.length >= SELECTED_ARTIFACTS_MAX_COUNT; }, canBulkDestroyArtifacts() { - return this.glFeatures[BULK_DELETE_FEATURE_FLAG] && this.canDestroyArtifacts; + return this.canDestroyArtifacts; }, isDeletingArtifactsForJob() { return this.jobArtifactsToDelete.length > 0; @@ -164,6 +166,25 @@ export default { artifactsToDelete() { return this.isDeletingArtifactsForJob ? this.jobArtifactsToDelete : this.selectedArtifacts; }, + isAnyVisibleArtifactSelected() { + return this.jobArtifacts.some((job) => + job.artifacts.nodes.some((artifactNode) => + this.selectedArtifacts.includes(artifactNode.id), + ), + ); + }, + areAllVisibleArtifactsSelected() { + return this.jobArtifacts.every((job) => + job.artifacts.nodes.every((artifactNode) => + this.selectedArtifacts.includes(artifactNode.id), + ), + ); + }, + selectAllTooltipText() { + return this.isSelectedArtifactsLimitReached && !this.isAnyVisibleArtifactSelected + ? I18N_BULK_DELETE_MAX_SELECTED + : ''; + }, }, methods: { refetchArtifacts() { @@ -205,11 +226,11 @@ export default { } }, selectArtifact(artifactNode, checked) { - if (checked) { - if (!this.isSelectedArtifactsLimitReached) { - this.selectedArtifacts.push(artifactNode.id); - } - } else { + const isSelected = this.selectedArtifacts.includes(artifactNode.id); + + if (checked && !isSelected && !this.isSelectedArtifactsLimitReached) { + this.selectedArtifacts.push(artifactNode.id); + } else if (isSelected) { this.selectedArtifacts.splice(this.selectedArtifacts.indexOf(artifactNode.id), 1); } }, @@ -274,6 +295,11 @@ export default { this.isBulkDeleteModalVisible = false; this.jobArtifactsToDelete = []; }, + handleSelectAllChecked(checked) { + this.jobArtifacts.map((job) => + job.artifacts.nodes.map((artifactNode) => this.selectArtifact(artifactNode, checked)), + ); + }, clearSelectedArtifacts() { this.selectedArtifacts = []; }, @@ -284,7 +310,13 @@ export default { return !job.archive?.downloadPath; }, browseButtonDisabled(job) { - return !job.browseArtifactsPath; + return !job.browseArtifactsPath || !job.hasMetadata; + }, + browseButtonHref(job) { + // make href blank when button is disabled so `cursor: not-allowed` is applied + if (this.browseButtonDisabled(job)) return ''; + + return job.browseArtifactsPath; }, deleteButtonDisabled(job) { return !job.hasArtifacts || !this.canBulkDestroyArtifacts; @@ -369,10 +401,12 @@ export default { </template> <template v-if="canBulkDestroyArtifacts" #head(checkbox)> <gl-form-checkbox - :disabled="!anyArtifactsSelected" - :checked="anyArtifactsSelected" - :indeterminate="anyArtifactsSelected" - @change="clearSelectedArtifacts" + v-gl-tooltip.right + :title="selectAllTooltipText" + :checked="isAnyVisibleArtifactSelected" + :indeterminate="isAnyVisibleArtifactSelected && !areAllVisibleArtifactsSelected" + :disabled="isSelectedArtifactsLimitReached && !isAnyVisibleArtifactSelected" + @change="handleSelectAllChecked" /> </template> <template @@ -469,7 +503,7 @@ export default { <gl-button icon="folder-open" :disabled="browseButtonDisabled(item)" - :href="item.browseArtifactsPath" + :href="browseButtonHref(item)" :title="$options.i18n.browse" :aria-label="$options.i18n.browse" data-testid="job-artifacts-browse-button" diff --git a/app/assets/javascripts/ci/artifacts/components/job_checkbox.vue b/app/assets/javascripts/ci/artifacts/components/job_checkbox.vue index 91296bd507e..861278147e9 100644 --- a/app/assets/javascripts/ci/artifacts/components/job_checkbox.vue +++ b/app/assets/javascripts/ci/artifacts/components/job_checkbox.vue @@ -48,7 +48,7 @@ export default { }, }, methods: { - handleInput(checked) { + handleChange(checked) { if (checked) { this.unselectedArtifacts.forEach((node) => this.$emit('selectArtifact', node, true)); } else { @@ -65,6 +65,6 @@ export default { :disabled="disabled" :checked="checked" :indeterminate="indeterminate" - @input="handleInput" + @change="handleChange" /> </template> diff --git a/app/assets/javascripts/ci/artifacts/constants.js b/app/assets/javascripts/ci/artifacts/constants.js index 7ba65e0f98f..2d89b6541f3 100644 --- a/app/assets/javascripts/ci/artifacts/constants.js +++ b/app/assets/javascripts/ci/artifacts/constants.js @@ -54,7 +54,6 @@ export const I18N_FEEDBACK_BANNER_BODY = s__( export const I18N_FEEDBACK_BANNER_BUTTON = s__('Artifacts|Take a quick survey'); export const FEEDBACK_URL = 'https://gitlab.fra1.qualtrics.com/jfe/form/SV_cI9rAUI20Vo2St8'; -export const BULK_DELETE_FEATURE_FLAG = 'ciJobArtifactBulkDestroy'; export const SELECTED_ARTIFACTS_MAX_COUNT = 50; export const I18N_BULK_DELETE_MAX_SELECTED = s__( 'Artifacts|Maximum selected artifacts limit reached', @@ -104,6 +103,7 @@ export const JOBS_PER_PAGE = 20; export const INITIAL_LAST_PAGE_SIZE = null; export const ARCHIVE_FILE_TYPE = 'ARCHIVE'; +export const METADATA_FILE_TYPE = 'METADATA'; export const ARTIFACT_ROW_HEIGHT = 56; export const ARTIFACTS_SHOWN_WITHOUT_SCROLLING = 4; diff --git a/app/assets/javascripts/ci/artifacts/utils.js b/app/assets/javascripts/ci/artifacts/utils.js index ebcf0af8d2a..74ade7d48aa 100644 --- a/app/assets/javascripts/ci/artifacts/utils.js +++ b/app/assets/javascripts/ci/artifacts/utils.js @@ -1,10 +1,10 @@ import { numberToHumanSize } from '~/lib/utils/number_utils'; -import { ARCHIVE_FILE_TYPE, JOB_STATUS_GROUP_SUCCESS } from './constants'; +import { ARCHIVE_FILE_TYPE, METADATA_FILE_TYPE, JOB_STATUS_GROUP_SUCCESS } from './constants'; export const totalArtifactsSizeForJob = (job) => numberToHumanSize( job.artifacts.nodes - .map((artifact) => artifact.size) + .map((artifact) => Number(artifact.size)) .reduce((total, artifact) => total + artifact, 0), ); @@ -21,6 +21,9 @@ export const mapBooleansToJobNodes = (jobNode) => { return { succeeded: jobNode.detailedStatus.group === JOB_STATUS_GROUP_SUCCESS, hasArtifacts: jobNode.artifacts.nodes.length > 0, + hasMetadata: jobNode.artifacts.nodes.some( + (artifact) => artifact.fileType === METADATA_FILE_TYPE, + ), ...jobNode, }; }; diff --git a/app/assets/javascripts/ci/ci_lint/components/ci_lint.vue b/app/assets/javascripts/ci/ci_lint/components/ci_lint.vue index 49a314e067c..39573b2180b 100644 --- a/app/assets/javascripts/ci/ci_lint/components/ci_lint.vue +++ b/app/assets/javascripts/ci/ci_lint/components/ci_lint.vue @@ -108,7 +108,7 @@ export default { @click="lint" >{{ __('Validate') }}</gl-button > - <gl-form-checkbox v-model="dryRun" + <gl-form-checkbox v-model="dryRun" data-testid="ci-lint-dryrun" >{{ __('Simulate a pipeline created for the default branch') }} <gl-link :href="pipelineSimulationHelpPagePath" target="_blank" ><gl-icon class="gl-text-blue-600" name="question-o" /></gl-link diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue index b3ecaceba69..41514d2d2f1 100644 --- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue +++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue @@ -125,7 +125,7 @@ export default { return regex.test(this.variable.value); }, canSubmit() { - return this.variableValidationState && this.variable.key !== '' && this.variable.value !== ''; + return this.variableValidationState && this.variable.key !== ''; }, containsVariableReference() { const regex = /\$/; @@ -154,7 +154,9 @@ export default { return createJoinedEnvironments(this.variables, this.environments, this.newEnvironments); }, maskedFeedback() { - return this.displayMaskedError ? __('This variable can not be masked.') : ''; + return this.displayMaskedError + ? __('This variable value does not meet the masking requirements.') + : ''; }, maskedState() { if (this.displayMaskedError) { @@ -190,6 +192,11 @@ export default { variableValidationState() { return this.variable.value === '' || (this.tokenValidationState && this.maskedState); }, + variableValueHelpText() { + return this.variable.masked + ? __('Value must meet regular expression requirements to be masked.') + : ''; + }, }, watch: { variable: { @@ -324,6 +331,7 @@ export default { :label="__('Value')" label-for="ci-variable-value" :state="variableValidationState" + :description="variableValueHelpText" :invalid-feedback="variableValidationFeedback" > <gl-form-textarea @@ -423,17 +431,19 @@ export default { > {{ __('Mask variable') }} <p class="gl-mt-2 text-secondary"> - {{ __('Variable will be masked in job logs.') }} - <span - :class="{ - 'bold text-plain': displayMaskedError, - }" - > - {{ __('Requires values to meet regular expression requirements.') }}</span + <gl-sprintf + :message=" + __( + 'Mask this variable in job logs if it meets %{linkStart}regular expression requirements%{linkEnd}.', + ) + " > - <gl-link target="_blank" :href="maskedEnvironmentVariablesLink">{{ - __('Learn more.') - }}</gl-link> + <template #link="{ content }" + ><gl-link target="_blank" :href="maskedEnvironmentVariablesLink">{{ + content + }}</gl-link> + </template> + </gl-sprintf> </p> </gl-form-checkbox> <gl-form-checkbox diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue index 6f6c55e07c7..ec7a921664f 100644 --- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue +++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue @@ -1,10 +1,12 @@ <script> import { GlAlert, + GlBadge, GlButton, GlLoadingIcon, GlModalDirective, GlKeysetPagination, + GlLink, GlTable, GlTooltipDirective, } from '@gitlab/ui'; @@ -15,18 +17,13 @@ import { DEFAULT_EXCEEDS_VARIABLE_LIMIT_TEXT, EXCEEDS_VARIABLE_LIMIT_TEXT, MAXIMUM_VARIABLE_LIMIT_REACHED, - variableText, + variableTypes, } from '../constants'; import { convertEnvironmentScope } from '../utils'; export default { modalId: ADD_CI_VARIABLE_MODAL_ID, - fields: [ - { - key: 'variableType', - label: s__('CiVariables|Type'), - thClass: 'gl-w-10p', - }, + defaultFields: [ { key: 'key', label: s__('CiVariables|Key'), @@ -36,12 +33,11 @@ export default { { key: 'value', label: s__('CiVariables|Value'), - thClass: 'gl-w-15p', }, { - key: 'options', - label: s__('CiVariables|Options'), - thClass: 'gl-w-10p', + key: 'Attributes', + label: s__('CiVariables|Attributes'), + thClass: 'gl-w-40p', }, { key: 'environmentScope', @@ -54,10 +50,31 @@ export default { thClass: 'gl-w-5p', }, ], + inheritedVarsFields: [ + { + key: 'key', + label: s__('CiVariables|Key'), + tdClass: 'text-plain', + }, + { + key: 'Attributes', + label: s__('CiVariables|Attributes'), + }, + { + key: 'environmentScope', + label: s__('CiVariables|Environments'), + }, + { + key: 'group', + label: s__('CiVariables|Group'), + }, + ], components: { GlAlert, + GlBadge, GlButton, GlKeysetPagination, + GlLink, GlLoadingIcon, GlTable, }, @@ -66,6 +83,7 @@ export default { GlTooltip: GlTooltipDirective, }, mixins: [glFeatureFlagsMixin()], + inject: ['isInheritedGroupVars'], props: { entity: { type: String, @@ -112,6 +130,9 @@ export default { showAlert() { return !this.isLoading && this.exceedsVariableLimit; }, + showPagination() { + return this.glFeatures.ciVariablesPages; + }, valuesButtonText() { return this.areValuesHidden ? __('Reveal values') : __('Hide values'); }, @@ -119,12 +140,17 @@ export default { return !this.variables || this.variables.length === 0; }, fields() { - return this.$options.fields; + return this.isInheritedGroupVars + ? this.$options.inheritedVarsFields + : this.$options.defaultFields; + }, + tableDataTestId() { + return this.isInheritedGroupVars ? 'inherited-ci-variable-table' : 'ci-variable-table'; }, - variablesWithOptions() { + variablesWithAttributes() { return this.variables?.map((item, index) => ({ ...item, - options: this.getOptions(item), + attributes: this.getAttributes(item), index, })); }, @@ -133,27 +159,27 @@ export default { convertEnvironmentScopeValue(env) { return convertEnvironmentScope(env); }, - generateTypeText(item) { - return variableText[item.variableType]; - }, toggleHiddenState() { this.areValuesHidden = !this.areValuesHidden; }, setSelectedVariable(index = -1) { this.$emit('set-selected-variable', this.variables[index] ?? null); }, - getOptions(item) { - const options = []; + getAttributes(item) { + const attributes = []; + if (item.variableType === variableTypes.fileType) { + attributes.push(s__('CiVariables|File')); + } if (item.protected) { - options.push(s__('CiVariables|Protected')); + attributes.push(s__('CiVariables|Protected')); } if (item.masked) { - options.push(s__('CiVariables|Masked')); + attributes.push(s__('CiVariables|Masked')); } if (!item.raw) { - options.push(s__('CiVariables|Expanded')); + attributes.push(s__('CiVariables|Expanded')); } - return options.join(', '); + return attributes; }, }, maximumVariableLimitReached: MAXIMUM_VARIABLE_LIMIT_REACHED, @@ -161,7 +187,7 @@ export default { </script> <template> - <div class="ci-variable-table" data-testid="ci-variable-table"> + <div class="ci-variable-table" :data-testid="tableDataTestId"> <gl-loading-icon v-if="isLoading" /> <gl-alert v-if="showAlert" @@ -172,7 +198,7 @@ export default { {{ exceedsVariableLimitText }} </gl-alert> <div - v-if="glFeatures.ciVariablesPages" + v-if="showPagination && !isInheritedGroupVars" class="ci-variable-actions gl-display-flex gl-justify-content-end gl-my-3" > <gl-button v-if="!isTableEmpty" @click="toggleHiddenState">{{ valuesButtonText }}</gl-button> @@ -191,13 +217,11 @@ export default { <gl-table v-if="!isLoading" :fields="fields" - :items="variablesWithOptions" + :items="variablesWithAttributes" tbody-tr-class="js-ci-variable-row" - data-qa-selector="ci_variable_table_content" sort-by="key" sort-direction="asc" stacked="lg" - table-class="gl-border-t" fixed show-empty sort-icon-left @@ -208,9 +232,6 @@ export default { <template #table-colgroup="scope"> <col v-for="field in scope.fields" :key="field.key" :style="field.customStyle" /> </template> - <template #cell(variableType)="{ item }"> - {{ generateTypeText(item) }} - </template> <template #cell(key)="{ item }"> <div class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3" @@ -231,7 +252,7 @@ export default { /> </div> </template> - <template #cell(value)="{ item }"> + <template v-if="!isInheritedGroupVars" #cell(value)="{ item }"> <div class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3" > @@ -254,8 +275,18 @@ export default { /> </div> </template> - <template #cell(options)="{ item }"> - <span data-testid="ci-variable-table-row-options">{{ item.options }}</span> + <template #cell(attributes)="{ item }"> + <span data-testid="ci-variable-table-row-attributes"> + <gl-badge + v-for="attribute in item.attributes" + :key="`${item.key}-${attribute}`" + class="gl-mr-2" + variant="info" + size="sm" + > + {{ attribute }} + </gl-badge> + </span> </template> <template #cell(environmentScope)="{ item }"> <div @@ -277,7 +308,21 @@ export default { /> </div> </template> - <template #cell(actions)="{ item }"> + <template v-if="isInheritedGroupVars" #cell(group)="{ item }"> + <div + class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3" + > + <gl-link + :id="`ci-variable-group-${item.id}`" + data-testid="ci-variable-table-row-cicd-path" + class="gl-display-inline-block gl-max-w-full gl-word-break-word" + :href="item.groupCiCdSettingsPath" + > + {{ item.groupName }} + </gl-link> + </div> + </template> + <template v-if="!isInheritedGroupVars" #cell(actions)="{ item }"> <gl-button v-gl-modal-directive="$options.modalId" icon="pencil" @@ -300,28 +345,32 @@ export default { > {{ exceedsVariableLimitText }} </gl-alert> - <div v-if="!glFeatures.ciVariablesPages" class="ci-variable-actions gl-display-flex gl-mt-5"> - <gl-button - v-gl-modal-directive="$options.modalId" - class="gl-mr-3" - data-qa-selector="add_ci_variable_button" - variant="confirm" - category="primary" - :aria-label="__('Add')" - :disabled="exceedsVariableLimit" - @click="setSelectedVariable()" - >{{ __('Add variable') }}</gl-button - > - <gl-button v-if="!isTableEmpty" @click="toggleHiddenState">{{ valuesButtonText }}</gl-button> - </div> - <div v-else class="gl-display-flex gl-justify-content-center gl-mt-6"> - <gl-keyset-pagination - v-bind="pageInfo" - :prev-text="__('Previous')" - :next-text="__('Next')" - @prev="$emit('handle-prev-page')" - @next="$emit('handle-next-page')" - /> + <div v-if="!isInheritedGroupVars"> + <div v-if="!showPagination" class="ci-variable-actions gl-display-flex gl-mt-5"> + <gl-button + v-gl-modal-directive="$options.modalId" + class="gl-mr-3" + data-qa-selector="add_ci_variable_button" + variant="confirm" + category="primary" + :aria-label="__('Add')" + :disabled="exceedsVariableLimit" + @click="setSelectedVariable()" + >{{ __('Add variable') }}</gl-button + > + <gl-button v-if="!isTableEmpty" @click="toggleHiddenState">{{ + valuesButtonText + }}</gl-button> + </div> + <div v-else class="gl-display-flex gl-justify-content-center gl-mt-6"> + <gl-keyset-pagination + v-bind="pageInfo" + :prev-text="__('Previous')" + :next-text="__('Next')" + @prev="$emit('handle-prev-page')" + @next="$emit('handle-next-page')" + /> + </div> </div> </div> </template> diff --git a/app/assets/javascripts/ci/ci_variable_list/constants.js b/app/assets/javascripts/ci/ci_variable_list/constants.js index c8f67bd3436..d702dd073ec 100644 --- a/app/assets/javascripts/ci/ci_variable_list/constants.js +++ b/app/assets/javascripts/ci/ci_variable_list/constants.js @@ -20,28 +20,14 @@ export const variableTypes = { fileType: 'FILE', }; -// Once REST is removed, we won't need `types` -export const types = { - variableType: 'env_var', - fileType: 'file', -}; - export const allEnvironments = { type: '*', text: __('All (default)'), }; -// Once REST is removed, we won't need `types` key -export const variableText = { - [types.variableType]: __('Variable'), - [types.fileType]: __('File'), - [variableTypes.envType]: __('Variable'), - [variableTypes.fileType]: __('File'), -}; - export const variableOptions = [ - { value: variableTypes.envType, text: variableText[variableTypes.envType] }, - { value: variableTypes.fileType, text: variableText[variableTypes.fileType] }, + { value: variableTypes.envType, text: variableTypes.envType }, + { value: variableTypes.fileType, text: variableTypes.fileType }, ]; export const defaultVariableState = { diff --git a/app/assets/javascripts/ci/ci_variable_list/index.js b/app/assets/javascripts/ci/ci_variable_list/index.js index 033cdbe864e..e47b41ceae5 100644 --- a/app/assets/javascripts/ci/ci_variable_list/index.js +++ b/app/assets/javascripts/ci/ci_variable_list/index.js @@ -67,6 +67,7 @@ const mountCiVariableListApp = (containerEl) => { groupId, groupPath, isGroup: parsedIsGroup, + isInheritedGroupVars: false, isProject: parsedIsProject, isProtectedByDefault, maskedEnvironmentVariablesLink, diff --git a/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue b/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue new file mode 100644 index 00000000000..27ee1b794f6 --- /dev/null +++ b/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue @@ -0,0 +1,110 @@ +<script> +import { produce } from 'immer'; +import { s__ } from '~/locale'; +import { createAlert } from '~/alert'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { reportMessageToSentry } from '~/ci/ci_variable_list/utils'; +import CiVariableTable from '~/ci/ci_variable_list/components/ci_variable_table.vue'; +import getInheritedCiVariables from '../graphql/queries/inherited_ci_variables.query.graphql'; + +export const i18n = { + fetchError: s__('CiVariables|There was an error fetching the inherited CI variables.'), + tooManyCallsError: s__( + 'CiVariables|Maximum number of Inherited Group CI variables loaded (2000)', + ), +}; + +export const VARIABLES_PER_FETCH = 100; +export const FETCH_LIMIT = 20; + +export default { + name: 'InheritedCiVariablesApp', + components: { + CiVariableTable, + }, + mixins: [glFeatureFlagsMixin()], + inject: ['projectPath'], + apollo: { + ciVariables: { + query: getInheritedCiVariables, + variables() { + return { + first: VARIABLES_PER_FETCH, + fullPath: this.projectPath, + }; + }, + update(data) { + return data.project.inheritedCiVariables?.nodes || []; + }, + result({ data }) { + this.pageInfo = data?.project?.inheritedCiVariables?.pageInfo || this.pageInfo; + this.hasNextPage = this.pageInfo?.hasNextPage || false; + if (!this.hasNextPage) { + return; + } + + // The query fetches 100 items at a time. + // Variables are batch loaded up to 20 consecutive API calls. + if (this.loadingCounter < FETCH_LIMIT) { + this.hasNextPage = false; + this.fetchMoreVariables(); + this.loadingCounter += 1; + } else { + createAlert({ message: this.$options.i18n.tooManyCallsError }); + reportMessageToSentry(this.$options.name, this.$options.i18n.tooManyCallsError, {}); + } + }, + error() { + this.showFetchError(); + }, + }, + }, + data() { + return { + ciVariables: [], + hasNextPage: false, + loadingCounter: 1, + pageInfo: {}, + }; + }, + computed: { + isLoading() { + return this.$apollo.queries.ciVariables.loading; + }, + }, + methods: { + fetchMoreVariables() { + this.$apollo.queries.ciVariables + .fetchMore({ + variables: { + after: this.pageInfo.endCursor, + }, + updateQuery(previousResult, { fetchMoreResult }) { + const previousVars = previousResult.project.inheritedCiVariables?.nodes; + const newVars = fetchMoreResult.project.inheritedCiVariables?.nodes; + + return produce(fetchMoreResult, (draftData) => { + draftData.project.inheritedCiVariables.nodes = previousVars.concat(newVars); + }); + }, + }) + .catch(this.showFetchError); + }, + showFetchError() { + this.hasNextPage = false; + createAlert({ message: this.$options.i18n.fetchError }); + }, + }, + i18n, +}; +</script> + +<template> + <ci-variable-table + entity="project" + :is-loading="isLoading" + :max-variable-limit="0" + :page-info="pageInfo" + :variables="ciVariables" + /> +</template> diff --git a/app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql b/app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql new file mode 100644 index 00000000000..b25768632e1 --- /dev/null +++ b/app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql @@ -0,0 +1,24 @@ +#import "~/graphql_shared/fragments/page_info.fragment.graphql" + +query getInheritedCiVariables($after: String, $first: Int, $fullPath: ID!) { + project(fullPath: $fullPath) { + id + inheritedCiVariables(after: $after, first: $first) { + pageInfo { + ...PageInfo + } + nodes { + __typename + id + key + variableType + environmentScope + groupCiCdSettingsPath + groupName + masked + protected + raw + } + } + } +} diff --git a/app/assets/javascripts/ci/inherited_ci_variables/index.js b/app/assets/javascripts/ci/inherited_ci_variables/index.js new file mode 100644 index 00000000000..324aae2a573 --- /dev/null +++ b/app/assets/javascripts/ci/inherited_ci_variables/index.js @@ -0,0 +1,36 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import { generateCacheConfig, resolvers } from '../ci_variable_list/graphql/settings'; +import InheritedCiVariables from './components/inherited_ci_variables_app.vue'; + +export default (containerId = 'js-inherited-group-ci-variables') => { + const el = document.getElementById(containerId); + + if (!el) { + return; + } + + const { projectPath } = el.dataset; + + Vue.use(VueApollo); + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient( + resolvers, + generateCacheConfig(false), // set to true if we're using key-set pagination + ), + }); + + // eslint-disable-next-line consistent-return + return new Vue({ + el, + apolloProvider, + provide: { + isInheritedGroupVars: true, + projectPath, + }, + render(createElement) { + return createElement(InheritedCiVariables); + }, + }); +}; diff --git a/app/assets/javascripts/ci/pipeline_editor/components/drawer/pipeline_editor_drawer.vue b/app/assets/javascripts/ci/pipeline_editor/components/drawer/pipeline_editor_drawer.vue index ea7201efcd9..c2e4c234d2b 100644 --- a/app/assets/javascripts/ci/pipeline_editor/components/drawer/pipeline_editor_drawer.vue +++ b/app/assets/javascripts/ci/pipeline_editor/components/drawer/pipeline_editor_drawer.vue @@ -1,8 +1,9 @@ <script> import { GlDrawer } from '@gitlab/ui'; +import { DRAWER_Z_INDEX } from '~/lib/utils/constants'; import { getContentWrapperHeight } from '~/lib/utils/dom_utils'; import { __ } from '~/locale'; -import { DRAWER_CONTAINER_CLASS } from '../job_assistant_drawer/constants'; +import { EDITOR_APP_DRAWER_NONE } from '~/ci/pipeline_editor/constants'; import FirstPipelineCard from './cards/first_pipeline_card.vue'; import GettingStartedCard from './cards/getting_started_card.vue'; import PipelineConfigReferenceCard from './cards/pipeline_config_reference_card.vue'; @@ -31,24 +32,24 @@ export default { zIndex: { type: Number, required: false, - default: 200, + default: DRAWER_Z_INDEX, }, }, computed: { - drawerHeightOffset() { - return getContentWrapperHeight(DRAWER_CONTAINER_CLASS); + getDrawerHeaderHeight() { + return getContentWrapperHeight(); }, }, methods: { closeDrawer() { - this.$emit('close-drawer'); + this.$emit('switch-drawer', EDITOR_APP_DRAWER_NONE); }, }, }; </script> <template> <gl-drawer - :header-height="drawerHeightOffset" + :header-height="getDrawerHeaderHeight" :open="isVisible" :z-index="zIndex" @close="closeDrawer" diff --git a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue index eabf4749e9c..6ba8884f9a6 100644 --- a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue +++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue @@ -3,7 +3,14 @@ import { GlButton } from '@gitlab/ui'; import { __, s__ } from '~/locale'; import Tracking from '~/tracking'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { pipelineEditorTrackingOptions, TEMPLATE_REPOSITORY_URL } from '../../constants'; +import { + EDITOR_APP_DRAWER_AI_ASSISTANT, + EDITOR_APP_DRAWER_HELP, + EDITOR_APP_DRAWER_JOB_ASSISTANT, + EDITOR_APP_DRAWER_NONE, + pipelineEditorTrackingOptions, + TEMPLATE_REPOSITORY_URL, +} from '../../constants'; export default { i18n: { @@ -19,7 +26,7 @@ export default { mixins: [glFeatureFlagMixin(), Tracking.mixin()], inject: ['aiChatAvailable'], props: { - showDrawer: { + showHelpDrawer: { type: Boolean, required: true, }, @@ -38,22 +45,24 @@ export default { }, }, methods: { - toggleDrawer() { - if (this.showDrawer) { - this.$emit('close-drawer'); + toggleHelpDrawer() { + if (this.showHelpDrawer) { + this.$emit('switch-drawer', EDITOR_APP_DRAWER_NONE); } else { - this.$emit('open-drawer'); + this.$emit('switch-drawer', EDITOR_APP_DRAWER_HELP); this.trackHelpDrawerClick(); } }, toggleJobAssistantDrawer() { this.$emit( - this.showJobAssistantDrawer ? 'close-job-assistant-drawer' : 'open-job-assistant-drawer', + 'switch-drawer', + this.showJobAssistantDrawer ? EDITOR_APP_DRAWER_NONE : EDITOR_APP_DRAWER_JOB_ASSISTANT, ); }, toggleAiAssistantDrawer() { this.$emit( - this.showAiAssistantDrawer ? 'close-ai-assistant-drawer' : 'open-ai-assistant-drawer', + 'switch-drawer', + this.showAiAssistantDrawer ? EDITOR_APP_DRAWER_NONE : EDITOR_APP_DRAWER_AI_ASSISTANT, ); }, trackHelpDrawerClick() { @@ -70,7 +79,10 @@ export default { </script> <template> - <div class="gl-display-flex gl-p-3 gl-gap-3 gl-border-solid gl-border-gray-100 gl-border-1"> + <div + class="gl-display-flex gl-p-3 gl-gap-3 gl-border-solid gl-border-gray-100 gl-border-1 gl-sm-flex-direction-column" + > + <slot></slot> <gl-button :href="$options.TEMPLATE_REPOSITORY_URL" size="small" @@ -87,7 +99,7 @@ export default { size="small" data-testid="drawer-toggle" data-qa-selector="drawer_toggle" - @click="toggleDrawer" + @click="toggleHelpDrawer" > {{ $options.i18n.help }} </gl-button> diff --git a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue index ef9acc1f8f1..a410e4c933c 100644 --- a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue +++ b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue @@ -229,7 +229,6 @@ export default { <gl-infinite-scroll :fetched-items="availableBranches.length" :max-list-height="250" - data-qa-selector="branch_menu_container" @bottomReached="fetchNextBranches" > <template #items> @@ -238,7 +237,6 @@ export default { :key="branch" :is-checked="currentBranch === branch" is-check-item - data-qa-selector="branch_menu_item_button" @click="selectBranch(branch)" > {{ branch }} diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue index a4dfb401f4c..656b1a6c347 100644 --- a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue +++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue @@ -2,7 +2,7 @@ import { __ } from '~/locale'; import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils'; import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue'; -import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql'; +import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql'; import { PIPELINE_FAILURE } from '../../constants'; export default { diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue index 372f04075ab..bb79a4d74da 100644 --- a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue +++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue @@ -9,7 +9,9 @@ import { getQueryHeaders, toggleQueryPollingByVisibility, } from '~/pipelines/components/graph/utils'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import GraphqlPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/graphql_pipeline_mini_graph.vue'; import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue'; const POLL_INTERVAL = 10000; @@ -32,11 +34,13 @@ export default { GlLink, GlLoadingIcon, GlSprintf, + GraphqlPipelineMiniGraph, PipelineEditorMiniGraph, }, directives: { GlTooltip: GlTooltipDirective, }, + mixins: [glFeatureFlagsMixin()], inject: ['projectFullPath'], props: { commitSha: { @@ -106,6 +110,9 @@ export default { hasPipelineData() { return Boolean(this.pipeline?.id); }, + isUsingPipelineMiniGraphQueries() { + return this.glFeatures.ciGraphqlPipelineMiniGraph; + }, pipelineId() { return getIdFromGraphQLId(this.pipeline.id); }, @@ -171,8 +178,14 @@ export default { </gl-sprintf> </span> </div> - <div class="gl-display-flex gl-flex-wrap"> - <pipeline-editor-mini-graph :pipeline="pipeline" v-on="$listeners" /> + <div class="gl-display-flex gl-flex-wrap-wrap"> + <graphql-pipeline-mini-graph + v-if="isUsingPipelineMiniGraphQueries" + :full-path="projectFullPath" + :iid="pipeline.iid" + :pipeline-etag="pipelineEtag" + /> + <pipeline-editor-mini-graph v-else :pipeline="pipeline" v-on="$listeners" /> <gl-button class="gl-ml-3" category="secondary" diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item.vue index 25bbd6b3180..794763e0cd8 100644 --- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item.vue +++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item.vue @@ -1,15 +1,19 @@ <script> -import { GlAccordionItem, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui'; +import { GlAccordionItem, GlFormInput, GlFormGroup, GlButton, GlLink, GlSprintf } from '@gitlab/ui'; import { get, toPath } from 'lodash'; -import { i18n } from '../constants'; +import { i18n, HELP_PATHS } from '../constants'; export default { i18n, + artifactsHelpPath: HELP_PATHS.artifactsHelpPath, + cacheHelpPath: HELP_PATHS.cacheHelpPath, components: { GlFormGroup, GlAccordionItem, GlFormInput, GlButton, + GlLink, + GlSprintf, }, props: { job: { @@ -61,6 +65,16 @@ export default { </script> <template> <gl-accordion-item :title="$options.i18n.ARTIFACTS_AND_CACHE"> + <div class="gl-pb-5"> + <gl-sprintf :message="$options.i18n.ARTIFACTS_AND_CACHE_DESCRIPTION"> + <template #artifactsLink="{ content }"> + <gl-link :href="$options.artifactsHelpPath">{{ content }}</gl-link> + </template> + <template #cacheLink="{ content }"> + <gl-link :href="$options.cacheHelpPath">{{ content }}</gl-link> + </template> + </gl-sprintf> + </div> <div v-for="entry in formOptions" :key="entry.key" class="form-group"> <div class="gl-display-flex"> <label class="gl-font-weight-bold gl-mb-3">{{ entry.title }}</label> @@ -82,6 +96,7 @@ export default { category="tertiary" icon="remove" :data-testid="entry.generateDeleteButtonDataTestId(index)" + :aria-label="entry.generateDeleteButtonDataTestId(index)" @click="deleteStringArrayItem(`${entry.key}[${index}]`)" /> </div> diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue index b4b468987d8..2c27b66f108 100644 --- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue +++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue @@ -1,15 +1,25 @@ <script> -import { GlFormGroup, GlAccordionItem, GlFormInput, GlFormTextarea } from '@gitlab/ui'; -import { i18n } from '../constants'; +import { + GlFormGroup, + GlAccordionItem, + GlFormInput, + GlFormTextarea, + GlLink, + GlSprintf, +} from '@gitlab/ui'; +import { i18n, HELP_PATHS } from '../constants'; export default { i18n, + helpPath: HELP_PATHS.imageHelpPath, placeholderText: i18n.ENTRYPOINT_PLACEHOLDER_TEXT, components: { GlAccordionItem, GlFormInput, GlFormTextarea, GlFormGroup, + GlLink, + GlSprintf, }, props: { job: { @@ -26,6 +36,13 @@ export default { </script> <template> <gl-accordion-item :title="$options.i18n.IMAGE"> + <div class="gl-pb-5"> + <gl-sprintf :message="$options.i18n.IMAGE_DESCRIPTION"> + <template #link="{ content }"> + <gl-link :href="$options.helpPath">{{ content }}</gl-link> + </template> + </gl-sprintf> + </div> <gl-form-group :label="$options.i18n.IMAGE_NAME"> <gl-form-input :value="job.image.name" diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item.vue index d068b370852..d0f206e767f 100644 --- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item.vue +++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item.vue @@ -5,11 +5,14 @@ import { GlFormInput, GlFormSelect, GlFormCheckbox, + GlLink, + GlSprintf, } from '@gitlab/ui'; -import { i18n, JOB_RULES_WHEN, JOB_RULES_START_IN } from '../constants'; +import { i18n, HELP_PATHS, JOB_RULES_WHEN, JOB_RULES_START_IN } from '../constants'; export default { i18n, + helpPath: HELP_PATHS.rulesHelpPath, whenOptions: Object.values(JOB_RULES_WHEN), unitOptions: Object.values(JOB_RULES_START_IN), components: { @@ -18,6 +21,8 @@ export default { GlFormSelect, GlFormCheckbox, GlFormGroup, + GlLink, + GlSprintf, }, props: { job: { @@ -54,6 +59,13 @@ export default { </script> <template> <gl-accordion-item :title="$options.i18n.RULES"> + <div class="gl-pb-5"> + <gl-sprintf :message="$options.i18n.RULES_DESCRIPTION"> + <template #link="{ content }"> + <gl-link :href="$options.helpPath">{{ content }}</gl-link> + </template> + </gl-sprintf> + </div> <div class="gl-display-flex"> <gl-form-group class="gl-flex-grow-1 gl-flex-basis-half gl-mr-3" :label="$options.i18n.WHEN"> <gl-form-select diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue index 9bada3ef110..0b12d0aedd6 100644 --- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue +++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue @@ -1,9 +1,18 @@ <script> -import { GlAccordionItem, GlFormInput, GlButton, GlFormGroup, GlFormTextarea } from '@gitlab/ui'; -import { i18n } from '../constants'; +import { + GlAccordionItem, + GlFormInput, + GlButton, + GlFormGroup, + GlFormTextarea, + GlLink, + GlSprintf, +} from '@gitlab/ui'; +import { i18n, HELP_PATHS } from '../constants'; export default { i18n, + helpPath: HELP_PATHS.servicesHelpPath, placeholderText: i18n.ENTRYPOINT_PLACEHOLDER_TEXT, components: { GlAccordionItem, @@ -11,6 +20,8 @@ export default { GlFormInput, GlFormTextarea, GlButton, + GlLink, + GlSprintf, }, props: { job: { @@ -45,6 +56,13 @@ export default { </script> <template> <gl-accordion-item :title="$options.i18n.SERVICE"> + <div class="gl-pb-5"> + <gl-sprintf :message="$options.i18n.SERVICES_DESCRIPTION"> + <template #link="{ content }"> + <gl-link :href="$options.helpPath">{{ content }}</gl-link> + </template> + </gl-sprintf> + </div> <div v-for="(service, index) in job.services" :key="index" @@ -56,6 +74,7 @@ export default { category="tertiary" icon="remove" :data-testid="`delete-job-service-button-${index}`" + :aria-label="`delete-job-service-button-${index}`" @click="deleteService(index)" /> <gl-form-group :label="$options.i18n.SERVICE_NAME"> diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js index e93a9e84302..087ae992916 100644 --- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js +++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js @@ -1,6 +1,5 @@ import { __, s__ } from '~/locale'; - -export const DRAWER_CONTAINER_CLASS = '.content-wrapper'; +import { DOCS_URL_IN_EE_DIR } from 'jh_else_ce/lib/utils/url_utility'; export const JOB_RULES_WHEN = { onSuccess: { @@ -115,4 +114,24 @@ export const i18n = { SERVICE_NAME: s__('JobAssistant|Service name (optional)'), SERVICE_ENTRYPOINT: s__('JobAssistant|Service entrypoint (optional)'), ENTRYPOINT_PLACEHOLDER_TEXT: s__('JobAssistant|Please enter the parameters.'), + IMAGE_DESCRIPTION: s__( + 'JobAssistant|Specify a Docker image that the job runs in. %{linkStart}Learn more%{linkEnd}', + ), + SERVICES_DESCRIPTION: s__( + 'JobAssistant|Specify any additional Docker images that your scripts require to run successfully. %{linkStart}Learn more%{linkEnd}', + ), + ARTIFACTS_AND_CACHE_DESCRIPTION: s__( + 'JobAssistant|Specify the %{artifactsLinkStart}artifacts%{artifactsLinkEnd} and %{cacheLinkStart}cache%{cacheLinkEnd} of the job.', + ), + RULES_DESCRIPTION: s__( + 'JobAssistant|Include or exclude jobs in pipelines. %{linkStart}Learn more%{linkEnd}', + ), +}; + +export const HELP_PATHS = { + artifactsHelpPath: `${DOCS_URL_IN_EE_DIR}/ci/yaml/#artifacts`, + cacheHelpPath: `${DOCS_URL_IN_EE_DIR}/ci/yaml/#cache`, + imageHelpPath: `${DOCS_URL_IN_EE_DIR}/ci/yaml/#image`, + rulesHelpPath: `${DOCS_URL_IN_EE_DIR}/ci/yaml/#rules`, + servicesHelpPath: `${DOCS_URL_IN_EE_DIR}/ci/yaml/#services`, }; diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue index 30746065732..1a58a112e50 100644 --- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue +++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue @@ -2,10 +2,12 @@ import { GlDrawer, GlAccordion, GlButton } from '@gitlab/ui'; import { stringify, parse } from 'yaml'; import { get, omit, toPath } from 'lodash'; +import { DRAWER_Z_INDEX } from '~/lib/utils/constants'; import { getContentWrapperHeight } from '~/lib/utils/dom_utils'; import eventHub, { SCROLL_EDITOR_TO_BOTTOM } from '~/ci/pipeline_editor/event_hub'; +import { EDITOR_APP_DRAWER_NONE } from '~/ci/pipeline_editor/constants'; import getRunnerTags from '../../graphql/queries/runner_tags.query.graphql'; -import { DRAWER_CONTAINER_CLASS, JOB_TEMPLATE, JOB_RULES_WHEN, i18n } from './constants'; +import { JOB_TEMPLATE, JOB_RULES_WHEN, i18n } from './constants'; import { removeEmptyObj, trimFields, validateEmptyValue, validateStartIn } from './utils'; import JobSetupItem from './accordion_items/job_setup_item.vue'; import ImageItem from './accordion_items/image_item.vue'; @@ -34,7 +36,7 @@ export default { zIndex: { type: Number, required: false, - default: 200, + default: DRAWER_Z_INDEX, }, ciConfigData: { type: Object, @@ -78,8 +80,8 @@ export default { }; }); }, - drawerHeightOffset() { - return getContentWrapperHeight(DRAWER_CONTAINER_CLASS); + getDrawerHeaderHeight() { + return getContentWrapperHeight(); }, isJobValid() { return this.isNameValid && this.isScriptValid && this.isStartValid; @@ -100,7 +102,7 @@ export default { methods: { closeDrawer() { this.clearJob(); - this.$emit('close-job-assistant-drawer'); + this.$emit('switch-drawer', EDITOR_APP_DRAWER_NONE); }, addCiConfig() { this.validateJob(); @@ -172,7 +174,7 @@ export default { <template> <gl-drawer class="job-assistant-drawer" - :header-height="drawerHeightOffset" + :header-height="getDrawerHeaderHeight" :open="isVisible" :z-index="zIndex" @close="closeDrawer" diff --git a/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue index 403793a255a..a954615ca8a 100644 --- a/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue +++ b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue @@ -1,5 +1,6 @@ <script> import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui'; +import CiEditorHeader from 'ee_else_ce/ci/pipeline_editor/components/editor/ci_editor_header.vue'; import { s__, __ } from '~/locale'; import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; import { getParameterValues, setUrlParams, updateHistory } from '~/lib/utils/url_utility'; @@ -19,7 +20,6 @@ import { } from '../constants'; import getAppStatus from '../graphql/queries/client/app_status.query.graphql'; import CiConfigMergedPreview from './editor/ci_config_merged_preview.vue'; -import CiEditorHeader from './editor/ci_editor_header.vue'; import CiValidate from './validate/ci_validate.vue'; import TextEditor from './editor/text_editor.vue'; import EditorTab from './ui/editor_tab.vue'; @@ -87,19 +87,19 @@ export default { type: String, required: true, }, - isNewCiConfigFile: { + showHelpDrawer: { type: Boolean, required: true, }, - showDrawer: { + showJobAssistantDrawer: { type: Boolean, required: true, }, - showJobAssistantDrawer: { + showAiAssistantDrawer: { type: Boolean, required: true, }, - showAiAssistantDrawer: { + isNewCiConfigFile: { type: Boolean, required: true, }, @@ -196,7 +196,7 @@ export default { > <walkthrough-popover v-if="isNewCiConfigFile" v-on="$listeners" /> <ci-editor-header - :show-drawer="showDrawer" + :show-help-drawer="showHelpDrawer" :show-job-assistant-drawer="showJobAssistantDrawer" :show-ai-assistant-drawer="showAiAssistantDrawer" v-on="$listeners" diff --git a/app/assets/javascripts/ci/pipeline_editor/constants.js b/app/assets/javascripts/ci/pipeline_editor/constants.js index 912e0fcbff9..e85138e361f 100644 --- a/app/assets/javascripts/ci/pipeline_editor/constants.js +++ b/app/assets/javascripts/ci/pipeline_editor/constants.js @@ -1,5 +1,10 @@ import { s__ } from '~/locale'; +export const EDITOR_APP_DRAWER_HELP = 'HELP'; +export const EDITOR_APP_DRAWER_JOB_ASSISTANT = 'JOB_ASSISTANT'; +export const EDITOR_APP_DRAWER_AI_ASSISTANT = 'AI_ASSISTANT'; +export const EDITOR_APP_DRAWER_NONE = ''; + // Values for CI_CONFIG_STATUS_* comes from lint graphQL export const CI_CONFIG_STATUS_INVALID = 'INVALID'; export const CI_CONFIG_STATUS_VALID = 'VALID'; @@ -65,6 +70,7 @@ export const CI_YAML_LINK = 'CI_YAML_LINK'; export const pipelineEditorTrackingOptions = { label: 'pipeline_editor', actions: { + browseCatalog: 'browse_catalog', browseTemplates: 'browse_templates', closeHelpDrawer: 'close_help_drawer', commitCiConfig: 'commit_ci_config', diff --git a/app/assets/javascripts/ci/pipeline_editor/graphql/resolvers.js b/app/assets/javascripts/ci/pipeline_editor/graphql/resolvers.js index fa1c70c1994..ed5be66d07a 100644 --- a/app/assets/javascripts/ci/pipeline_editor/graphql/resolvers.js +++ b/app/assets/javascripts/ci/pipeline_editor/graphql/resolvers.js @@ -7,30 +7,36 @@ import getPipelineEtag from './queries/client/pipeline_etag.query.graphql'; export const resolvers = { Mutation: { lintCI: (_, { endpoint, content, dry_run }) => { - return axios.post(endpoint, { content, dry_run }).then(({ data }) => ({ - valid: data.valid, - errors: data.errors, - warnings: data.warnings, - jobs: data.jobs.map((job) => { - const only = job.only ? { refs: job.only.refs, __typename: 'CiLintJobOnlyPolicy' } : null; + return axios.post(endpoint, { content, dry_run }).then(({ data }) => { + const { errors, warnings, valid, jobs } = data; - return { - name: job.name, - stage: job.stage, - beforeScript: job.before_script, - script: job.script, - afterScript: job.after_script, - tags: job.tag_list, - environment: job.environment, - when: job.when, - allowFailure: job.allow_failure, - only, - except: job.except, - __typename: 'CiLintJob', - }; - }), - __typename: 'CiLintContent', - })); + return { + valid, + errors, + warnings, + jobs: jobs.map((job) => { + const only = job.only + ? { refs: job.only.refs, __typename: 'CiLintJobOnlyPolicy' } + : null; + + return { + name: job.name, + stage: job.stage, + beforeScript: job.before_script, + script: job.script, + afterScript: job.after_script, + tags: job.tag_list, + environment: job.environment, + when: job.when, + allowFailure: job.allow_failure, + only, + except: job.except, + __typename: 'CiLintJob', + }; + }), + __typename: 'CiLintContent', + }; + }); }, updateAppStatus: (_, { appStatus }, { cache }) => { cache.writeQuery({ diff --git a/app/assets/javascripts/ci/pipeline_editor/index.js b/app/assets/javascripts/ci/pipeline_editor/index.js index b8d6c27435d..bc20e478876 100644 --- a/app/assets/javascripts/ci/pipeline_editor/index.js +++ b/app/assets/javascripts/ci/pipeline_editor/index.js @@ -1,17 +1,6 @@ import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import createDefaultClient from '~/lib/graphql'; -import { parseBoolean } from '~/lib/utils/common_utils'; -import { EDITOR_APP_STATUS_LOADING } from './constants'; -import { CODE_SNIPPET_SOURCE_SETTINGS } from './components/code_snippet_alert/constants'; -import getCurrentBranch from './graphql/queries/client/current_branch.query.graphql'; -import getAppStatus from './graphql/queries/client/app_status.query.graphql'; -import getLastCommitBranch from './graphql/queries/client/last_commit_branch.query.graphql'; -import getPipelineEtag from './graphql/queries/client/pipeline_etag.query.graphql'; -import { resolvers } from './graphql/resolvers'; -import typeDefs from './graphql/typedefs.graphql'; -import PipelineEditorApp from './pipeline_editor_app.vue'; +import { createAppOptions } from 'ee_else_ce/ci/pipeline_editor/options'; export const initPipelineEditor = (selector = '#js-pipeline-editor') => { const el = document.querySelector(selector); @@ -20,129 +9,9 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => { return null; } - const { - // Add to apollo cache as it can be updated by future queries - initialBranchName, - pipelineEtag, - // Add to provide/inject API for static values - ciConfigPath, - ciExamplesHelpPagePath, - ciHelpPagePath, - ciLintPath, - ciTroubleshootingPath, - defaultBranch, - emptyStateIllustrationPath, - helpPaths, - includesHelpPagePath, - lintHelpPagePath, - needsHelpPagePath, - newMergeRequestPath, - pipelinePagePath, - projectFullPath, - projectPath, - projectNamespace, - simulatePipelineHelpPagePath, - totalBranches, - usesExternalConfig, - validateTabIllustrationPath, - ymlHelpPagePath, - aiChatAvailable, - } = el.dataset; + const options = createAppOptions(el); - const configurationPaths = Object.fromEntries( - Object.entries(CODE_SNIPPET_SOURCE_SETTINGS).map(([source, { datasetKey }]) => [ - source, - el.dataset[datasetKey], - ]), - ); - - Vue.use(VueApollo); - - const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(resolvers, { - typeDefs, - useGet: true, - }), - }); - const { cache } = apolloProvider.clients.defaultClient; - - cache.writeQuery({ - query: getAppStatus, - data: { - app: { - __typename: 'PipelineEditorApp', - status: EDITOR_APP_STATUS_LOADING, - }, - }, - }); - - cache.writeQuery({ - query: getCurrentBranch, - data: { - workBranches: { - __typename: 'BranchList', - current: { - __typename: 'WorkBranch', - name: initialBranchName || defaultBranch, - }, - }, - }, - }); - - cache.writeQuery({ - query: getLastCommitBranch, - data: { - workBranches: { - __typename: 'BranchList', - lastCommit: { - __typename: 'WorkBranch', - name: '', - }, - }, - }, - }); - - cache.writeQuery({ - query: getPipelineEtag, - data: { - etags: { - __typename: 'EtagValues', - pipeline: pipelineEtag, - }, - }, - }); - - return new Vue({ - el, - apolloProvider, - provide: { - aiChatAvailable: parseBoolean(aiChatAvailable), - ciConfigPath, - ciExamplesHelpPagePath, - ciHelpPagePath, - ciLintPath, - ciTroubleshootingPath, - configurationPaths, - dataMethod: 'graphql', - defaultBranch, - emptyStateIllustrationPath, - helpPaths, - includesHelpPagePath, - lintHelpPagePath, - needsHelpPagePath, - newMergeRequestPath, - pipelinePagePath, - projectFullPath, - projectPath, - projectNamespace, - simulatePipelineHelpPagePath, - totalBranches: parseInt(totalBranches, 10), - usesExternalConfig: parseBoolean(usesExternalConfig), - validateTabIllustrationPath, - ymlHelpPagePath, - }, - render(h) { - return h(PipelineEditorApp); - }, - }); + return new Vue(options); }; + +initPipelineEditor(); diff --git a/app/assets/javascripts/ci/pipeline_editor/options.js b/app/assets/javascripts/ci/pipeline_editor/options.js new file mode 100644 index 00000000000..922c8eee8fc --- /dev/null +++ b/app/assets/javascripts/ci/pipeline_editor/options.js @@ -0,0 +1,142 @@ +import Vue from 'vue'; + +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import { EDITOR_APP_STATUS_LOADING } from './constants'; +import { CODE_SNIPPET_SOURCE_SETTINGS } from './components/code_snippet_alert/constants'; +import getCurrentBranch from './graphql/queries/client/current_branch.query.graphql'; +import getAppStatus from './graphql/queries/client/app_status.query.graphql'; +import getLastCommitBranch from './graphql/queries/client/last_commit_branch.query.graphql'; +import getPipelineEtag from './graphql/queries/client/pipeline_etag.query.graphql'; +import { resolvers } from './graphql/resolvers'; +import typeDefs from './graphql/typedefs.graphql'; +import PipelineEditorApp from './pipeline_editor_app.vue'; + +export const createAppOptions = (el) => { + const { + // Add to apollo cache as it can be updated by future queries + initialBranchName, + pipelineEtag, + // Add to provide/inject API for static values + ciConfigPath, + ciExamplesHelpPagePath, + ciHelpPagePath, + ciLintPath, + ciTroubleshootingPath, + defaultBranch, + emptyStateIllustrationPath, + helpPaths, + includesHelpPagePath, + lintHelpPagePath, + needsHelpPagePath, + newMergeRequestPath, + pipelinePagePath, + projectFullPath, + projectPath, + projectNamespace, + simulatePipelineHelpPagePath, + totalBranches, + usesExternalConfig, + validateTabIllustrationPath, + ymlHelpPagePath, + aiChatAvailable, + } = el.dataset; + + const configurationPaths = Object.fromEntries( + Object.entries(CODE_SNIPPET_SOURCE_SETTINGS).map(([source, { datasetKey }]) => [ + source, + el.dataset[datasetKey], + ]), + ); + + Vue.use(VueApollo); + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(resolvers, { + typeDefs, + useGet: true, + }), + }); + const { cache } = apolloProvider.clients.defaultClient; + + cache.writeQuery({ + query: getAppStatus, + data: { + app: { + __typename: 'PipelineEditorApp', + status: EDITOR_APP_STATUS_LOADING, + }, + }, + }); + + cache.writeQuery({ + query: getCurrentBranch, + data: { + workBranches: { + __typename: 'BranchList', + current: { + __typename: 'WorkBranch', + name: initialBranchName || defaultBranch, + }, + }, + }, + }); + + cache.writeQuery({ + query: getLastCommitBranch, + data: { + workBranches: { + __typename: 'BranchList', + lastCommit: { + __typename: 'WorkBranch', + name: '', + }, + }, + }, + }); + + cache.writeQuery({ + query: getPipelineEtag, + data: { + etags: { + __typename: 'EtagValues', + pipeline: pipelineEtag, + }, + }, + }); + + return { + el, + apolloProvider, + provide: { + aiChatAvailable: parseBoolean(aiChatAvailable), + ciConfigPath, + ciExamplesHelpPagePath, + ciHelpPagePath, + ciLintPath, + ciTroubleshootingPath, + configurationPaths, + dataMethod: 'graphql', + defaultBranch, + emptyStateIllustrationPath, + helpPaths, + includesHelpPagePath, + lintHelpPagePath, + needsHelpPagePath, + newMergeRequestPath, + pipelinePagePath, + projectFullPath, + projectPath, + projectNamespace, + simulatePipelineHelpPagePath, + totalBranches: parseInt(totalBranches, 10), + usesExternalConfig: parseBoolean(usesExternalConfig), + validateTabIllustrationPath, + ymlHelpPagePath, + }, + render(h) { + return h(PipelineEditorApp); + }, + }; +}; diff --git a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue index 647e33333ce..0495546529a 100644 --- a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue +++ b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue @@ -1,6 +1,7 @@ <script> import { GlModal } from '@gitlab/ui'; import { __ } from '~/locale'; +import { DRAWER_Z_INDEX } from '~/lib/utils/constants'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import CommitSection from './components/commit/commit_section.vue'; import PipelineEditorDrawer from './components/drawer/pipeline_editor_drawer.vue'; @@ -9,12 +10,22 @@ import PipelineEditorFileNav from './components/file_nav/pipeline_editor_file_na import PipelineEditorFileTree from './components/file_tree/container.vue'; import PipelineEditorHeader from './components/header/pipeline_editor_header.vue'; import PipelineEditorTabs from './components/pipeline_editor_tabs.vue'; -import { CREATE_TAB, FILE_TREE_DISPLAY_KEY } from './constants'; +import { + CREATE_TAB, + FILE_TREE_DISPLAY_KEY, + EDITOR_APP_DRAWER_HELP, + EDITOR_APP_DRAWER_JOB_ASSISTANT, + EDITOR_APP_DRAWER_AI_ASSISTANT, + EDITOR_APP_DRAWER_NONE, +} from './constants'; const AiAssistantDrawer = () => import('ee_component/ci/pipeline_editor/components/ai_assistant_drawer.vue'); export default { + EDITOR_APP_DRAWER_HELP, + EDITOR_APP_DRAWER_JOB_ASSISTANT, + EDITOR_APP_DRAWER_AI_ASSISTANT, commitSectionRef: 'commitSectionRef', modal: { switchBranch: { @@ -67,15 +78,16 @@ export default { }, data() { return { + currentDrawer: EDITOR_APP_DRAWER_NONE, currentTab: CREATE_TAB, scrollToCommitForm: false, shouldLoadNewBranch: false, - showDrawer: false, - showJobAssistantDrawer: false, - showAiAssistantDrawer: false, - drawerIndex: 200, - jobAssistantIndex: 200, - aiAssistantIndex: 200, + currentDrawerIndex: DRAWER_Z_INDEX, + drawerIndex: { + [EDITOR_APP_DRAWER_HELP]: DRAWER_Z_INDEX, + [EDITOR_APP_DRAWER_JOB_ASSISTANT]: DRAWER_Z_INDEX, + [EDITOR_APP_DRAWER_AI_ASSISTANT]: DRAWER_Z_INDEX, + }, showFileTree: false, showSwitchBranchModal: false, }; @@ -87,6 +99,15 @@ export default { includesFiles() { return this.ciConfigData?.includes || []; }, + showHelpDrawer() { + return this.currentDrawer === EDITOR_APP_DRAWER_HELP; + }, + showJobAssistantDrawer() { + return this.currentDrawer === EDITOR_APP_DRAWER_JOB_ASSISTANT; + }, + showAiAssistantDrawer() { + return this.currentDrawer === EDITOR_APP_DRAWER_AI_ASSISTANT; + }, }, mounted() { this.showFileTree = JSON.parse(localStorage.getItem(FILE_TREE_DISPLAY_KEY)) || false; @@ -95,29 +116,15 @@ export default { closeBranchModal() { this.showSwitchBranchModal = false; }, - closeDrawer() { - this.showDrawer = false; - }, - closeJobAssistantDrawer() { - this.showJobAssistantDrawer = false; - }, - closeAiAssistantDrawer() { - this.showAiAssistantDrawer = false; - }, - openAiAssistantDrawer() { - this.showAiAssistantDrawer = true; - this.aiAssistantIndex = this.drawerIndex + 1; - }, handleConfirmSwitchBranch() { this.showSwitchBranchModal = true; }, - openDrawer() { - this.showDrawer = true; - this.drawerIndex = this.jobAssistantIndex + 1; - }, - openJobAssistantDrawer() { - this.showJobAssistantDrawer = true; - this.jobAssistantIndex = this.drawerIndex + 1; + switchDrawer(drawerName) { + this.currentDrawer = drawerName; + if (this.drawerIndex[drawerName]) { + this.currentDrawerIndex += 1; + this.drawerIndex[drawerName] = this.currentDrawerIndex; + } }, toggleFileTree() { this.showFileTree = !this.showFileTree; @@ -180,16 +187,11 @@ export default { :commit-sha="commitSha" :current-tab="currentTab" :is-new-ci-config-file="isNewCiConfigFile" - :show-drawer="showDrawer" + :show-help-drawer="showHelpDrawer" :show-job-assistant-drawer="showJobAssistantDrawer" :show-ai-assistant-drawer="showAiAssistantDrawer" v-on="$listeners" - @open-drawer="openDrawer" - @close-drawer="closeDrawer" - @open-job-assistant-drawer="openJobAssistantDrawer" - @close-job-assistant-drawer="closeJobAssistantDrawer" - @open-ai-assistant-drawer="openAiAssistantDrawer" - @close-ai-assistant-drawer="closeAiAssistantDrawer" + @switch-drawer="switchDrawer" @set-current-tab="setCurrentTab" @walkthrough-popover-cta-clicked="setScrollToCommitForm" /> @@ -207,24 +209,24 @@ export default { v-on="$listeners" /> <pipeline-editor-drawer - :is-visible="showDrawer" - :z-index="drawerIndex" + :is-visible="showHelpDrawer" + :z-index="drawerIndex[$options.EDITOR_APP_DRAWER_HELP]" v-on="$listeners" - @close-drawer="closeDrawer" + @switch-drawer="switchDrawer" /> <job-assistant-drawer :ci-config-data="ciConfigData" :ci-file-content="ciFileContent" :is-visible="showJobAssistantDrawer" - :z-index="jobAssistantIndex" + :z-index="drawerIndex[$options.EDITOR_APP_DRAWER_JOB_ASSISTANT]" v-on="$listeners" - @close-job-assistant-drawer="closeJobAssistantDrawer" + @switch-drawer="switchDrawer" /> <ai-assistant-drawer v-if="glFeatures.aiCiConfigGenerator" :is-visible="showAiAssistantDrawer" - :z-index="aiAssistantIndex" - @close-ai-assistant-drawer="closeAiAssistantDrawer" + :z-index="drawerIndex[$options.EDITOR_APP_DRAWER_AI_ASSISTANT]" + @switch-drawer="switchDrawer" /> </div> </template> diff --git a/app/assets/javascripts/ci/pipeline_new/components/refs_dropdown.vue b/app/assets/javascripts/ci/pipeline_new/components/refs_dropdown.vue index 429f8e78dbe..cfcc729b5c9 100644 --- a/app/assets/javascripts/ci/pipeline_new/components/refs_dropdown.vue +++ b/app/assets/javascripts/ci/pipeline_new/components/refs_dropdown.vue @@ -28,6 +28,13 @@ export default { required: false, default: () => ({}), }, + queryParams: { + type: Object, + required: false, + default: () => ({ + sort: 'updated_desc', + }), + }, }, computed: { refShortName() { @@ -51,6 +58,7 @@ export default { :project-id="projectId" :translations="$options.i18n" :use-symbolic-ref-names="true" + :query-params="queryParams" toggle-button-class="gl-w-auto! gl-mb-0!" @input="setRefSelected" /> diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue index f633ba053ee..39ac55bb9c5 100644 --- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue +++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue @@ -1,5 +1,5 @@ <script> -import scheduleSvg from '@gitlab/svgs/dist/illustrations/schedule-md.svg'; +import scheduleSvg from '@gitlab/svgs/dist/illustrations/schedule-md.svg?raw'; import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui'; import { helpPagePath } from '~/helpers/help_page_helper'; import { s__ } from '~/locale'; diff --git a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue index e4d47fba464..f0a41a5949e 100644 --- a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue +++ b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue @@ -1,6 +1,6 @@ <script> import { createAlert, VARIANT_SUCCESS } from '~/alert'; -import { redirectTo, setUrlParams } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated +import { visitUrl, setUrlParams } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue'; @@ -32,7 +32,7 @@ export default { message: s__('Runners|Runner created.'), variant: VARIANT_SUCCESS, }); - redirectTo(ephemeralRegisterUrl); // eslint-disable-line import/no-deprecated + visitUrl(ephemeralRegisterUrl); }, onError(error) { createAlert({ message: error.message }); @@ -60,7 +60,7 @@ export default { <hr aria-hidden="true" /> - <h2 class="gl-font-weight-normal gl-font-lg gl-my-5"> + <h2 class="gl-font-size-h2 gl-my-5"> {{ s__('Runners|Platform') }} </h2> <runner-platforms-radio-group v-model="platform" /> diff --git a/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue b/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue index 668a55d2437..d385d32fd9d 100644 --- a/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue +++ b/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue @@ -2,7 +2,7 @@ import { createAlert, VARIANT_SUCCESS } from '~/alert'; import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; -import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated +import { visitUrl } from '~/lib/utils/url_utility'; import RunnerDeleteButton from '../components/runner_delete_button.vue'; import RunnerEditButton from '../components/runner_edit_button.vue'; @@ -71,7 +71,7 @@ export default { }, onDeleted({ message }) { saveAlertToLocalStorage({ message, variant: VARIANT_SUCCESS }); - redirectTo(this.runnersPath); // eslint-disable-line import/no-deprecated + visitUrl(this.runnersPath); }, }, }; diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue index 4d04b5d4b14..e287e4e17d1 100644 --- a/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue +++ b/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue @@ -20,7 +20,13 @@ export default { }, computed: { paused() { - return !this.runner.active; + return this.runner.paused; + }, + contactedAt() { + return this.runner.contactedAt; + }, + status() { + return this.runner.status; }, }, }; @@ -29,7 +35,8 @@ export default { <template> <div> <runner-status-badge - :runner="runner" + :contacted-at="contactedAt" + :status="status" class="gl-display-inline-block gl-max-w-full gl-text-truncate" /> <runner-paused-badge diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue index f24fb5575ae..9f4ce14f704 100644 --- a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue +++ b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue @@ -8,6 +8,7 @@ import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import RunnerName from '../runner_name.vue'; import RunnerTags from '../runner_tags.vue'; import RunnerTypeBadge from '../runner_type_badge.vue'; +import RunnerManagersBadge from '../runner_managers_badge.vue'; import { formatJobCount } from '../../utils'; import { @@ -29,6 +30,7 @@ export default { RunnerName, RunnerTags, RunnerTypeBadge, + RunnerManagersBadge, RunnerUpgradeStatusIcon: () => import('ee_component/ci/runner/components/runner_upgrade_status_icon.vue'), UserAvatarLink, @@ -44,6 +46,9 @@ export default { }, }, computed: { + managersCount() { + return this.runner.managers?.count || 0; + }, jobCount() { return formatJobCount(this.runner.jobCount); }, @@ -75,6 +80,8 @@ export default { <slot :runner="runner" name="runner-name"> <runner-name :runner="runner" /> </slot> + + <runner-managers-badge :count="managersCount" size="sm" class="gl-vertical-align-middle" /> <gl-icon v-if="runner.locked" v-gl-tooltip @@ -87,7 +94,7 @@ export default { <div class="gl-mb-3 gl-ml-auto gl-display-inline-flex gl-max-w-full"> <template v-if="runner.version"> <div class="gl-flex-shrink-0"> - <runner-upgrade-status-icon :runner="runner" /> + <runner-upgrade-status-icon :upgrade-status="runner.upgradeStatus" /> <gl-sprintf :message="$options.i18n.I18N_VERSION_LABEL"> <template #version>{{ runner.version }}</template> </gl-sprintf> diff --git a/app/assets/javascripts/ci/runner/components/registration/platforms_drawer.vue b/app/assets/javascripts/ci/runner/components/registration/platforms_drawer.vue index ff182c61ccf..9cf2572c924 100644 --- a/app/assets/javascripts/ci/runner/components/registration/platforms_drawer.vue +++ b/app/assets/javascripts/ci/runner/components/registration/platforms_drawer.vue @@ -42,8 +42,8 @@ export default { }; }, computed: { - drawerHeightOffset() { - return getContentWrapperHeight('.content-wrapper'); + getDrawerHeaderHeight() { + return getContentWrapperHeight(); }, architectureOptions() { return platformArchitectures({ platform: this.selectedPlatform }); @@ -86,7 +86,7 @@ export default { <template> <gl-drawer :open="open" - :header-height="drawerHeightOffset" + :header-height="getDrawerHeaderHeight" :z-index="$options.DRAWER_Z_INDEX" data-testid="runner-platforms-drawer" @close="onClose" diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue b/app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue index fe19977f783..6fd4edf5847 100644 --- a/app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue +++ b/app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue @@ -1,5 +1,5 @@ <script> -import ILLUSTRATION_URL from '@gitlab/svgs/dist/illustrations/multi-editor_all_changes_committed_empty.svg?url'; +import ILLUSTRATION_URL from '@gitlab/svgs/dist/illustrations/rocket-launch-md.svg?url'; import { GlBanner } from '@gitlab/ui'; import { s__ } from '~/locale'; import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue'; diff --git a/app/assets/javascripts/ci/runner/components/runner_create_form.vue b/app/assets/javascripts/ci/runner/components/runner_create_form.vue index 6107b4dd3ea..1b363174d28 100644 --- a/app/assets/javascripts/ci/runner/components/runner_create_form.vue +++ b/app/assets/javascripts/ci/runner/components/runner_create_form.vue @@ -9,7 +9,7 @@ import { DEFAULT_ACCESS_LEVEL, PROJECT_TYPE, GROUP_TYPE, - INSTANCE_TYPE, + I18N_CREATE_ERROR, } from '../constants'; export default { @@ -40,11 +40,13 @@ export default { return { saving: false, runner: { + runnerType: this.runnerType, description: '', maintenanceNote: '', paused: false, accessLevel: DEFAULT_ACCESS_LEVEL, runUntagged: false, + locked: false, tagList: '', maximumTimeout: '', }, @@ -57,26 +59,22 @@ export default { if (this.runnerType === GROUP_TYPE) { return { ...input, - runnerType: GROUP_TYPE, groupId: this.groupId, }; } if (this.runnerType === PROJECT_TYPE) { return { ...input, - runnerType: PROJECT_TYPE, projectId: this.projectId, }; } - return { - ...input, - runnerType: INSTANCE_TYPE, - }; + return input; }, }, methods: { async onSubmit() { this.saving = true; + try { const { data: { @@ -90,16 +88,29 @@ export default { }); if (errors?.length) { - this.$emit('error', new Error(errors.join(' '))); - } else { - this.onSuccess(runner); + this.onError(new Error(errors.join(' ')), true); + return; + } + + if (!runner?.ephemeralRegisterUrl) { + // runner is missing information, report issue and + // fail naviation to register page. + this.onError(new Error(I18N_CREATE_ERROR)); + return; } + + this.onSuccess(runner); } catch (error) { + this.onError(error); + } + }, + onError(error, isValidationError = false) { + if (!isValidationError) { captureException({ error, component: this.$options.name }); - this.$emit('error', error); - } finally { - this.saving = false; } + + this.$emit('error', error); + this.saving = false; }, onSuccess(runner) { this.$emit('saved', runner); @@ -111,9 +122,9 @@ export default { <gl-form @submit.prevent="onSubmit"> <runner-form-fields v-model="runner" /> - <div class="gl-display-flex"> + <div class="gl-display-flex gl-mt-6"> <gl-button type="submit" variant="confirm" class="js-no-auto-disable" :loading="saving"> - {{ __('Submit') }} + {{ s__('Runners|Create runner') }} </gl-button> </div> </gl-form> diff --git a/app/assets/javascripts/ci/runner/components/runner_delete_button.vue b/app/assets/javascripts/ci/runner/components/runner_delete_button.vue index 020487fc727..3560521e8d7 100644 --- a/app/assets/javascripts/ci/runner/components/runner_delete_button.vue +++ b/app/assets/javascripts/ci/runner/components/runner_delete_button.vue @@ -45,6 +45,9 @@ export default { runnerName() { return `#${this.runnerId} (${this.runner.shortSha})`; }, + runnerManagersCount() { + return this.runner.managers?.count || 0; + }, runnerDeleteModalId() { return `delete-runner-modal-${this.runnerId}`; }, @@ -150,6 +153,7 @@ export default { <runner-delete-modal :modal-id="runnerDeleteModalId" :runner-name="runnerName" + :managers-count="runnerManagersCount" @primary="onDelete" /> </div> diff --git a/app/assets/javascripts/ci/runner/components/runner_delete_modal.vue b/app/assets/javascripts/ci/runner/components/runner_delete_modal.vue index 8be216a7eb5..93f79fd67ea 100644 --- a/app/assets/javascripts/ci/runner/components/runner_delete_modal.vue +++ b/app/assets/javascripts/ci/runner/components/runner_delete_modal.vue @@ -1,12 +1,9 @@ <script> import { GlModal } from '@gitlab/ui'; -import { __, s__, sprintf } from '~/locale'; +import { __, s__, n__, sprintf } from '~/locale'; const I18N_TITLE = s__('Runners|Delete runner %{name}?'); -const I18N_BODY = s__( - 'Runners|The runner will be permanently deleted and no longer available for projects or groups in the instance. Are you sure you want to continue?', -); -const I18N_PRIMARY = s__('Runners|Delete runner'); +const I18N_TITLE_PLURAL = s__('Runners|Delete %{count} runners?'); const I18N_CANCEL = __('Cancel'); export default { @@ -18,10 +15,40 @@ export default { type: String, required: true, }, + managersCount: { + type: Number, + required: false, + default: 0, + }, }, computed: { + count() { + // Only show count if MORE than 1 manager, for 0 we still + // assume 1 runner that happens to be disconnected. + return this.managersCount > 1 ? this.managersCount : 1; + }, title() { - return sprintf(I18N_TITLE, { name: this.runnerName }); + if (this.count === 1) { + return sprintf(I18N_TITLE, { name: this.runnerName }); + } + return sprintf(I18N_TITLE_PLURAL, { count: this.count }); + }, + body() { + return n__( + 'Runners|The runner will be permanently deleted and no longer available for projects or groups in the instance. Are you sure you want to continue?', + 'Runners|%d runners will be permanently deleted and no longer available for projects or groups in the instance. Are you sure you want to continue?', + this.count, + ); + }, + actionPrimary() { + return { + text: n__( + 'Runners|Permanently delete runner', + 'Runners|Permanently delete %d runners', + this.count, + ), + attributes: { variant: 'danger' }, + }; }, }, methods: { @@ -29,9 +56,7 @@ export default { this.$refs.modal.hide(); }, }, - actionPrimary: { text: I18N_PRIMARY, attributes: { variant: 'danger' } }, - actionCancel: { text: I18N_CANCEL }, - I18N_BODY, + ACTION_CANCEL: { text: I18N_CANCEL }, }; </script> @@ -40,12 +65,12 @@ export default { ref="modal" size="sm" :title="title" - :action-primary="$options.actionPrimary" - :action-cancel="$options.actionCancel" + :action-primary="actionPrimary" + :action-cancel="$options.ACTION_CANCEL" v-bind="$attrs" v-on="$listeners" @primary="onPrimary" > - {{ $options.I18N_BODY }} + {{ body }} </gl-modal> </template> diff --git a/app/assets/javascripts/ci/runner/components/runner_details.vue b/app/assets/javascripts/ci/runner/components/runner_details.vue index 6eba8f2e49f..8c1280cffb9 100644 --- a/app/assets/javascripts/ci/runner/components/runner_details.vue +++ b/app/assets/javascripts/ci/runner/components/runner_details.vue @@ -1,20 +1,28 @@ <script> -import { GlIntersperse, GlLink } from '@gitlab/ui'; +import { GlIntersperse, GlLink, GlSprintf } from '@gitlab/ui'; import { helpPagePath } from '~/helpers/help_page_helper'; import { s__ } from '~/locale'; import HelpPopover from '~/vue_shared/components/help_popover.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; -import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants'; +import { + ACCESS_LEVEL_REF_PROTECTED, + GROUP_TYPE, + PROJECT_TYPE, + RUNNER_MANAGERS_HELP_URL, + I18N_STATUS_NEVER_CONTACTED, +} from '../constants'; import RunnerDetail from './runner_detail.vue'; import RunnerGroups from './runner_groups.vue'; import RunnerProjects from './runner_projects.vue'; import RunnerTags from './runner_tags.vue'; +import RunnerManagersDetail from './runner_managers_detail.vue'; export default { components: { GlIntersperse, GlLink, + GlSprintf, HelpPopover, RunnerDetail, RunnerMaintenanceNoteDetail: () => @@ -26,6 +34,7 @@ export default { RunnerUpgradeStatusAlert: () => import('ee_component/ci/runner/components/runner_upgrade_status_alert.vue'), RunnerTags, + RunnerManagersDetail, TimeAgo, }, props: { @@ -76,6 +85,8 @@ export default { }, }, ACCESS_LEVEL_REF_PROTECTED, + RUNNER_MANAGERS_HELP_URL, + I18N_STATUS_NEVER_CONTACTED, }; </script> @@ -90,7 +101,7 @@ export default { <runner-detail :label="s__('Runners|Description')" :value="runner.description" /> <runner-detail :label="s__('Runners|Last contact')" - :empty-value="s__('Runners|Never contacted')" + :empty-value="$options.I18N_STATUS_NEVER_CONTACTED" > <template v-if="runner.contactedAt" #value> <time-ago :time="runner.contactedAt" /> @@ -150,6 +161,33 @@ export default { class="gl-pt-4 gl-border-t-gray-100 gl-border-t-1 gl-border-t-solid" :value="runner.maintenanceNoteHtml" /> + + <runner-detail> + <template #label> + {{ s__('Runners|Runners') }} + <help-popover> + <gl-sprintf + :message=" + s__( + 'Runners|Runners are grouped when they have the same authentication token. This happens when you re-use a runner configuration in more than one runner manager. %{linkStart}How does this work?%{linkEnd}', + ) + " + > + <template #link="{ content }" + ><gl-link + :href="$options.RUNNER_MANAGERS_HELP_URL" + target="_blank" + class="gl-reset-font-size" + >{{ content }}</gl-link + ></template + > + </gl-sprintf> + </help-popover> + </template> + <template #value> + <runner-managers-detail :runner="runner" /> + </template> + </runner-detail> </dl> </div> diff --git a/app/assets/javascripts/ci/runner/components/runner_form_fields.vue b/app/assets/javascripts/ci/runner/components/runner_form_fields.vue index e37ac5e6e26..d090a562ff7 100644 --- a/app/assets/javascripts/ci/runner/components/runner_form_fields.vue +++ b/app/assets/javascripts/ci/runner/components/runner_form_fields.vue @@ -1,7 +1,16 @@ <script> -import { GlFormGroup, GlFormCheckbox, GlFormInput, GlLink, GlSprintf } from '@gitlab/ui'; +import { isEqual } from 'lodash'; +import { + GlFormGroup, + GlFormCheckbox, + GlFormInput, + GlIcon, + GlLink, + GlSprintf, + GlSkeletonLoader, +} from '@gitlab/ui'; import { helpPagePath } from '~/helpers/help_page_helper'; -import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED } from '../constants'; +import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants'; export default { name: 'RunnerFormFields', @@ -9,8 +18,10 @@ export default { GlFormGroup, GlFormCheckbox, GlFormInput, + GlIcon, GlLink, GlSprintf, + GlSkeletonLoader, RunnerMaintenanceNoteField: () => import('ee_component/ci/runner/components/runner_maintenance_note_field.vue'), }, @@ -20,15 +31,32 @@ export default { default: null, required: false, }, + loading: { + type: Boolean, + default: false, + required: false, + }, }, data() { return { - model: { - ...this.value, - }, + model: null, }; }, + computed: { + canBeLockedToProject() { + return this.value?.runnerType === PROJECT_TYPE; + }, + }, watch: { + value: { + handler(newVal, oldVal) { + // update only when values change, avoids infinite loop + if (!isEqual(newVal, oldVal)) { + this.model = { ...newVal }; + } + }, + immediate: true, + }, model: { handler() { this.$emit('input', this.model); @@ -45,96 +73,122 @@ export default { </script> <template> <div> - <h2 class="gl-font-weight-normal gl-font-lg gl-my-5"> + <h2 class="gl-font-size-h2 gl-my-5"> + {{ s__('Runners|Tags') }} + </h2> + <gl-skeleton-loader v-if="loading" :lines="12" /> + <template v-else-if="model"> + <gl-form-group :label="__('Tags')" label-for="runner-tags"> + <template #description> + <gl-sprintf + :message=" + s__('Runners|Multiple tags must be separated by a comma. For example, %{example}.') + " + > + <template #example> + <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings --> + <code>macos, shared</code> + </template> + </gl-sprintf> + </template> + <template #label-description> + <gl-sprintf + :message=" + s__( + 'Runners|Add tags for the types of jobs the runner processes to ensure that the runner only runs jobs that you intend it to. %{helpLinkStart}Learn more.%{helpLinkEnd}', + ) + " + > + <template #helpLink="{ content }"> + <gl-link :href="$options.HELP_LABELS_PAGE_PATH" target="_blank">{{ + content + }}</gl-link> + </template> + </gl-sprintf> + </template> + <gl-form-input id="runner-tags" v-model="model.tagList" name="tags" /> + </gl-form-group> + <gl-form-checkbox v-model="model.runUntagged" name="run-untagged"> + {{ __('Run untagged jobs') }} + <template #help> + {{ s__('Runners|Use the runner for jobs without tags in addition to tagged jobs.') }} + </template> + </gl-form-checkbox> + </template> + + <hr aria-hidden="true" /> + + <h2 class="gl-font-size-h2 gl-my-5"> {{ s__('Runners|Details') }} {{ __('(optional)') }} </h2> - <gl-form-group :label="s__('Runners|Runner description')" label-for="runner-description"> - <gl-form-input id="runner-description" v-model="model.description" name="description" /> - </gl-form-group> - <runner-maintenance-note-field v-model="model.maintenanceNote" class="gl-mt-5" /> + <gl-skeleton-loader v-if="loading" :lines="15" /> + <template v-else-if="model"> + <gl-form-group :label="s__('Runners|Runner description')" label-for="runner-description"> + <gl-form-input id="runner-description" v-model="model.description" name="description" /> + </gl-form-group> + <runner-maintenance-note-field v-model="model.maintenanceNote" class="gl-mt-5" /> + </template> <hr aria-hidden="true" /> - <h2 class="gl-font-weight-normal gl-font-lg gl-my-5"> + <h2 class="gl-font-size-h2 gl-my-5"> {{ s__('Runners|Configuration') }} {{ __('(optional)') }} </h2> - <div class="gl-mb-5"> - <gl-form-checkbox v-model="model.paused" name="paused"> - {{ __('Paused') }} - <template #help> - {{ s__('Runners|Stop the runner from accepting new jobs.') }} - </template> - </gl-form-checkbox> - - <gl-form-checkbox - v-model="model.accessLevel" - name="protected" - :value="$options.ACCESS_LEVEL_REF_PROTECTED" - :unchecked-value="$options.ACCESS_LEVEL_NOT_PROTECTED" - > - {{ __('Protected') }} - <template #help> - {{ s__('Runners|Use the runner on pipelines for protected branches only.') }} - </template> - </gl-form-checkbox> - - <gl-form-checkbox v-model="model.runUntagged" name="run-untagged"> - {{ __('Run untagged jobs') }} - <template #help> - {{ s__('Runners|Use the runner for jobs without tags in addition to tagged jobs.') }} - </template> - </gl-form-checkbox> - </div> + <gl-skeleton-loader v-if="loading" :lines="15" /> + <template v-else-if="model"> + <div class="gl-mb-5"> + <gl-form-checkbox v-model="model.paused" name="paused"> + {{ __('Paused') }} + <template #help> + {{ s__('Runners|Stop the runner from accepting new jobs.') }} + </template> + </gl-form-checkbox> - <gl-form-group :label="__('Tags')" label-for="runner-tags"> - <template #description> - <gl-sprintf - :message=" - s__('Runners|Multiple tags must be separated by a comma. For example, %{example}.') - " + <gl-form-checkbox + v-model="model.accessLevel" + name="protected" + :value="$options.ACCESS_LEVEL_REF_PROTECTED" + :unchecked-value="$options.ACCESS_LEVEL_NOT_PROTECTED" > - <template #example> - <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings --> - <code>macos, shared</code> + {{ __('Protected') }} + <template #help> + {{ s__('Runners|Use the runner on pipelines for protected branches only.') }} </template> - </gl-sprintf> - </template> - <template #label-description> - <gl-sprintf - :message=" - s__( - 'Runners|Add tags for the types of jobs the runner processes to ensure that the runner only runs jobs that you intend it to. %{helpLinkStart}Learn more.%{helpLinkEnd}', - ) - " - > - <template #helpLink="{ content }"> - <gl-link :href="$options.HELP_LABELS_PAGE_PATH" target="_blank">{{ content }}</gl-link> + </gl-form-checkbox> + + <gl-form-checkbox v-if="canBeLockedToProject" v-model="model.locked" name="locked"> + {{ __('Lock to current projects') }} <gl-icon name="lock" /> + <template #help> + {{ + s__( + 'Runners|Use the runner for the currently assigned projects only. Only administrators can change the assigned projects.', + ) + }} </template> - </gl-sprintf> - </template> - <gl-form-input id="runner-tags" v-model="model.tagList" name="tags" /> - </gl-form-group> + </gl-form-checkbox> + </div> - <gl-form-group - :label="__('Maximum job timeout')" - :label-description=" - s__( - 'Runners|Maximum amount of time the runner can run before it terminates. If a project has a shorter job timeout period, the job timeout period of the instance runner is used instead.', - ) - " - label-for="runner-max-timeout" - :description="s__('Runners|Enter the number of seconds.')" - > - <gl-form-input - id="runner-max-timeout" - v-model.number="model.maximumTimeout" - name="max-timeout" - type="number" - /> - </gl-form-group> + <gl-form-group + :label="__('Maximum job timeout')" + :label-description=" + s__( + 'Runners|Maximum amount of time the runner can run before it terminates. If a project has a shorter job timeout period, the job timeout period of the instance runner is used instead.', + ) + " + label-for="runner-max-timeout" + :description="s__('Runners|Enter the number of seconds.')" + > + <gl-form-input + id="runner-max-timeout" + v-model.number="model.maximumTimeout" + name="max-timeout" + type="number" + /> + </gl-form-group> + </template> </div> </template> diff --git a/app/assets/javascripts/ci/runner/components/runner_header.vue b/app/assets/javascripts/ci/runner/components/runner_header.vue index 874c234ca4c..f46e894bf2e 100644 --- a/app/assets/javascripts/ci/runner/components/runner_header.vue +++ b/app/assets/javascripts/ci/runner/components/runner_header.vue @@ -1,9 +1,8 @@ <script> import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; -import { sprintf } from '~/locale'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { I18N_DETAILS_TITLE, I18N_LOCKED_RUNNER_DESCRIPTION } from '../constants'; +import { I18N_LOCKED_RUNNER_DESCRIPTION } from '../constants'; +import { formatRunnerName } from '../utils'; import RunnerTypeBadge from './runner_type_badge.vue'; import RunnerStatusBadge from './runner_status_badge.vue'; @@ -25,12 +24,8 @@ export default { }, }, computed: { - paused() { - return !this.runner.active; - }, - heading() { - const id = getIdFromGraphQLId(this.runner.id); - return sprintf(I18N_DETAILS_TITLE, { runner_id: id }); + name() { + return formatRunnerName(this.runner); }, }, I18N_LOCKED_RUNNER_DESCRIPTION, @@ -38,16 +33,16 @@ export default { </script> <template> <div - class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-gap-3 gl-flex-wrap gl-py-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100" + class="gl-display-flex gl-justify-content-space-between gl-align-items-flex-start gl-gap-3 gl-flex-wrap gl-py-5" > - <div class="gl-display-flex gl-align-items-flex-start gl-gap-3 gl-flex-wrap"> - <runner-status-badge :runner="runner" /> - <runner-type-badge v-if="runner" :type="runner.runnerType" /> - <span> - <template v-if="runner.createdAt"> - <gl-sprintf :message="__('%{runner} created %{timeago}')"> - <template #runner> - <strong>{{ heading }}</strong> + <div> + <h1 class="gl-font-size-h-display gl-my-0">{{ name }}</h1> + <div class="gl-display-flex gl-align-items-flex-start gl-gap-3 gl-flex-wrap gl-mt-3"> + <runner-status-badge :contacted-at="runner.contactedAt" :status="runner.status" /> + <runner-type-badge :type="runner.runnerType" /> + <span v-if="runner.createdAt"> + <gl-sprintf :message="__('%{locked} created %{timeago}')"> + <template #locked> <gl-icon v-if="runner.locked" v-gl-tooltip="$options.I18N_LOCKED_RUNNER_DESCRIPTION" @@ -59,11 +54,8 @@ export default { <time-ago :time="runner.createdAt" /> </template> </gl-sprintf> - </template> - <template v-else> - <strong>{{ heading }}</strong> - </template> - </span> + </span> + </div> </div> <div class="gl-display-flex gl-gap-3 gl-flex-wrap"><slot name="actions"></slot></div> </div> diff --git a/app/assets/javascripts/ci/runner/components/runner_jobs_empty_state.vue b/app/assets/javascripts/ci/runner/components/runner_jobs_empty_state.vue index c30a824120d..4e68c2ea71a 100644 --- a/app/assets/javascripts/ci/runner/components/runner_jobs_empty_state.vue +++ b/app/assets/javascripts/ci/runner/components/runner_jobs_empty_state.vue @@ -1,5 +1,5 @@ <script> -import EMPTY_STATE_SVG_URL from '@gitlab/svgs/dist/illustrations/pipelines_empty.svg?url'; +import EMPTY_STATE_SVG_URL from '@gitlab/svgs/dist/illustrations/empty-state/empty-pipeline-md.svg?url'; import { GlEmptyState } from '@gitlab/ui'; import { s__ } from '~/locale'; @@ -19,7 +19,11 @@ export default { </script> <template> - <gl-empty-state :svg-path="$options.EMPTY_STATE_SVG_URL" :title="$options.i18n.title"> + <gl-empty-state + :svg-path="$options.EMPTY_STATE_SVG_URL" + :svg-height="150" + :title="$options.i18n.title" + > <template #description> <p>{{ $options.i18n.description }}</p> </template> diff --git a/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue b/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue index 8606c22db34..d2836962a97 100644 --- a/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue +++ b/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue @@ -1,10 +1,20 @@ <script> -import EMPTY_STATE_SVG_URL from '@gitlab/svgs/dist/illustrations/pipelines_empty.svg?url'; -import FILTERED_SVG_URL from '@gitlab/svgs/dist/illustrations/magnifying-glass.svg?url'; +import EMPTY_STATE_SVG_URL from '@gitlab/svgs/dist/illustrations/empty-state/empty-pipeline-md.svg?url'; +import FILTERED_SVG_URL from '@gitlab/svgs/dist/illustrations/empty-state/empty-search-md.svg?url'; import { GlEmptyState, GlLink, GlSprintf, GlModalDirective } from '@gitlab/ui'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; +import { + I18N_GET_STARTED, + I18N_RUNNERS_ARE_AGENTS, + I18N_CREATE_RUNNER_LINK, + I18N_STILL_USING_REGISTRATION_TOKENS, + I18N_CONTACT_ADMIN_TO_REGISTER, + I18N_FOLLOW_REGISTRATION_INSTRUCTIONS, + I18N_NO_RESULTS, + I18N_EDIT_YOUR_SEARCH, +} from '~/ci/runner/constants'; export default { components: { @@ -38,9 +48,8 @@ export default { shouldShowCreateRunnerWorkflow() { // create_runner_workflow_for_admin or create_runner_workflow_for_namespace return ( - this.newRunnerPath && - (this.glFeatures?.createRunnerWorkflowForAdmin || - this.glFeatures?.createRunnerWorkflowForNamespace) + this.glFeatures?.createRunnerWorkflowForAdmin || + this.glFeatures?.createRunnerWorkflowForNamespace ); }, }, @@ -48,35 +57,59 @@ export default { svgHeight: 145, EMPTY_STATE_SVG_URL, FILTERED_SVG_URL, + + I18N_GET_STARTED, + I18N_RUNNERS_ARE_AGENTS, + I18N_CREATE_RUNNER_LINK, + I18N_STILL_USING_REGISTRATION_TOKENS, + I18N_CONTACT_ADMIN_TO_REGISTER, + I18N_FOLLOW_REGISTRATION_INSTRUCTIONS, + I18N_NO_RESULTS, + I18N_EDIT_YOUR_SEARCH, }; </script> <template> <gl-empty-state v-if="isSearchFiltered" - :title="s__('Runners|No results found')" + :title="$options.I18N_NO_RESULTS" :svg-path="$options.FILTERED_SVG_URL" :svg-height="$options.svgHeight" - :description="s__('Runners|Edit your search and try again')" + :description="$options.I18N_EDIT_YOUR_SEARCH" /> <gl-empty-state v-else - :title="s__('Runners|Get started with runners')" + :title="$options.I18N_GET_STARTED" :svg-path="$options.EMPTY_STATE_SVG_URL" :svg-height="$options.svgHeight" > - <template v-if="registrationToken" #description> + <template #description> + {{ $options.I18N_RUNNERS_ARE_AGENTS }} + <template v-if="shouldShowCreateRunnerWorkflow"> + <gl-sprintf v-if="newRunnerPath" :message="$options.I18N_CREATE_RUNNER_LINK"> + <template #link="{ content }"> + <gl-link :href="newRunnerPath">{{ content }}</gl-link> + </template> + </gl-sprintf> + <template v-if="registrationToken"> + <br /> + <gl-link v-gl-modal="$options.modalId">{{ + $options.I18N_STILL_USING_REGISTRATION_TOKENS + }}</gl-link> + <runner-instructions-modal + :modal-id="$options.modalId" + :registration-token="registrationToken" + /> + </template> + <template v-if="!newRunnerPath && !registrationToken"> + {{ $options.I18N_CONTACT_ADMIN_TO_REGISTER }} + </template> + </template> <gl-sprintf - :message=" - s__( - 'Runners|Runners are the agents that run your CI/CD jobs. Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.', - ) - " + v-else-if="registrationToken" + :message="$options.I18N_FOLLOW_REGISTRATION_INSTRUCTIONS" > - <template v-if="shouldShowCreateRunnerWorkflow" #link="{ content }"> - <gl-link :href="newRunnerPath">{{ content }}</gl-link> - </template> - <template v-else #link="{ content }"> + <template #link="{ content }"> <gl-link v-gl-modal="$options.modalId">{{ content }}</gl-link> <runner-instructions-modal :modal-id="$options.modalId" @@ -84,13 +117,9 @@ export default { /> </template> </gl-sprintf> - </template> - <template v-else #description> - {{ - s__( - 'Runners|Runners are the agents that run your CI/CD jobs. To register new runners, please contact your administrator.', - ) - }} + <template v-else> + {{ $options.I18N_CONTACT_ADMIN_TO_REGISTER }} + </template> </template> </gl-empty-state> </template> diff --git a/app/assets/javascripts/ci/runner/components/runner_managers_badge.vue b/app/assets/javascripts/ci/runner/components/runner_managers_badge.vue new file mode 100644 index 00000000000..d298d8ded82 --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/runner_managers_badge.vue @@ -0,0 +1,47 @@ +<script> +import { GlBadge, GlTooltipDirective } from '@gitlab/ui'; +import { formatNumber, s__, sprintf } from '~/locale'; + +export default { + name: 'RunnerManagersBadge', + components: { + GlBadge, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + count: { + type: Number, + required: false, + default: 0, + }, + }, + computed: { + shouldShowBadge() { + // runner managers can be grouped, but this information is only shown + // when we have 2 or more. + return this.count >= 2; + }, + formattedCount() { + return formatNumber(this.count); + }, + tooltip() { + return sprintf(s__('Runners|%{count} runners in this group'), { + count: this.formattedCount, + }); + }, + }, +}; +</script> +<template> + <gl-badge + v-if="shouldShowBadge" + v-gl-tooltip="tooltip" + variant="muted" + icon="container-image" + v-bind="$attrs" + > + {{ formattedCount }} + </gl-badge> +</template> diff --git a/app/assets/javascripts/ci/runner/components/runner_managers_detail.vue b/app/assets/javascripts/ci/runner/components/runner_managers_detail.vue new file mode 100644 index 00000000000..5cc1bbef481 --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/runner_managers_detail.vue @@ -0,0 +1,111 @@ +<script> +import { GlCollapse, GlButton, GlIcon, GlSkeletonLoader } from '@gitlab/ui'; +import { __, s__, formatNumber } from '~/locale'; +import { createAlert } from '~/alert'; +import runnerManagersQuery from '../graphql/show/runner_managers.query.graphql'; +import { I18N_FETCH_ERROR } from '../constants'; +import { captureException } from '../sentry_utils'; +import { tableField } from '../utils'; +import RunnerManagersTable from './runner_managers_table.vue'; + +export default { + name: 'RunnerManagersDetail', + components: { + GlCollapse, + GlButton, + GlIcon, + GlSkeletonLoader, + RunnerManagersTable, + }, + props: { + runner: { + type: Object, + required: true, + validator: (runner) => { + return Boolean(runner?.id); + }, + }, + }, + data() { + return { + skip: true, + expanded: false, + managers: [], + }; + }, + apollo: { + managers: { + query: runnerManagersQuery, + skip() { + return this.skip; + }, + variables() { + return { runnerId: this.runner.id }; + }, + update({ runner }) { + return runner?.managers?.nodes || []; + }, + error(error) { + createAlert({ message: I18N_FETCH_ERROR }); + captureException({ error, component: this.$options.name }); + }, + }, + }, + computed: { + runnerManagersCount() { + return this.runner?.managers?.count || 0; + }, + runnerManagersCountFormatted() { + return formatNumber(this.runnerManagersCount); + }, + icon() { + return this.expanded ? 'chevron-down' : 'chevron-right'; + }, + text() { + return this.expanded ? __('Hide details') : __('Show details'); + }, + loading() { + return this.$apollo?.queries.managers.loading; + }, + }, + methods: { + fetchManagers() { + this.skip = false; + }, + toggleExpanded() { + this.expanded = !this.expanded; + }, + }, + fields: [ + tableField({ key: 'systemId', label: s__('Runners|System ID') }), + tableField({ + key: 'contactedAt', + label: s__('Runners|Last contact'), + tdClass: ['gl-text-right'], + thClasses: ['gl-text-right'], + }), + ], +}; +</script> + +<template> + <div> + <gl-icon name="container-image" class="gl-text-secondary" /> + {{ runnerManagersCountFormatted }} + <gl-button + v-if="runnerManagersCount" + variant="link" + @mouseover.once="fetchManagers" + @focus.once="fetchManagers" + @click.once="fetchManagers" + @click="toggleExpanded" + > + <gl-icon :name="icon" /> {{ text }} + </gl-button> + + <gl-collapse :visible="expanded" class="gl-mt-5"> + <gl-skeleton-loader v-if="loading" /> + <runner-managers-table v-else-if="managers.length" :items="managers" /> + </gl-collapse> + </div> +</template> diff --git a/app/assets/javascripts/ci/runner/components/runner_managers_table.vue b/app/assets/javascripts/ci/runner/components/runner_managers_table.vue new file mode 100644 index 00000000000..10790c398b0 --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/runner_managers_table.vue @@ -0,0 +1,75 @@ +<script> +import { GlIntersperse, GlTableLite } from '@gitlab/ui'; +import HelpPopover from '~/vue_shared/components/help_popover.vue'; +import { s__ } from '~/locale'; +import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; +import { tableField } from '../utils'; +import { I18N_STATUS_NEVER_CONTACTED } from '../constants'; +import RunnerStatusBadge from './runner_status_badge.vue'; + +export default { + name: 'RunnerManagersTable', + components: { + GlTableLite, + TimeAgo, + HelpPopover, + GlIntersperse, + RunnerStatusBadge, + RunnerUpgradeStatusIcon: () => + import('ee_component/ci/runner/components/runner_upgrade_status_icon.vue'), + }, + props: { + items: { + type: Array, + required: false, + default: () => [], + }, + }, + fields: [ + tableField({ key: 'systemId', label: s__('Runners|System ID') }), + tableField({ key: 'status', label: s__('Runners|Status') }), + tableField({ key: 'version', label: s__('Runners|Version') }), + tableField({ key: 'ipAddress', label: s__('Runners|IP Address') }), + tableField({ key: 'executorName', label: s__('Runners|Executor') }), + tableField({ key: 'architecturePlatform', label: s__('Runners|Arch/Platform') }), + tableField({ + key: 'contactedAt', + label: s__('Runners|Last contact'), + tdClass: ['gl-text-right'], + thClasses: ['gl-text-right'], + }), + ], + I18N_STATUS_NEVER_CONTACTED, +}; +</script> + +<template> + <gl-table-lite :fields="$options.fields" :items="items"> + <template #head(systemId)="{ label }"> + {{ label }} + <help-popover> + {{ s__('Runners|The unique ID for each runner that uses this configuration.') }} + </help-popover> + </template> + <template #cell(status)="{ item = {} }"> + <runner-status-badge :contacted-at="item.contactedAt" :status="item.status" /> + </template> + <template #cell(version)="{ item = {} }"> + {{ item.version }} + <template v-if="item.revision">({{ item.revision }})</template> + <runner-upgrade-status-icon :upgrade-status="item.upgradeStatus" /> + </template> + <template #cell(architecturePlatform)="{ item = {} }"> + <gl-intersperse separator="/"> + <span v-if="item.architectureName">{{ item.architectureName }}</span> + <span v-if="item.platformName">{{ item.platformName }}</span> + </gl-intersperse> + </template> + <template #cell(contactedAt)="{ item = {} }"> + <template v-if="item.contactedAt"> + <time-ago :time="item.contactedAt" /> + </template> + <template v-else>{{ $options.I18N_STATUS_NEVER_CONTACTED }}</template> + </template> + </gl-table-lite> +</template> diff --git a/app/assets/javascripts/ci/runner/components/runner_name.vue b/app/assets/javascripts/ci/runner/components/runner_name.vue index d4ecfd2d776..a877ff0f06c 100644 --- a/app/assets/javascripts/ci/runner/components/runner_name.vue +++ b/app/assets/javascripts/ci/runner/components/runner_name.vue @@ -1,5 +1,5 @@ <script> -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { formatRunnerName } from '../utils'; export default { props: { @@ -8,13 +8,13 @@ export default { required: true, }, }, - methods: { - getIdFromGraphQLId, + computed: { + name() { + return formatRunnerName(this.runner); + }, }, }; </script> <template> - <span class="gl-font-weight-bold gl-vertical-align-middle" - >#{{ getIdFromGraphQLId(runner.id) }} ({{ runner.shortSha }})</span - > + <span class="gl-font-weight-bold gl-vertical-align-middle">{{ name }}</span> </template> diff --git a/app/assets/javascripts/ci/runner/components/runner_pause_button.vue b/app/assets/javascripts/ci/runner/components/runner_pause_button.vue index a27af232e97..d16c8f98bad 100644 --- a/app/assets/javascripts/ci/runner/components/runner_pause_button.vue +++ b/app/assets/javascripts/ci/runner/components/runner_pause_button.vue @@ -1,6 +1,6 @@ <script> import { GlButton, GlTooltipDirective } from '@gitlab/ui'; -import runnerToggleActiveMutation from '~/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql'; +import runnerTogglePausedMutation from '~/ci/runner/graphql/shared/runner_toggle_paused.mutation.graphql'; import { createAlert } from '~/alert'; import { captureException } from '~/ci/runner/sentry_utils'; import { I18N_PAUSE, I18N_PAUSE_TOOLTIP, I18N_RESUME, I18N_RESUME_TOOLTIP } from '../constants'; @@ -31,14 +31,14 @@ export default { }; }, computed: { - isActive() { - return this.runner.active; + isPaused() { + return this.runner.paused; }, icon() { - return this.isActive ? 'pause' : 'play'; + return this.isPaused ? 'play' : 'pause'; }, label() { - return this.isActive ? I18N_PAUSE : I18N_RESUME; + return this.isPaused ? I18N_RESUME : I18N_PAUSE; }, buttonContent() { if (this.compact) { @@ -56,7 +56,7 @@ export default { // Prevent a "sticky" tooltip: If this button is disabled, // mouseout listeners don't run leaving the tooltip stuck if (!this.updating) { - return this.isActive ? I18N_PAUSE_TOOLTIP : I18N_RESUME_TOOLTIP; + return this.isPaused ? I18N_RESUME_TOOLTIP : I18N_PAUSE_TOOLTIP; } return ''; }, @@ -67,7 +67,7 @@ export default { try { const input = { id: this.runner.id, - active: !this.isActive, + paused: !this.isPaused, }; const { @@ -75,7 +75,7 @@ export default { runnerUpdate: { errors }, }, } = await this.$apollo.mutate({ - mutation: runnerToggleActiveMutation, + mutation: runnerTogglePausedMutation, variables: { input, }, diff --git a/app/assets/javascripts/ci/runner/components/runner_status_badge.vue b/app/assets/javascripts/ci/runner/components/runner_status_badge.vue index d084408781e..c2c52bd756a 100644 --- a/app/assets/javascripts/ci/runner/components/runner_status_badge.vue +++ b/app/assets/javascripts/ci/runner/components/runner_status_badge.vue @@ -26,21 +26,27 @@ export default { GlTooltip: GlTooltipDirective, }, props: { - runner: { - required: true, - type: Object, + contactedAt: { + type: String, + required: false, + default: null, + }, + status: { + type: String, + required: false, + default: null, }, }, computed: { contactedAtTimeAgo() { - if (this.runner.contactedAt) { - return getTimeago().format(this.runner.contactedAt); + if (this.contactedAt) { + return getTimeago().format(this.contactedAt); } // Prevent "just now" from being rendered, in case data is missing. return __('never'); }, badge() { - switch (this.runner?.status) { + switch (this.status) { case STATUS_ONLINE: return { icon: 'status-active', @@ -68,7 +74,7 @@ export default { variant: 'warning', label: I18N_STATUS_STALE, // runner may have contacted (or not) and be stale: consider both cases. - tooltip: this.runner.contactedAt + tooltip: this.contactedAt ? this.timeAgoTooltip(I18N_STALE_TIMEAGO_TOOLTIP) : I18N_STALE_NEVER_CONTACTED_TOOLTIP, }; diff --git a/app/assets/javascripts/ci/runner/components/runner_update_form.vue b/app/assets/javascripts/ci/runner/components/runner_update_form.vue index 2d34c551d6d..6b94e594f1c 100644 --- a/app/assets/javascripts/ci/runner/components/runner_update_form.vue +++ b/app/assets/javascripts/ci/runner/components/runner_update_form.vue @@ -1,23 +1,16 @@ <script> -import { - GlButton, - GlIcon, - GlForm, - GlFormCheckbox, - GlFormGroup, - GlFormInputGroup, - GlSkeletonLoader, - GlTooltipDirective, -} from '@gitlab/ui'; +import { GlButton, GlForm } from '@gitlab/ui'; +import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; +import { visitUrl } from '~/lib/utils/url_utility'; +import { __ } from '~/locale'; +import { captureException } from '~/ci/runner/sentry_utils'; + import { modelToUpdateMutationVariables, runnerToModel, } from 'ee_else_ce/ci/runner/runner_update_form_utils'; -import { createAlert, VARIANT_SUCCESS } from '~/alert'; -import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated -import { __ } from '~/locale'; -import { captureException } from '~/ci/runner/sentry_utils'; -import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants'; +import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED } from '../constants'; import runnerUpdateMutation from '../graphql/edit/runner_update.mutation.graphql'; import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage'; @@ -25,20 +18,11 @@ export default { name: 'RunnerUpdateForm', components: { GlButton, - GlIcon, GlForm, - GlFormCheckbox, - GlFormGroup, - GlFormInputGroup, - GlSkeletonLoader, - RunnerMaintenanceNoteField: () => - import('ee_component/ci/runner/components/runner_maintenance_note_field.vue'), + RunnerFormFields, RunnerUpdateCostFactorFields: () => import('ee_component/ci/runner/components/runner_update_cost_factor_fields.vue'), }, - directives: { - GlTooltip: GlTooltipDirective, - }, props: { runner: { type: Object, @@ -59,19 +43,17 @@ export default { data() { return { saving: false, - model: runnerToModel(this.runner), + model: null, }; }, computed: { - canBeLockedToProject() { - return this.runner?.runnerType === PROJECT_TYPE; + runnerType() { + return this.runner?.runnerType; }, }, watch: { - runner(newVal, oldVal) { - if (oldVal === null) { - this.model = runnerToModel(newVal); - } + runner(val) { + this.model = runnerToModel(val); }, }, methods: { @@ -101,7 +83,7 @@ export default { }, onSuccess() { saveAlertToLocalStorage({ message: __('Changes saved.'), variant: VARIANT_SUCCESS }); - redirectTo(this.runnerPath); // eslint-disable-line import/no-deprecated + visitUrl(this.runnerPath); }, onError(message) { this.saving = false; @@ -114,99 +96,8 @@ export default { </script> <template> <gl-form @submit.prevent="onSubmit"> - <h4 class="gl-font-lg gl-my-5">{{ s__('Runners|Details') }}</h4> - - <gl-skeleton-loader v-if="loading" /> - - <template v-else> - <gl-form-group :label="__('Description')" data-testid="runner-field-description"> - <gl-form-input-group v-model="model.description" /> - </gl-form-group> - <runner-maintenance-note-field v-model="model.maintenanceNote" /> - </template> - - <hr /> - - <h4 class="gl-font-lg gl-my-5">{{ s__('Runners|Configuration') }}</h4> - - <template v-if="loading"> - <gl-skeleton-loader v-for="i in 3" :key="i" /> - </template> - <template v-else> - <div class="gl-mb-5"> - <gl-form-checkbox - v-model="model.active" - data-testid="runner-field-paused" - :value="false" - :unchecked-value="true" - > - {{ __('Paused') }} - <template #help> - {{ s__('Runners|Stop the runner from accepting new jobs.') }} - </template> - </gl-form-checkbox> - - <gl-form-checkbox - v-model="model.accessLevel" - data-testid="runner-field-protected" - :value="$options.ACCESS_LEVEL_REF_PROTECTED" - :unchecked-value="$options.ACCESS_LEVEL_NOT_PROTECTED" - > - {{ __('Protected') }} - <template #help> - {{ s__('Runners|Use the runner on pipelines for protected branches only.') }} - </template> - </gl-form-checkbox> - - <gl-form-checkbox v-model="model.runUntagged" data-testid="runner-field-run-untagged"> - {{ __('Run untagged jobs') }} - <template #help> - {{ s__('Runners|Use the runner for jobs without tags, in addition to tagged jobs.') }} - </template> - </gl-form-checkbox> - - <gl-form-checkbox - v-if="canBeLockedToProject" - v-model="model.locked" - data-testid="runner-field-locked" - > - {{ __('Lock to current projects') }} <gl-icon name="lock" /> - <template #help> - {{ - s__( - 'Runners|Use the runner for the currently assigned projects only. Only administrators can change the assigned projects.', - ) - }} - </template> - </gl-form-checkbox> - </div> - - <gl-form-group - data-testid="runner-field-max-timeout" - :label="__('Maximum job timeout')" - :description=" - s__( - 'Runners|Enter the number of seconds. This timeout takes precedence over lower timeouts set for the project.', - ) - " - > - <gl-form-input-group v-model.number="model.maximumTimeout" type="number" /> - </gl-form-group> - - <gl-form-group - data-testid="runner-field-tags" - :label="__('Tags')" - :description=" - __( - 'You can set up jobs to only use runners with specific tags. Separate tags with commas.', - ) - " - > - <gl-form-input-group v-model="model.tagList" /> - </gl-form-group> - - <runner-update-cost-factor-fields v-model="model" /> - </template> + <runner-form-fields v-model="model" :loading="loading" /> + <runner-update-cost-factor-fields v-model="model" :runner-type="runnerType" /> <div class="gl-mt-6"> <gl-button diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js index 4e36a410a66..40841696ead 100644 --- a/app/assets/javascripts/ci/runner/constants.js +++ b/app/assets/javascripts/ci/runner/constants.js @@ -9,7 +9,9 @@ export const RUNNER_DETAILS_PROJECTS_PAGE_SIZE = 5; export const RUNNER_DETAILS_JOBS_PAGE_SIZE = 30; export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.'); -export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}'); +export const I18N_CREATE_ERROR = s__( + 'Runners|An error occurred while creating the runner. Please try again.', +); export const FILTER_CSS_CLASSES = 'gl-bg-gray-10 gl-p-5 gl-border-solid gl-border-gray-100 gl-border-0 gl-border-t-1 gl-border-b-1'; @@ -103,6 +105,26 @@ export const I18N_CREATED_AT_BY_LABEL = s__('Runners|Created %{timeAgo} by %{ava export const I18N_SHOW_ONLY_INHERITED = s__('Runners|Show only inherited'); export const I18N_ADMIN = s__('Runners|Administrator'); +// No runners registered +export const I18N_GET_STARTED = s__('Runners|Get started with runners'); +export const I18N_RUNNERS_ARE_AGENTS = s__( + 'Runners|Runners are the agents that run your CI/CD jobs.', +); +export const I18N_CREATE_RUNNER_LINK = s__( + 'Runners|%{linkStart}Create a new runner%{linkEnd} to get started.', +); +export const I18N_STILL_USING_REGISTRATION_TOKENS = s__('Runners|Still using registration tokens?'); +export const I18N_CONTACT_ADMIN_TO_REGISTER = s__( + 'Runners|To register new runners, contact your administrator.', +); +export const I18N_FOLLOW_REGISTRATION_INSTRUCTIONS = s__( + 'Runners|Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.', +); + +// No runners found +export const I18N_NO_RESULTS = s__('Runners|No results found'); +export const I18N_EDIT_YOUR_SEARCH = s__('Runners|Edit your search and try again'); + // Runner details export const JOBS_ROUTE_PATH = '/jobs'; // vue-router route path @@ -256,3 +278,5 @@ export const SERVICE_COMMANDS_HELP_URL = export const CHANGELOG_URL = 'https://gitlab.com/gitlab-org/gitlab-runner/blob/main/CHANGELOG.md'; export const DOCKER_HELP_URL = 'https://docs.gitlab.com/runner/install/docker.html'; export const KUBERNETES_HELP_URL = 'https://docs.gitlab.com/runner/install/kubernetes.html'; +export const RUNNER_MANAGERS_HELP_URL = + 'https://docs.gitlab.com/runner/fleet_scaling/#workers-executors-and-autoscaling-capabilities'; diff --git a/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql index d18b80511fb..41ec9967d90 100644 --- a/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql +++ b/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql @@ -2,7 +2,7 @@ fragment RunnerFieldsShared on CiRunner { id shortSha runnerType - active + paused accessLevel runUntagged locked diff --git a/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql index 4eebcd01be6..c0b888e758b 100644 --- a/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql +++ b/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql @@ -7,7 +7,7 @@ fragment ListItemShared on CiRunner { shortSha version ipAddress - active + paused locked jobCount tagList @@ -22,6 +22,9 @@ fragment ListItemShared on CiRunner { updateRunner deleteRunner } + managers { + count + } groups(first: 1) { nodes { id diff --git a/app/assets/javascripts/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql b/app/assets/javascripts/ci/runner/graphql/shared/runner_toggle_paused.mutation.graphql index 9b15570dbc0..e862a20750f 100644 --- a/app/assets/javascripts/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql +++ b/app/assets/javascripts/ci/runner/graphql/shared/runner_toggle_paused.mutation.graphql @@ -1,11 +1,11 @@ # Mutation executed for the pause/resume button in the # runner list and details views. -mutation runnerToggleActive($input: RunnerUpdateInput!) { +mutation runnerTogglePaused($input: RunnerUpdateInput!) { runnerUpdate(input: $input) { runner { id - active + paused } errors } diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql index bd53fb29bd0..1a2ad59650e 100644 --- a/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql +++ b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql @@ -2,7 +2,7 @@ fragment RunnerDetailsShared on CiRunner { id shortSha runnerType - active + paused accessLevel runUntagged locked @@ -20,6 +20,9 @@ fragment RunnerDetailsShared on CiRunner { tokenExpiresAt version editAdminUrl + managers { + count + } userPermissions { updateRunner deleteRunner diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_manager.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_manager.fragment.graphql new file mode 100644 index 00000000000..b630786b3d5 --- /dev/null +++ b/app/assets/javascripts/ci/runner/graphql/show/runner_manager.fragment.graphql @@ -0,0 +1,5 @@ +#import "./runner_manager_shared.fragment.graphql" + +fragment CiRunnerManager on CiRunnerManager { + ...CiRunnerManagerShared +} diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql new file mode 100644 index 00000000000..ead005d1252 --- /dev/null +++ b/app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql @@ -0,0 +1,12 @@ +fragment CiRunnerManagerShared on CiRunnerManager { + id + systemId + status + version + revision + executorName + architectureName + platformName + ipAddress + contactedAt +} diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_managers.query.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_managers.query.graphql new file mode 100644 index 00000000000..cc16267e619 --- /dev/null +++ b/app/assets/javascripts/ci/runner/graphql/show/runner_managers.query.graphql @@ -0,0 +1,13 @@ +#import "ee_else_ce/ci/runner/graphql/show/runner_manager.fragment.graphql" + +query getRunnerManagers($runnerId: CiRunnerID!) { + runner(id: $runnerId) { + id + managers { + count + nodes { + ...CiRunnerManager + } + } + } +} diff --git a/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue b/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue index 67d29daf66f..2e1706ddae9 100644 --- a/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue +++ b/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue @@ -1,6 +1,6 @@ <script> import { createAlert, VARIANT_SUCCESS } from '~/alert'; -import { redirectTo, setUrlParams } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated +import { visitUrl, setUrlParams } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue'; @@ -38,7 +38,7 @@ export default { message: s__('Runners|Runner created.'), variant: VARIANT_SUCCESS, }); - redirectTo(ephemeralRegisterUrl); // eslint-disable-line import/no-deprecated + visitUrl(ephemeralRegisterUrl); }, onError(error) { createAlert({ message: error.message }); @@ -66,7 +66,7 @@ export default { <hr aria-hidden="true" /> - <h2 class="gl-font-weight-normal gl-font-lg gl-my-5"> + <h2 class="gl-font-size-h2 gl-my-5"> {{ s__('Runners|Platform') }} </h2> <runner-platforms-radio-group v-model="platform" /> diff --git a/app/assets/javascripts/ci/runner/group_runner_show/group_runner_show_app.vue b/app/assets/javascripts/ci/runner/group_runner_show/group_runner_show_app.vue index 1318bf5a2e6..e885cf45c5a 100644 --- a/app/assets/javascripts/ci/runner/group_runner_show/group_runner_show_app.vue +++ b/app/assets/javascripts/ci/runner/group_runner_show/group_runner_show_app.vue @@ -2,7 +2,7 @@ import { createAlert, VARIANT_SUCCESS } from '~/alert'; import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; -import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated +import { visitUrl } from '~/lib/utils/url_utility'; import RunnerDeleteButton from '../components/runner_delete_button.vue'; import RunnerEditButton from '../components/runner_edit_button.vue'; @@ -76,7 +76,7 @@ export default { }, onDeleted({ message }) { saveAlertToLocalStorage({ message, variant: VARIANT_SUCCESS }); - redirectTo(this.runnersPath); // eslint-disable-line import/no-deprecated + visitUrl(this.runnersPath); }, }, }; diff --git a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue index f0ae54c0232..51f5a9ce8d9 100644 --- a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue +++ b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue @@ -1,6 +1,6 @@ <script> import { createAlert, VARIANT_SUCCESS } from '~/alert'; -import { redirectTo, setUrlParams } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated +import { visitUrl, setUrlParams } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue'; @@ -38,7 +38,7 @@ export default { message: s__('Runners|Runner created.'), variant: VARIANT_SUCCESS, }); - redirectTo(ephemeralRegisterUrl); // eslint-disable-line import/no-deprecated + visitUrl(ephemeralRegisterUrl); }, onError(error) { createAlert({ message: error.message }); @@ -66,7 +66,7 @@ export default { <hr aria-hidden="true" /> - <h2 class="gl-font-weight-normal gl-font-lg gl-my-5"> + <h2 class="gl-font-size-h2 gl-my-5"> {{ s__('Runners|Platform') }} </h2> <runner-platforms-radio-group v-model="platform" /> diff --git a/app/assets/javascripts/ci/runner/runner_update_form_utils.js b/app/assets/javascripts/ci/runner/runner_update_form_utils.js index 3b519fa7d71..6f6c9f64af0 100644 --- a/app/assets/javascripts/ci/runner/runner_update_form_utils.js +++ b/app/assets/javascripts/ci/runner/runner_update_form_utils.js @@ -4,7 +4,7 @@ export const runnerToModel = (runner) => { description, maximumTimeout, accessLevel, - active, + paused, locked, runUntagged, tagList = [], @@ -15,7 +15,7 @@ export const runnerToModel = (runner) => { description, maximumTimeout, accessLevel, - active, + paused, locked, runUntagged, tagList: tagList.join(', '), diff --git a/app/assets/javascripts/ci/runner/utils.js b/app/assets/javascripts/ci/runner/utils.js index 1ca0a9e86b5..bb1ffca62ee 100644 --- a/app/assets/javascripts/ci/runner/utils.js +++ b/app/assets/javascripts/ci/runner/utils.js @@ -1,4 +1,5 @@ import { formatNumber } from '~/locale'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { RUNNER_JOB_COUNT_LIMIT } from './constants'; /** @@ -81,3 +82,13 @@ export const getPaginationVariables = (pagination, pageSize = 10) => { export const parseInterval = (interval) => { return typeof interval === 'string' ? parseInt(interval, 10) : null; }; + +/** + * Creates formatted runner name + * + * @param {Object} runner - Runner object + * @returns Formatted name + */ +export const formatRunnerName = ({ id, shortSha }) => { + return `#${getIdFromGraphQLId(id)} (${shortSha})`; +}; |