Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/ci')
-rw-r--r--app/assets/javascripts/ci/artifacts/components/artifact_row.vue5
-rw-r--r--app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue64
-rw-r--r--app/assets/javascripts/ci/artifacts/components/job_checkbox.vue4
-rw-r--r--app/assets/javascripts/ci/artifacts/constants.js2
-rw-r--r--app/assets/javascripts/ci/artifacts/utils.js7
-rw-r--r--app/assets/javascripts/ci/ci_lint/components/ci_lint.vue2
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue34
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue163
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/constants.js18
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/index.js1
-rw-r--r--app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue110
-rw-r--r--app/assets/javascripts/ci/inherited_ci_variables/graphql/queries/inherited_ci_variables.query.graphql24
-rw-r--r--app/assets/javascripts/ci/inherited_ci_variables/index.js36
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/drawer/pipeline_editor_drawer.vue13
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue32
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue17
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item.vue19
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue21
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item.vue14
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue23
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js23
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue14
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue12
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/constants.js6
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/resolvers.js52
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/index.js141
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/options.js142
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue84
-rw-r--r--app/assets/javascripts/ci/pipeline_new/components/refs_dropdown.vue8
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue2
-rw-r--r--app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue6
-rw-r--r--app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue4
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue11
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue9
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/platforms_drawer.vue6
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_create_form.vue41
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_delete_button.vue4
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_delete_modal.vue49
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_details.vue44
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_form_fields.vue214
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_header.vue38
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_jobs_empty_state.vue8
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue79
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_managers_badge.vue47
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_managers_detail.vue111
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_managers_table.vue75
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_name.vue12
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_pause_button.vue16
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_status_badge.vue20
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_update_form.vue143
-rw-r--r--app/assets/javascripts/ci/runner/constants.js26
-rw-r--r--app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql2
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql5
-rw-r--r--app/assets/javascripts/ci/runner/graphql/shared/runner_toggle_paused.mutation.graphql (renamed from app/assets/javascripts/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql)4
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql5
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_manager.fragment.graphql5
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql12
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_managers.query.graphql13
-rw-r--r--app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue6
-rw-r--r--app/assets/javascripts/ci/runner/group_runner_show/group_runner_show_app.vue4
-rw-r--r--app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue6
-rw-r--r--app/assets/javascripts/ci/runner/runner_update_form_utils.js4
-rw-r--r--app/assets/javascripts/ci/runner/utils.js11
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})`;
+};