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:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-07-19 17:16:28 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-07-19 17:16:28 +0300
commite4384360a16dd9a19d4d2d25d0ef1f2b862ed2a6 (patch)
tree2fcdfa7dcdb9db8f5208b2562f4b4e803d671243 /app/assets/javascripts/ci
parentffda4e7bcac36987f936b4ba515995a6698698f0 (diff)
Add latest changes from gitlab-org/gitlab@16-2-stable-eev16.2.0-rc42
Diffstat (limited to 'app/assets/javascripts/ci')
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue39
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_group_variables.vue16
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue7
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue5
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue25
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/queries/group_environments.query.graphql10
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/first_pipeline_card.vue3
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item.vue9
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue25
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue15
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue309
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue15
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/constants.js5
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/create_pipeline_schedule.mutation.graphql6
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/update_pipeline_schedule.mutation.graphql6
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql20
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_app.js4
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js9
-rw-r--r--app/assets/javascripts/ci/reports/components/grouped_issues_list.vue106
-rw-r--r--app/assets/javascripts/ci/reports/components/summary_row.vue93
-rw-r--r--app/assets/javascripts/ci/reports/constants.js5
-rw-r--r--app/assets/javascripts/ci/reports/sast/constants.js44
-rw-r--r--app/assets/javascripts/ci/reports/utils.js20
-rw-r--r--app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue16
-rw-r--r--app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue8
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue18
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue106
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/registration_token.vue1
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/registration_token_reset_dropdown_item.vue34
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_delete_action.vue126
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_delete_button.vue126
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_delete_disclosure_dropdown_item.vue38
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_delete_modal.vue3
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_detail.vue4
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_edit_button.vue10
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_edit_disclosure_dropdown_item.vue29
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_header.vue46
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_header_actions.vue80
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue54
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_pause_action.vue89
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_pause_button.vue97
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_pause_disclosure_dropdown_item.vue34
-rw-r--r--app/assets/javascripts/ci/runner/constants.js21
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql6
-rw-r--r--app/assets/javascripts/ci/runner/group_runner_show/group_runner_show_app.vue16
-rw-r--r--app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue12
47 files changed, 1039 insertions, 733 deletions
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue
index 09b02068388..a25f871ac92 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue
@@ -24,6 +24,10 @@ export default {
type: Array,
required: true,
},
+ hasEnvScopeQuery: {
+ type: Boolean,
+ required: true,
+ },
selectedEnvironmentScope: {
type: String,
required: false,
@@ -32,6 +36,7 @@ export default {
},
data() {
return {
+ isDropdownShown: false,
selectedEnvironment: '',
searchTerm: '',
};
@@ -46,17 +51,20 @@ export default {
return environment.toLowerCase().includes(lowerCasedSearchTerm);
});
},
- isEnvScopeLimited() {
- return this.glFeatures?.ciLimitEnvironmentScope;
+ isDropdownLoading() {
+ return this.areEnvironmentsLoading && this.hasEnvScopeQuery && !this.isDropdownShown;
+ },
+ isDropdownSearching() {
+ return this.areEnvironmentsLoading && this.hasEnvScopeQuery && this.isDropdownShown;
},
searchedEnvironments() {
- // If FF is enabled, search query will be fired so this component will already
- // receive filtered environments during the refetch.
- // If FF is disabled, search the existing list of environments in the frontend
- let filtered = this.isEnvScopeLimited ? this.environments : this.filteredEnvironments;
+ // If hasEnvScopeQuery (applies only to projects for now), search query will be fired so this
+ // component will already receive filtered environments during the refetch.
+ // Otherwise (applies to groups), search the existing list of environments in the frontend
+ let filtered = this.hasEnvScopeQuery ? this.environments : this.filteredEnvironments;
// If there is no search term, make sure to include *
- if (this.isEnvScopeLimited && !this.searchTerm) {
+ if (this.hasEnvScopeQuery && !this.searchTerm) {
filtered = uniq([...filtered, '*']);
}
@@ -65,15 +73,12 @@ export default {
text: environment,
}));
},
- shouldShowSearchLoading() {
- return this.areEnvironmentsLoading && this.isEnvScopeLimited;
- },
shouldRenderCreateButton() {
return this.searchTerm && !this.environments.includes(this.searchTerm);
},
shouldRenderDivider() {
return (
- (this.isEnvScopeLimited || this.shouldRenderCreateButton) && !this.shouldShowSearchLoading
+ (this.hasEnvScopeQuery || this.shouldRenderCreateButton) && !this.areEnvironmentsLoading
);
},
environmentScopeLabel() {
@@ -84,7 +89,7 @@ export default {
debouncedSearch: debounce(function debouncedSearch(searchTerm) {
const newSearchTerm = searchTerm.trim();
this.searchTerm = newSearchTerm;
- if (this.isEnvScopeLimited) {
+ if (this.hasEnvScopeQuery) {
this.$emit('search-environment-scope', newSearchTerm);
}
}, 500),
@@ -96,6 +101,9 @@ export default {
this.$emit('create-environment-scope', this.searchTerm);
this.selectEnvironment(this.searchTerm);
},
+ toggleDropdownShown(isShown) {
+ this.isDropdownShown = isShown;
+ },
},
ENVIRONMENT_QUERY_LIMIT,
i18n: {
@@ -111,14 +119,17 @@ export default {
block
searchable
:items="searchedEnvironments"
- :searching="shouldShowSearchLoading"
+ :loading="isDropdownLoading"
+ :searching="isDropdownSearching"
:toggle-text="environmentScopeLabel"
@search="debouncedSearch"
@select="selectEnvironment"
+ @shown="toggleDropdownShown(true)"
+ @hidden="toggleDropdownShown(false)"
>
<template #footer>
<gl-dropdown-divider v-if="shouldRenderDivider" />
- <div v-if="isEnvScopeLimited" data-testid="max-envs-notice">
+ <div v-if="hasEnvScopeQuery" data-testid="max-envs-notice">
<gl-dropdown-item class="gl-list-style-none" disabled>
<gl-sprintf :message="$options.i18n.maxEnvsNote" class="gl-font-sm">
<template #limit>
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_group_variables.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_group_variables.vue
index 9c79adffdae..2045b127a82 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_group_variables.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_group_variables.vue
@@ -3,6 +3,7 @@ import { TYPENAME_GROUP } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ADD_MUTATION_ACTION, DELETE_MUTATION_ACTION, UPDATE_MUTATION_ACTION } from '../constants';
+import getGroupEnvironments from '../graphql/queries/group_environments.query.graphql';
import getGroupVariables from '../graphql/queries/group_variables.query.graphql';
import addGroupVariable from '../graphql/mutations/group_add_variable.mutation.graphql';
import deleteGroupVariable from '../graphql/mutations/group_delete_variable.mutation.graphql';
@@ -22,6 +23,15 @@ export default {
graphqlId() {
return convertToGraphQLId(TYPENAME_GROUP, this.groupId);
},
+ queriesAvailable() {
+ if (this.glFeatures.ciGroupEnvScopeGraphql) {
+ return this.$options.queryData;
+ }
+
+ return {
+ ciVariables: this.$options.queryData.ciVariables,
+ };
+ },
},
mutationData: {
[ADD_MUTATION_ACTION]: addGroupVariable,
@@ -33,6 +43,10 @@ export default {
lookup: (data) => data?.group?.ciVariables,
query: getGroupVariables,
},
+ environments: {
+ lookup: (data) => data?.group?.environmentScopes,
+ query: getGroupEnvironments,
+ },
},
};
</script>
@@ -45,6 +59,6 @@ export default {
entity="group"
:full-path="groupPath"
:mutation-data="$options.mutationData"
- :query-data="$options.queryData"
+ :query-data="queriesAvailable"
/>
</template>
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 41514d2d2f1..3af48635f3f 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
@@ -93,6 +93,10 @@ export default {
required: false,
default: false,
},
+ hasEnvScopeQuery: {
+ type: Boolean,
+ required: true,
+ },
mode: {
type: String,
required: true,
@@ -147,7 +151,7 @@ export default {
return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
},
environmentsList() {
- if (this.glFeatures?.ciLimitEnvironmentScope) {
+ if (this.hasEnvScopeQuery) {
return this.environments;
}
@@ -385,6 +389,7 @@ export default {
<ci-environments-dropdown
v-if="areScopedVariablesAvailable"
:are-environments-loading="areEnvironmentsLoading"
+ :has-env-scope-query="hasEnvScopeQuery"
:selected-environment-scope="variable.environmentScope"
:environments="environmentsList"
@select-environment="setEnvironmentScope"
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
index 26e20c690bc..b8a95f9081a 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
@@ -33,6 +33,10 @@ export default {
required: false,
default: false,
},
+ hasEnvScopeQuery: {
+ type: Boolean,
+ required: true,
+ },
isLoading: {
type: Boolean,
required: false,
@@ -107,6 +111,7 @@ export default {
:are-environments-loading="areEnvironmentsLoading"
:are-scoped-variables-available="areScopedVariablesAvailable"
:environments="environments"
+ :has-env-scope-query="hasEnvScopeQuery"
:hide-environment-scope="hideEnvironmentScope"
:variables="variables"
:mode="mode"
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
index ee2c0a771cf..9786f25ed87 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
@@ -159,12 +159,13 @@ export default {
return this.queryData?.environments?.query || {};
},
skip() {
- return !this.queryData?.environments?.query;
+ return !this.hasEnvScopeQuery;
},
variables() {
return {
+ first: ENVIRONMENT_QUERY_LIMIT,
fullPath: this.fullPath,
- ...this.environmentQueryVariables,
+ search: '',
};
},
update(data) {
@@ -179,23 +180,12 @@ export default {
areEnvironmentsLoading() {
return this.$apollo.queries.environments.loading;
},
- environmentQueryVariables() {
- if (this.glFeatures?.ciLimitEnvironmentScope) {
- return {
- first: ENVIRONMENT_QUERY_LIMIT,
- search: '',
- };
- }
-
- return {};
+ hasEnvScopeQuery() {
+ return Boolean(this.queryData?.environments?.query);
},
isLoading() {
- // TODO: Remove areEnvironmentsLoading and show loading icon in dropdown when
- // environment query is loading and FF is enabled
- // https://gitlab.com/gitlab-org/gitlab/-/issues/396990
return (
(this.$apollo.queries.ciVariables.loading && this.isInitialLoading) ||
- this.areEnvironmentsLoading ||
this.isLoadingMoreItems
);
},
@@ -248,9 +238,7 @@ export default {
this.variableMutation(UPDATE_MUTATION_ACTION, variable);
},
async searchEnvironmentScope(searchTerm) {
- if (this.glFeatures?.ciLimitEnvironmentScope) {
- this.$apollo.queries.environments.refetch({ search: searchTerm });
- }
+ this.$apollo.queries.environments.refetch({ search: searchTerm });
},
async variableMutation(mutationAction, variable) {
try {
@@ -296,6 +284,7 @@ export default {
:are-scoped-variables-available="areScopedVariablesAvailable"
:entity="entity"
:environments="environments"
+ :has-env-scope-query="hasEnvScopeQuery"
:hide-environment-scope="hideEnvironmentScope"
:is-loading="isLoading"
:max-variable-limit="maxVariableLimit"
diff --git a/app/assets/javascripts/ci/ci_variable_list/graphql/queries/group_environments.query.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/queries/group_environments.query.graphql
new file mode 100644
index 00000000000..5768d370474
--- /dev/null
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/queries/group_environments.query.graphql
@@ -0,0 +1,10 @@
+query getGroupEnvironments($fullPath: ID!, $first: Int, $search: String) {
+ group(fullPath: $fullPath) {
+ id
+ environmentScopes(first: $first, search: $search) {
+ nodes {
+ name
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/first_pipeline_card.vue b/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
index 0b57433e894..8d670cb5389 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
@@ -2,6 +2,7 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
+import { DOCS_URL } from 'jh_else_ce/lib/utils/url_utility';
import { pipelineEditorTrackingOptions } from '../../../constants';
export default {
@@ -34,7 +35,7 @@ export default {
this.track(actions.helpDrawerLinks.runners, { label });
},
},
- RUNNER_HELP_URL: 'https://docs.gitlab.com/runner/register/index.html',
+ RUNNER_HELP_URL: `${DOCS_URL}/runner/register/index.html`,
};
</script>
<template>
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 794763e0cd8..76db9613dc1 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
@@ -26,7 +26,7 @@ export default {
return [
{
key: 'artifacts.paths',
- title: i18n.ARTIFACTS_AND_CACHE,
+ title: i18n.ARTIFACTS_PATHS,
paths: this.job.artifacts.paths,
generateInputDataTestId: (index) => `artifacts-paths-input-${index}`,
generateDeleteButtonDataTestId: (index) => `delete-artifacts-paths-button-${index}`,
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 d0f206e767f..460f508ee74 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
@@ -54,6 +54,13 @@ export default {
`${this.startInNumber} ${this.startInUnit}${plural}`,
);
},
+ updateWhen(when) {
+ this.$emit('update-job', 'rules[0].when', when);
+
+ if (when === JOB_RULES_WHEN.delayed.value) {
+ this.updateStartIn();
+ }
+ },
},
};
</script>
@@ -73,7 +80,7 @@ export default {
:options="$options.whenOptions"
data-testid="rules-when-select"
:value="job.rules[0].when"
- @input="$emit('update-job', 'rules[0].when', $event)"
+ @input="updateWhen"
/>
</gl-form-group>
<gl-form-group
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
index 6695c6179cf..0700d9e5439 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
@@ -16,6 +16,7 @@ import deletePipelineScheduleMutation from '../graphql/mutations/delete_pipeline
import playPipelineScheduleMutation from '../graphql/mutations/play_pipeline_schedule.mutation.graphql';
import takeOwnershipMutation from '../graphql/mutations/take_ownership.mutation.graphql';
import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql';
+import { ALL_SCOPE } from '../constants';
import PipelineSchedulesTable from './table/pipeline_schedules_table.vue';
import TakeOwnershipModal from './take_ownership_modal.vue';
import DeletePipelineScheduleModal from './delete_pipeline_schedule_modal.vue';
@@ -58,6 +59,9 @@ export default {
pipelinesPath: {
default: '',
},
+ newSchedulePath: {
+ default: '',
+ },
},
apollo: {
schedules: {
@@ -65,7 +69,9 @@ export default {
variables() {
return {
projectPath: this.fullPath,
- status: this.scope,
+ // we need to ensure we send null to the API when
+ // the scope is 'ALL'
+ status: this.scope === ALL_SCOPE ? null : this.scope,
};
},
update(data) {
@@ -111,7 +117,7 @@ export default {
{
text: s__('PipelineSchedules|All'),
count: limitedCounterWithDelimiter(this.count),
- scope: null,
+ scope: ALL_SCOPE,
showBadge: true,
attrs: { 'data-testid': 'pipeline-schedules-all-tab' },
},
@@ -134,7 +140,7 @@ export default {
// this watcher ensures that the count on the all tab
// is not updated when switching to other tabs
schedulesCount(newCount) {
- if (!this.scope) {
+ if (!this.scope || this.scope === ALL_SCOPE) {
this.count = newCount;
}
},
@@ -253,10 +259,10 @@ export default {
</gl-alert>
<gl-tabs
- v-if="isLoading || count > 0"
+ v-if="isLoading || schedulesCount > 0"
sync-active-tab-with-query-params
query-param-name="scope"
- nav-class="gl-flex-grow-1 gl-align-items-center"
+ nav-class="gl-flex-grow-1 gl-align-items-center gl-mt-2"
>
<gl-tab
v-for="tab in tabs"
@@ -289,13 +295,18 @@ export default {
</gl-tab>
<template #tabs-end>
- <gl-button variant="confirm" class="gl-ml-auto" data-testid="new-schedule-button">
+ <gl-button
+ :href="newSchedulePath"
+ variant="confirm"
+ class="gl-ml-auto"
+ data-testid="new-schedule-button"
+ >
{{ $options.i18n.newSchedule }}
</gl-button>
</template>
</gl-tabs>
- <pipeline-schedule-empty-state v-else-if="!isLoading && count === 0" />
+ <pipeline-schedule-empty-state v-else-if="!isLoading && schedulesCount === 0" />
<take-ownership-modal
:visible="showTakeOwnershipModal"
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 39ac55bb9c5..fbdb60f61f1 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?raw';
+import SCHEDULE_MD_SVG_URL from '@gitlab/svgs/dist/illustrations/schedule-md.svg?url';
import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
@@ -20,15 +20,18 @@ export default {
],
createNew: s__('PipelineSchedules|Create a new pipeline schedule'),
},
+ SCHEDULE_MD_SVG_URL,
components: {
GlEmptyState,
GlLink,
GlSprintf,
},
- computed: {
- scheduleSvgPath() {
- return `data:image/svg+xml;utf8,${encodeURIComponent(scheduleSvg)}`;
+ inject: {
+ newSchedulePath: {
+ default: '',
},
+ },
+ computed: {
schedulesHelpPath() {
return helpPagePath('ci/pipelines/schedules');
},
@@ -37,9 +40,9 @@ export default {
</script>
<template>
<gl-empty-state
- :svg-path="scheduleSvgPath"
+ :svg-path="$options.SCHEDULE_MD_SVG_URL"
:primary-button-text="$options.i18n.createNew"
- primary-button-link="#"
+ :primary-button-link="newSchedulePath"
>
<template #title>
<h3>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
index 367b1812a27..d84a9a4a4b5 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
@@ -8,18 +8,22 @@ import {
GlFormGroup,
GlFormInput,
GlFormTextarea,
- GlLink,
- GlSprintf,
+ GlLoadingIcon,
} from '@gitlab/ui';
-import { uniqueId } from 'lodash';
-import Vue from 'vue';
import { __, s__ } from '~/locale';
+import { createAlert } from '~/alert';
+import { visitUrl, queryToObject } from '~/lib/utils/url_utility';
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
import RefSelector from '~/ref/components/ref_selector.vue';
import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
+import createPipelineScheduleMutation from '../graphql/mutations/create_pipeline_schedule.mutation.graphql';
+import updatePipelineScheduleMutation from '../graphql/mutations/update_pipeline_schedule.mutation.graphql';
+import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql';
import { VARIABLE_TYPE, FILE_TYPE } from '../constants';
+const scheduleId = queryToObject(window.location.search).id;
+
export default {
components: {
GlButton,
@@ -30,21 +34,12 @@ export default {
GlFormGroup,
GlFormInput,
GlFormTextarea,
- GlLink,
- GlSprintf,
+ GlLoadingIcon,
RefSelector,
TimezoneDropdown,
IntervalPatternInput,
},
- inject: [
- 'fullPath',
- 'projectId',
- 'defaultBranch',
- 'cron',
- 'cronTimezone',
- 'dailyLimit',
- 'settingsLink',
- ],
+ inject: ['fullPath', 'projectId', 'defaultBranch', 'dailyLimit', 'settingsLink', 'schedulesPath'],
props: {
timezoneData: {
type: Array,
@@ -55,34 +50,79 @@ export default {
required: false,
default: '',
},
+ editing: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ apollo: {
+ schedule: {
+ query: getPipelineSchedulesQuery,
+ variables() {
+ return {
+ projectPath: this.fullPath,
+ ids: scheduleId,
+ };
+ },
+ update(data) {
+ return data.project?.pipelineSchedules?.nodes[0] || {};
+ },
+ result({ data }) {
+ if (data) {
+ const {
+ project: {
+ pipelineSchedules: { nodes },
+ },
+ } = data;
+
+ const schedule = nodes[0];
+ const variables = schedule.variables?.nodes || [];
+
+ this.description = schedule.description;
+ this.cron = schedule.cron;
+ this.cronTimezone = schedule.cronTimezone;
+ this.scheduleRef = schedule.ref;
+ this.variables = variables.map((variable) => {
+ return {
+ id: variable.id,
+ variableType: variable.variableType,
+ key: variable.key,
+ value: variable.value,
+ destroy: false,
+ };
+ });
+ this.addEmptyVariable();
+ this.activated = schedule.active;
+ }
+ },
+ skip() {
+ return !this.editing;
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.scheduleFetchError });
+ },
+ },
},
data() {
return {
- refValue: {
- shortName: this.refParam,
- // this is needed until we add support for ref type in url query strings
- // ensure default branch is called with full ref on load
- // https://gitlab.com/gitlab-org/gitlab/-/issues/287815
- fullName: this.refParam === this.defaultBranch ? `refs/heads/${this.refParam}` : undefined,
- },
+ cron: '',
description: '',
scheduleRef: this.defaultBranch,
activated: true,
- timezone: this.cronTimezone,
- formCiVariables: {},
- // TODO: Add the GraphQL query to help populate the predefined variables
- // app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue#131
- predefinedValueOptions: {},
+ cronTimezone: '',
+ variables: [],
+ schedule: {},
};
},
i18n: {
activated: __('Activated'),
- cronTimezone: s__('PipelineSchedules|Cron timezone'),
+ cronTimezoneText: s__('PipelineSchedules|Cron timezone'),
description: s__('PipelineSchedules|Description'),
shortDescriptionPipeline: s__(
'PipelineSchedules|Provide a short description for this pipeline',
),
- savePipelineSchedule: s__('PipelineSchedules|Save pipeline schedule'),
+ editScheduleBtnText: s__('PipelineSchedules|Edit pipeline schedule'),
+ createScheduleBtnText: s__('PipelineSchedules|Create pipeline schedule'),
cancel: __('Cancel'),
targetBranchTag: __('Select target branch or tag'),
intervalPattern: s__('PipelineSchedules|Interval Pattern'),
@@ -91,6 +131,15 @@ export default {
),
removeVariableLabel: s__('CiVariables|Remove variable'),
variables: s__('Pipeline|Variables'),
+ scheduleCreateError: s__(
+ 'PipelineSchedules|An error occurred while creating the pipeline schedule.',
+ ),
+ scheduleUpdateError: s__(
+ 'PipelineSchedules|An error occurred while updating the pipeline schedule.',
+ ),
+ scheduleFetchError: s__(
+ 'PipelineSchedules|An error occurred while trying to fetch the pipeline schedule.',
+ ),
},
typeOptions: {
[VARIABLE_TYPE]: __('Variable'),
@@ -103,15 +152,6 @@ export default {
dropdownHeader: this.$options.i18n.targetBranchTag,
};
},
- refFullName() {
- return this.refValue.fullName;
- },
- variables() {
- return this.formCiVariables[this.refFullName]?.variables ?? [];
- },
- descriptions() {
- return this.formCiVariables[this.refFullName]?.descriptions ?? {};
- },
typeOptionsListbox() {
return [
{
@@ -127,52 +167,136 @@ export default {
getEnabledRefTypes() {
return [REF_TYPE_BRANCHES, REF_TYPE_TAGS];
},
+ preparedVariablesUpdate() {
+ return this.variables.filter((variable) => variable.key !== '');
+ },
+ preparedVariablesCreate() {
+ return this.preparedVariablesUpdate.map((variable) => {
+ return {
+ key: variable.key,
+ value: variable.value,
+ variableType: variable.variableType,
+ };
+ });
+ },
+ loading() {
+ return this.$apollo.queries.schedule.loading;
+ },
+ buttonText() {
+ return this.editing
+ ? this.$options.i18n.editScheduleBtnText
+ : this.$options.i18n.createScheduleBtnText;
+ },
},
created() {
- Vue.set(this.formCiVariables, this.refFullName, {
- variables: [],
- descriptions: {},
- });
-
- this.addEmptyVariable(this.refFullName);
+ this.addEmptyVariable();
},
methods: {
- addEmptyVariable(refValue) {
- const { variables } = this.formCiVariables[refValue];
+ addEmptyVariable() {
+ const lastVar = this.variables[this.variables.length - 1];
- const lastVar = variables[variables.length - 1];
if (lastVar?.key === '' && lastVar?.value === '') {
return;
}
- variables.push({
- uniqueId: uniqueId(`var-${refValue}`),
- variable_type: VARIABLE_TYPE,
+ this.variables.push({
+ variableType: VARIABLE_TYPE,
key: '',
value: '',
+ destroy: false,
});
},
setVariableAttribute(key, attribute, value) {
- const { variables } = this.formCiVariables[this.refFullName];
- const variable = variables.find((v) => v.key === key);
+ const variable = this.variables.find((v) => v.key === key);
variable[attribute] = value;
},
- shouldShowValuesDropdown(key) {
- return this.predefinedValueOptions[key]?.length > 1;
- },
removeVariable(index) {
- this.variables.splice(index, 1);
+ this.variables[index].destroy = true;
},
canRemove(index) {
return index < this.variables.length - 1;
},
+ async createPipelineSchedule() {
+ try {
+ const {
+ data: {
+ pipelineScheduleCreate: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: createPipelineScheduleMutation,
+ variables: {
+ input: {
+ description: this.description,
+ cron: this.cron,
+ cronTimezone: this.cronTimezone,
+ ref: this.scheduleRef,
+ variables: this.preparedVariablesCreate,
+ active: this.activated,
+ projectPath: this.fullPath,
+ },
+ },
+ });
+
+ if (errors.length > 0) {
+ createAlert({ message: errors[0] });
+ } else {
+ visitUrl(this.schedulesPath);
+ }
+ } catch {
+ createAlert({ message: this.$options.i18n.scheduleCreateError });
+ }
+ },
+ async updatePipelineSchedule() {
+ try {
+ const {
+ data: {
+ pipelineScheduleUpdate: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: updatePipelineScheduleMutation,
+ variables: {
+ input: {
+ id: this.schedule.id,
+ description: this.description,
+ cron: this.cron,
+ cronTimezone: this.cronTimezone,
+ ref: this.scheduleRef,
+ variables: this.preparedVariablesUpdate,
+ active: this.activated,
+ },
+ },
+ });
+
+ if (errors.length > 0) {
+ createAlert({ message: errors[0] });
+ } else {
+ visitUrl(this.schedulesPath);
+ }
+ } catch {
+ createAlert({ message: this.$options.i18n.scheduleUpdateError });
+ }
+ },
+ scheduleHandler() {
+ if (this.editing) {
+ this.updatePipelineSchedule();
+ } else {
+ this.createPipelineSchedule();
+ }
+ },
+ setCronValue(cron) {
+ this.cron = cron;
+ },
+ setTimezone(timezone) {
+ this.cronTimezone = timezone.identifier || '';
+ },
},
};
</script>
<template>
- <div class="col-lg-8">
- <gl-form>
+ <div class="col-lg-8 gl-pl-0">
+ <gl-loading-icon v-if="loading && editing" size="lg" />
+ <gl-form v-else>
<!--Description-->
<gl-form-group :label="$options.i18n.description" label-for="schedule-description">
<gl-form-input
@@ -181,6 +305,7 @@ export default {
type="text"
:placeholder="$options.i18n.shortDescriptionPipeline"
data-testid="schedule-description"
+ required
/>
</gl-form-group>
<!--Interval Pattern-->
@@ -190,21 +315,24 @@ export default {
:initial-cron-interval="cron"
:daily-limit="dailyLimit"
:send-native-errors="false"
+ @cronValue="setCronValue"
/>
</gl-form-group>
<!--Timezone-->
- <gl-form-group :label="$options.i18n.cronTimezone" label-for="schedule-timezone">
+ <gl-form-group :label="$options.i18n.cronTimezoneText" label-for="schedule-timezone">
<timezone-dropdown
id="schedule-timezone"
- :value="timezone"
+ :value="cronTimezone"
:timezone-data="timezoneData"
name="schedule-timezone"
+ @input="setTimezone"
/>
</gl-form-group>
<!--Branch/Tag Selector-->
<gl-form-group :label="$options.i18n.targetBranchTag" label-for="schedule-target-branch-tag">
<ref-selector
id="schedule-target-branch-tag"
+ v-model="scheduleRef"
:enabled-ref-types="getEnabledRefTypes"
:project-id="projectId"
:value="scheduleRef"
@@ -217,23 +345,23 @@ export default {
<gl-form-group :label="$options.i18n.variables">
<div
v-for="(variable, index) in variables"
- :key="variable.uniqueId"
- class="gl-mb-3 gl-pb-2"
- data-testid="ci-variable-row"
+ :key="`var-${index}`"
data-qa-selector="ci_variable_row_container"
>
<div
- class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row"
+ v-if="!variable.destroy"
+ class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row gl-mb-3 gl-pb-2"
+ data-testid="ci-variable-row"
>
<gl-dropdown
- :text="$options.typeOptions[variable.variable_type]"
+ :text="$options.typeOptions[variable.variableType]"
:class="$options.formElementClasses"
data-testid="pipeline-form-ci-variable-type"
>
<gl-dropdown-item
v-for="type in Object.keys($options.typeOptions)"
:key="type"
- @click="setVariableAttribute(variable.key, 'variable_type', type)"
+ @click="setVariableAttribute(variable.key, 'variableType', type)"
>
{{ $options.typeOptions[type] }}
</gl-dropdown-item>
@@ -244,26 +372,10 @@ export default {
:class="$options.formElementClasses"
data-testid="pipeline-form-ci-variable-key"
data-qa-selector="ci_variable_key_field"
- @change="addEmptyVariable(refFullName)"
+ @change="addEmptyVariable()"
/>
- <gl-dropdown
- v-if="shouldShowValuesDropdown(variable.key)"
- :text="variable.value"
- :class="$options.formElementClasses"
- class="gl-flex-grow-1 gl-mr-0!"
- data-testid="pipeline-form-ci-variable-value-dropdown"
- >
- <gl-dropdown-item
- v-for="value in predefinedValueOptions[variable.key]"
- :key="value"
- data-testid="pipeline-form-ci-variable-value-dropdown-items"
- @click="setVariableAttribute(variable.key, 'value', value)"
- >
- {{ value }}
- </gl-dropdown-item>
- </gl-dropdown>
+
<gl-form-textarea
- v-else
v-model="variable.value"
:placeholder="s__('CiVariables|Input variable value')"
class="gl-mb-3 gl-h-7!"
@@ -292,30 +404,19 @@ export default {
/>
</template>
</div>
- <div v-if="descriptions[variable.key]" class="gl-text-gray-500 gl-mb-3">
- {{ descriptions[variable.key] }}
- </div>
</div>
-
- <template #description
- ><gl-sprintf :message="$options.i18n.variablesDescription">
- <template #link="{ content }">
- <gl-link :href="settingsLink">{{ content }}</gl-link>
- </template>
- </gl-sprintf></template
- >
</gl-form-group>
<!--Activated-->
- <gl-form-checkbox id="schedule-active" v-model="activated" class="gl-mb-3">{{
- $options.i18n.activated
- }}</gl-form-checkbox>
+ <gl-form-checkbox id="schedule-active" v-model="activated" class="gl-mb-3">
+ {{ $options.i18n.activated }}
+ </gl-form-checkbox>
- <gl-button type="submit" variant="confirm" data-testid="schedule-submit-button">{{
- $options.i18n.savePipelineSchedule
- }}</gl-button>
- <gl-button type="reset" data-testid="schedule-cancel-button">{{
- $options.i18n.cancel
- }}</gl-button>
+ <gl-button variant="confirm" data-testid="schedule-submit-button" @click="scheduleHandler">
+ {{ buttonText }}
+ </gl-button>
+ <gl-button :href="schedulesPath" data-testid="schedule-cancel-button">
+ {{ $options.i18n.cancel }}
+ </gl-button>
</gl-form>
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
index 5bd58bfd95d..a56da06f5da 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
@@ -1,6 +1,7 @@
<script>
import { GlButton, GlButtonGroup, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
import { s__ } from '~/locale';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
export const i18n = {
playTooltip: s__('PipelineSchedules|Run pipeline schedule'),
@@ -44,6 +45,11 @@ export default {
canRemove() {
return this.schedule.userPermissions.adminPipelineSchedule;
},
+ editPathWithIdParam() {
+ const id = getIdFromGraphQLId(this.schedule.id);
+
+ return `${this.schedule.editPath}?id=${id}`;
+ },
},
};
</script>
@@ -67,7 +73,14 @@ export default {
data-testid="take-ownership-pipeline-schedule-btn"
@click="$emit('showTakeOwnershipModal', schedule.id)"
/>
- <gl-button v-if="canUpdate" v-gl-tooltip :title="$options.i18n.editTooltip" icon="pencil" />
+ <gl-button
+ v-if="canUpdate"
+ v-gl-tooltip
+ :href="editPathWithIdParam"
+ :title="$options.i18n.editTooltip"
+ icon="pencil"
+ data-testid="edit-pipeline-schedule-btn"
+ />
<gl-button
v-if="canRemove"
v-gl-tooltip
diff --git a/app/assets/javascripts/ci/pipeline_schedules/constants.js b/app/assets/javascripts/ci/pipeline_schedules/constants.js
index b4ab1143f60..16dab33ce29 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/constants.js
+++ b/app/assets/javascripts/ci/pipeline_schedules/constants.js
@@ -1,2 +1,3 @@
-export const VARIABLE_TYPE = 'env_var';
-export const FILE_TYPE = 'file';
+export const VARIABLE_TYPE = 'ENV_VAR';
+export const FILE_TYPE = 'FILE';
+export const ALL_SCOPE = 'ALL';
diff --git a/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/create_pipeline_schedule.mutation.graphql b/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/create_pipeline_schedule.mutation.graphql
new file mode 100644
index 00000000000..0bea1bb8360
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/create_pipeline_schedule.mutation.graphql
@@ -0,0 +1,6 @@
+mutation createPipelineSchedule($input: PipelineScheduleCreateInput!) {
+ pipelineScheduleCreate(input: $input) {
+ clientMutationId
+ errors
+ }
+}
diff --git a/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/update_pipeline_schedule.mutation.graphql b/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/update_pipeline_schedule.mutation.graphql
new file mode 100644
index 00000000000..a6a937af74a
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/update_pipeline_schedule.mutation.graphql
@@ -0,0 +1,6 @@
+mutation updatePipelineSchedule($input: PipelineScheduleUpdateInput!) {
+ pipelineScheduleUpdate(input: $input) {
+ clientMutationId
+ errors
+ }
+}
diff --git a/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql b/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
index 6167c7dc577..29a26be0344 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
@@ -1,16 +1,24 @@
-query getPipelineSchedulesQuery($projectPath: ID!, $status: PipelineScheduleStatus) {
+query getPipelineSchedulesQuery(
+ $projectPath: ID!
+ $status: PipelineScheduleStatus
+ $ids: [ID!] = null
+) {
currentUser {
id
username
}
project(fullPath: $projectPath) {
id
- pipelineSchedules(status: $status) {
+ pipelineSchedules(status: $status, ids: $ids) {
count
nodes {
id
description
+ cron
+ cronTimezone
+ ref
forTag
+ editPath
refPath
refForDisplay
lastPipeline {
@@ -34,6 +42,14 @@ query getPipelineSchedulesQuery($projectPath: ID!, $status: PipelineScheduleStat
name
webPath
}
+ variables {
+ nodes {
+ id
+ variableType
+ key
+ value
+ }
+ }
userPermissions {
playPipelineSchedule
updatePipelineSchedule
diff --git a/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_app.js b/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_app.js
index 8bca4f85e9f..71db9400909 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_app.js
+++ b/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_app.js
@@ -18,7 +18,7 @@ export default () => {
return false;
}
- const { fullPath, pipelinesPath } = containerEl.dataset;
+ const { fullPath, pipelinesPath, newSchedulePath, schedulesPath } = containerEl.dataset;
return new Vue({
el: containerEl,
@@ -27,6 +27,8 @@ export default () => {
provide: {
fullPath,
pipelinesPath,
+ newSchedulePath,
+ schedulesPath,
},
render(createElement) {
return createElement(PipelineSchedules);
diff --git a/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js b/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js
index 445161f99cb..6bf121d39b6 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js
+++ b/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js
@@ -9,7 +9,7 @@ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
-export default (selector) => {
+export default (selector, editing = false) => {
const containerEl = document.querySelector(selector);
if (!containerEl) {
@@ -18,13 +18,12 @@ export default (selector) => {
const {
fullPath,
- cron,
dailyLimit,
timezoneData,
- cronTimezone,
projectId,
defaultBranch,
settingsLink,
+ schedulesPath,
} = containerEl.dataset;
return new Vue({
@@ -36,15 +35,15 @@ export default (selector) => {
projectId,
defaultBranch,
dailyLimit: dailyLimit ?? '',
- cronTimezone: cronTimezone ?? '',
- cron: cron ?? '',
settingsLink,
+ schedulesPath,
},
render(createElement) {
return createElement(PipelineSchedulesForm, {
props: {
timezoneData: JSON.parse(timezoneData),
refParam: defaultBranch,
+ editing,
},
});
},
diff --git a/app/assets/javascripts/ci/reports/components/grouped_issues_list.vue b/app/assets/javascripts/ci/reports/components/grouped_issues_list.vue
deleted file mode 100644
index b21a486e259..00000000000
--- a/app/assets/javascripts/ci/reports/components/grouped_issues_list.vue
+++ /dev/null
@@ -1,106 +0,0 @@
-<script>
-import { s__ } from '~/locale';
-import ReportItem from '~/ci/reports/components/report_item.vue';
-import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
-
-export default {
- components: {
- ReportItem,
- SmartVirtualList,
- },
- props: {
- component: {
- type: String,
- required: false,
- default: '',
- },
- nestedLevel: {
- type: Number,
- required: false,
- default: 0,
- validator: (value) => [0, 1, 2].includes(value),
- },
- resolvedIssues: {
- type: Array,
- required: false,
- default: () => [],
- },
- unresolvedIssues: {
- type: Array,
- required: false,
- default: () => [],
- },
- resolvedHeading: {
- type: String,
- required: false,
- default: s__('ciReport|Fixed'),
- },
- unresolvedHeading: {
- type: String,
- required: false,
- default: s__('ciReport|New'),
- },
- },
- groups: ['unresolved', 'resolved'],
- typicalReportItemHeight: 32,
- maxShownReportItems: 20,
- computed: {
- groups() {
- return this.$options.groups
- .map((group) => ({
- name: group,
- issues: this[`${group}Issues`],
- heading: this[`${group}Heading`],
- }))
- .filter(({ issues }) => issues.length > 0);
- },
- listLength() {
- // every group has a header which is rendered as a list item
- const groupsCount = this.groups.length;
- const issuesCount = this.groups.reduce(
- (totalIssues, { issues }) => totalIssues + issues.length,
- 0,
- );
-
- return groupsCount + issuesCount;
- },
- listClasses() {
- return {
- 'gl-pl-9': this.nestedLevel === 1,
- 'gl-pl-11-5': this.nestedLevel === 2,
- };
- },
- },
-};
-</script>
-
-<template>
- <smart-virtual-list
- :length="listLength"
- :remain="$options.maxShownReportItems"
- :size="$options.typicalReportItemHeight"
- :class="listClasses"
- class="report-block-container"
- wtag="ul"
- wclass="report-block-list"
- >
- <template v-for="(group, groupIndex) in groups">
- <h2
- :key="group.name"
- :data-testid="`${group.name}Heading`"
- :class="[groupIndex > 0 ? 'mt-2' : 'mt-0']"
- class="h5 mb-1"
- >
- {{ group.heading }}
- </h2>
- <report-item
- v-for="(issue, issueIndex) in group.issues"
- :key="`${group.name}-${issue.name}-${group.name}-${issueIndex}`"
- :issue="issue"
- :show-report-section-status-icon="false"
- :component="component"
- status="none"
- />
- </template>
- </smart-virtual-list>
-</template>
diff --git a/app/assets/javascripts/ci/reports/components/summary_row.vue b/app/assets/javascripts/ci/reports/components/summary_row.vue
deleted file mode 100644
index ee55368c829..00000000000
--- a/app/assets/javascripts/ci/reports/components/summary_row.vue
+++ /dev/null
@@ -1,93 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import HelpPopover from '~/vue_shared/components/help_popover.vue';
-import { ICON_WARNING } from '../constants';
-
-/**
- * Renders the summary row for each report
- *
- * Used both in MR widget and Pipeline's view for:
- * - Unit tests reports
- * - Security reports
- */
-
-export default {
- name: 'ReportSummaryRow',
- components: {
- CiIcon,
- HelpPopover,
- GlLoadingIcon,
- },
- props: {
- nestedSummary: {
- type: Boolean,
- required: false,
- default: false,
- },
- summary: {
- type: String,
- required: false,
- default: '',
- },
- statusIcon: {
- type: String,
- required: true,
- },
- popoverOptions: {
- type: Object,
- required: false,
- default: null,
- },
- },
- computed: {
- iconStatus() {
- return {
- group: this.statusIcon,
- icon: `status_${this.statusIcon}`,
- };
- },
- rowClasses() {
- if (!this.nestedSummary) {
- return ['gl-px-5'];
- }
- return ['gl-pl-9', 'gl-pr-5', { 'gl-bg-gray-10': this.statusIcon === ICON_WARNING }];
- },
- statusIconSize() {
- if (!this.nestedSummary) {
- return 24;
- }
- return 16;
- },
- },
-};
-</script>
-<template>
- <div
- class="gl-border-t-solid gl-border-t-gray-100 gl-border-t-1 gl-py-3 gl-display-flex gl-align-items-center"
- :class="rowClasses"
- >
- <div class="gl-mr-3">
- <gl-loading-icon
- v-if="statusIcon === 'loading'"
- css-class="report-block-list-loading-icon"
- size="lg"
- />
- <ci-icon v-else :status="iconStatus" :size="statusIconSize" data-testid="summary-row-icon" />
- </div>
- <div class="report-block-list-issue-description">
- <div class="report-block-list-issue-description-text" data-testid="summary-row-description">
- <slot name="summary">{{ summary }}</slot
- ><span v-if="popoverOptions" class="text-nowrap"
- >&nbsp;<help-popover v-if="popoverOptions" :options="popoverOptions" class="align-top" />
- </span>
- </div>
- </div>
- <div
- v-if="$slots.default /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */"
- class="text-right flex-fill d-flex justify-content-end flex-column flex-sm-row"
- >
- <slot></slot>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ci/reports/constants.js b/app/assets/javascripts/ci/reports/constants.js
index 1137236d355..3968f8db752 100644
--- a/app/assets/javascripts/ci/reports/constants.js
+++ b/app/assets/javascripts/ci/reports/constants.js
@@ -7,8 +7,6 @@ export const STATUS_SUCCESS = 'success';
export const STATUS_NEUTRAL = 'neutral';
export const STATUS_NOT_FOUND = 'not_found';
-export const ICON_WARNING = 'warning';
-
export const status = {
LOADING,
ERROR,
@@ -22,3 +20,6 @@ export const status = {
export const SLOT_SUCCESS = 'success';
export const SLOT_LOADING = 'loading';
export const SLOT_ERROR = 'error';
+
+export const CODE_QUALITY_SCALE_KEY = 'codeQuality';
+export const SAST_SCALE_KEY = 'sast';
diff --git a/app/assets/javascripts/ci/reports/sast/constants.js b/app/assets/javascripts/ci/reports/sast/constants.js
new file mode 100644
index 00000000000..3800065917b
--- /dev/null
+++ b/app/assets/javascripts/ci/reports/sast/constants.js
@@ -0,0 +1,44 @@
+export const SEVERITY_CLASSES = {
+ info: 'gl-text-blue-400',
+ low: 'gl-text-orange-300',
+ medium: 'gl-text-orange-400',
+ high: 'gl-text-red-600',
+ critical: 'gl-text-red-800',
+ unknown: 'gl-text-gray-400',
+};
+
+export const SEVERITY_ICONS = {
+ info: 'severity-info',
+ low: 'severity-low',
+ medium: 'severity-medium',
+ high: 'severity-high',
+ critical: 'severity-critical',
+ unknown: 'severity-unknown',
+};
+
+export const SEVERITIES = {
+ info: {
+ class: SEVERITY_CLASSES.info,
+ name: SEVERITY_ICONS.info,
+ },
+ low: {
+ class: SEVERITY_CLASSES.low,
+ name: SEVERITY_ICONS.low,
+ },
+ medium: {
+ class: SEVERITY_CLASSES.medium,
+ name: SEVERITY_ICONS.medium,
+ },
+ high: {
+ class: SEVERITY_CLASSES.high,
+ name: SEVERITY_ICONS.high,
+ },
+ critical: {
+ class: SEVERITY_CLASSES.critical,
+ name: SEVERITY_ICONS.critical,
+ },
+ unknown: {
+ class: SEVERITY_CLASSES.unknown,
+ name: SEVERITY_ICONS.unknown,
+ },
+};
diff --git a/app/assets/javascripts/ci/reports/utils.js b/app/assets/javascripts/ci/reports/utils.js
new file mode 100644
index 00000000000..bb6eddf2cce
--- /dev/null
+++ b/app/assets/javascripts/ci/reports/utils.js
@@ -0,0 +1,20 @@
+import { SEVERITIES as SEVERITIES_CODE_QUALITY } from '~/ci/reports/codequality_report/constants';
+import { SEVERITIES as SEVERITIES_SAST } from '~/ci/reports/sast/constants';
+import { SAST_SCALE_KEY } from './constants';
+
+function mapSeverity(findings) {
+ const severityInfo =
+ findings.scale === SAST_SCALE_KEY ? SEVERITIES_SAST : SEVERITIES_CODE_QUALITY;
+ return {
+ ...findings,
+ class: severityInfo[findings.severity].class,
+ name: severityInfo[findings.severity].name,
+ };
+}
+
+export function getSeverity(findings) {
+ if (Array.isArray(findings)) {
+ return findings.map((finding) => mapSeverity(finding));
+ }
+ return mapSeverity(findings);
+}
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 d385d32fd9d..c2ec8462a0e 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
@@ -4,10 +4,8 @@ import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { visitUrl } from '~/lib/utils/url_utility';
-import RunnerDeleteButton from '../components/runner_delete_button.vue';
-import RunnerEditButton from '../components/runner_edit_button.vue';
-import RunnerPauseButton from '../components/runner_pause_button.vue';
import RunnerHeader from '../components/runner_header.vue';
+import RunnerHeaderActions from '../components/runner_header_actions.vue';
import RunnerDetailsTabs from '../components/runner_details_tabs.vue';
import { I18N_FETCH_ERROR } from '../constants';
@@ -18,10 +16,8 @@ import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_lo
export default {
name: 'AdminRunnerShowApp',
components: {
- RunnerDeleteButton,
- RunnerEditButton,
- RunnerPauseButton,
RunnerHeader,
+ RunnerHeaderActions,
RunnerDetailsTabs,
},
props: {
@@ -80,9 +76,11 @@ export default {
<div>
<runner-header v-if="runner" :runner="runner">
<template #actions>
- <runner-edit-button v-if="canUpdate && runner.editAdminUrl" :href="runner.editAdminUrl" />
- <runner-pause-button v-if="canUpdate" :runner="runner" />
- <runner-delete-button v-if="canDelete" :runner="runner" @deleted="onDeleted" />
+ <runner-header-actions
+ :runner="runner"
+ :edit-path="runner.editAdminUrl"
+ @deleted="onDeleted"
+ />
</template>
</runner-header>
<runner-details-tabs v-if="runner" :runner="runner" />
diff --git a/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
index 4d88feebe53..2168685e703 100644
--- a/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
@@ -126,10 +126,6 @@ export default {
isSearchFiltered() {
return isSearchFiltered(this.search);
},
- shouldShowCreateRunnerWorkflow() {
- // create_runner_workflow_for_admin feature flag
- return this.glFeatures.createRunnerWorkflowForAdmin;
- },
},
watch: {
search: {
@@ -193,14 +189,14 @@ export default {
/>
<div class="gl-w-full gl-md-w-auto gl-display-flex">
- <gl-button v-if="shouldShowCreateRunnerWorkflow" :href="newRunnerPath" variant="confirm">
+ <gl-button :href="newRunnerPath" variant="confirm">
{{ s__('Runners|New instance runner') }}
</gl-button>
<registration-dropdown
class="gl-ml-3"
:registration-token="registrationToken"
:type="$options.INSTANCE_TYPE"
- right
+ placement="right"
/>
</div>
</div>
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 9f4ce14f704..cc31afea88c 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
@@ -1,6 +1,6 @@
<script>
import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
-import { sprintf, __ } from '~/locale';
+import { sprintf, __, formatNumber } from '~/locale';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
@@ -49,6 +49,12 @@ export default {
managersCount() {
return this.runner.managers?.count || 0;
},
+ firstIpAddress() {
+ return this.runner.managers?.nodes?.[0]?.ipAddress || null;
+ },
+ additionalIpAddressCount() {
+ return this.managersCount - 1;
+ },
jobCount() {
return formatJobCount(this.runner.jobCount);
},
@@ -63,6 +69,9 @@ export default {
return null;
},
},
+ methods: {
+ formatNumber,
+ },
i18n: {
I18N_NO_DESCRIPTION,
I18N_LOCKED_RUNNER_DESCRIPTION,
@@ -120,8 +129,11 @@ export default {
</gl-sprintf>
</runner-summary-field>
- <runner-summary-field v-if="runner.ipAddress" icon="disk" :tooltip="__('IP Address')">
- {{ runner.ipAddress }}
+ <runner-summary-field v-if="firstIpAddress" icon="disk" :tooltip="__('IP Address')">
+ {{ firstIpAddress }}
+ <template v-if="additionalIpAddressCount"
+ >(+{{ formatNumber(additionalIpAddressCount) }})</template
+ >
</runner-summary-field>
<runner-summary-field icon="pipeline" data-testid="job-count" :tooltip="__('Jobs')">
diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue b/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue
index 2fdf8456615..0154cd2a3ec 100644
--- a/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue
+++ b/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue
@@ -1,5 +1,11 @@
<script>
-import { GlDropdown, GlDropdownForm, GlDropdownItem, GlDropdownDivider, GlIcon } from '@gitlab/ui';
+import {
+ GlDisclosureDropdown,
+ GlDropdownForm,
+ GlDisclosureDropdownItem,
+ GlDisclosureDropdownGroup,
+ GlIcon,
+} from '@gitlab/ui';
import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
@@ -20,12 +26,15 @@ export default {
showInstallationInstructions: s__(
'Runners|Show runner installation and registration instructions',
),
+ supportForRegistrationTokensDeprecated: s__(
+ 'Runners|Support for registration tokens is deprecated',
+ ),
},
components: {
- GlDropdown,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlDisclosureDropdownGroup,
GlDropdownForm,
- GlDropdownItem,
- GlDropdownDivider,
GlIcon,
RegistrationToken,
RunnerInstructionsModal,
@@ -51,14 +60,6 @@ export default {
};
},
computed: {
- isDeprecated() {
- // Show a compact version when used as secondary option
- // create_runner_workflow_for_admin or create_runner_workflow_for_namespace
- return (
- this.glFeatures?.createRunnerWorkflowForAdmin ||
- this.glFeatures?.createRunnerWorkflowForNamespace
- );
- },
actionText() {
switch (this.type) {
case INSTANCE_TYPE:
@@ -71,30 +72,6 @@ export default {
return I18N_REGISTER_RUNNER;
}
},
- dropdownText() {
- if (this.isDeprecated) {
- return '';
- }
- return this.actionText;
- },
- dropdownToggleClass() {
- if (this.isDeprecated) {
- return ['gl-px-3!'];
- }
- return [];
- },
- dropdownCategory() {
- if (this.isDeprecated) {
- return 'tertiary';
- }
- return 'primary';
- },
- dropdownVariant() {
- if (this.isDeprecated) {
- return 'default';
- }
- return 'confirm';
- },
},
methods: {
onShowInstructionsClick() {
@@ -103,46 +80,51 @@ export default {
onTokenReset(token) {
this.currentRegistrationToken = token;
- this.$refs.runnerRegistrationDropdown.hide(true);
+ this.$refs.runnerRegistrationDropdown.close();
+ },
+ onCopy() {
+ this.$refs.runnerRegistrationDropdown.close();
},
},
};
</script>
<template>
- <gl-dropdown
+ <gl-disclosure-dropdown
ref="runnerRegistrationDropdown"
- menu-class="gl-w-auto!"
- :text="dropdownText"
- :toggle-class="dropdownToggleClass"
- :variant="dropdownVariant"
- :category="dropdownCategory"
+ :toggle-text="actionText"
+ toggle-class="gl-px-3!"
+ variant="default"
+ category="tertiary"
v-bind="$attrs"
+ icon="ellipsis_v"
+ text-sr-only
+ no-caret
>
- <template v-if="isDeprecated" #button-content>
- <span class="gl-sr-only">{{ actionText }}</span>
- <gl-icon name="ellipsis_v" />
- </template>
<gl-dropdown-form class="gl-p-4!">
- <registration-token input-id="token-value" :value="currentRegistrationToken">
- <template v-if="isDeprecated" #label-description>
+ <registration-token input-id="token-value" :value="currentRegistrationToken" @copy="onCopy">
+ <template #label-description>
<gl-icon name="warning" class="gl-text-orange-500" />
<span class="gl-text-secondary">
- {{ s__('Runners|Support for registration tokens is deprecated') }}
+ {{ $options.i18n.supportForRegistrationTokensDeprecated }}
</span>
</template>
</registration-token>
</gl-dropdown-form>
- <gl-dropdown-divider />
- <gl-dropdown-item @click.capture.native.stop="onShowInstructionsClick">
- {{ $options.i18n.showInstallationInstructions }}
- <runner-instructions-modal
- ref="runnerInstructionsModal"
- :registration-token="currentRegistrationToken"
- data-testid="runner-instructions-modal"
- />
- </gl-dropdown-item>
- <gl-dropdown-divider />
- <registration-token-reset-dropdown-item :type="type" @tokenReset="onTokenReset" />
- </gl-dropdown>
+ <gl-disclosure-dropdown-group bordered>
+ <gl-disclosure-dropdown-item @action="onShowInstructionsClick">
+ <template #list-item>
+ {{ $options.i18n.showInstallationInstructions }}
+ <runner-instructions-modal
+ ref="runnerInstructionsModal"
+ :registration-token="currentRegistrationToken"
+ data-testid="runner-instructions-modal"
+ />
+ </template>
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown-group>
+ <gl-disclosure-dropdown-group bordered>
+ <registration-token-reset-dropdown-item :type="type" @tokenReset="onTokenReset" />
+ </gl-disclosure-dropdown-group>
+ </gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_token.vue b/app/assets/javascripts/ci/runner/components/registration/registration_token.vue
index b196bccf66f..339c92a427f 100644
--- a/app/assets/javascripts/ci/runner/components/registration/registration_token.vue
+++ b/app/assets/javascripts/ci/runner/components/registration/registration_token.vue
@@ -31,6 +31,7 @@ export default {
onCopy() {
// value already in the clipboard, simply notify the user
this.$toast?.show(s__('Runners|Registration token copied!'));
+ this.$emit('copy');
},
},
I18N_COPY_BUTTON_TITLE: s__('Runners|Copy registration token'),
diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_token_reset_dropdown_item.vue b/app/assets/javascripts/ci/runner/components/registration/registration_token_reset_dropdown_item.vue
index 6ce88fc54de..47ca3ed6227 100644
--- a/app/assets/javascripts/ci/runner/components/registration/registration_token_reset_dropdown_item.vue
+++ b/app/assets/javascripts/ci/runner/components/registration/registration_token_reset_dropdown_item.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdownItem, GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui';
+import { GlDisclosureDropdownItem, GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { TYPENAME_GROUP, TYPENAME_PROJECT } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
@@ -19,7 +19,7 @@ export default {
name: 'RunnerRegistrationTokenReset',
i18n,
components: {
- GlDropdownItem,
+ GlDisclosureDropdownItem,
GlLoadingIcon,
GlModal,
},
@@ -124,18 +124,20 @@ export default {
};
</script>
<template>
- <gl-dropdown-item v-gl-modal="$options.modalId">
- {{ __('Reset registration token') }}
- <gl-modal
- size="sm"
- :modal-id="$options.modalId"
- :action-primary="actionPrimary"
- :action-secondary="actionSecondary"
- :title="$options.i18n.modalTitle"
- @primary="handleModalPrimary"
- >
- <p>{{ $options.i18n.modalCopy }}</p>
- </gl-modal>
- <gl-loading-icon v-if="loading" inline />
- </gl-dropdown-item>
+ <gl-disclosure-dropdown-item v-gl-modal="$options.modalId">
+ <template #list-item>
+ {{ __('Reset registration token') }}
+ <gl-modal
+ size="sm"
+ :modal-id="$options.modalId"
+ :action-primary="actionPrimary"
+ :action-secondary="actionSecondary"
+ :title="$options.i18n.modalTitle"
+ @primary="handleModalPrimary"
+ >
+ <p>{{ $options.i18n.modalCopy }}</p>
+ </gl-modal>
+ <gl-loading-icon v-if="loading" inline />
+ </template>
+ </gl-disclosure-dropdown-item>
</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_delete_action.vue b/app/assets/javascripts/ci/runner/components/runner_delete_action.vue
new file mode 100644
index 00000000000..db8133c1ccb
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_delete_action.vue
@@ -0,0 +1,126 @@
+<script>
+import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutation.graphql';
+import { createAlert } from '~/alert';
+import { sprintf, s__ } from '~/locale';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { I18N_DELETED_TOAST } from '../constants';
+import RunnerDeleteModal from './runner_delete_modal.vue';
+
+/**
+ * Component that wraps a delete GraphQL mutation for the
+ * runner, given its id.
+ *
+ * You can use the slot to define a presentation for the
+ * delete action, like a button or dropdown item.
+ *
+ * Usage:
+ *
+ * ```vue
+ * <runner-delete-action
+ * #default="{ loading, onClick }"
+ * :runner="runner"
+ * @done="onDeleted"
+ * >
+ * <button :disabled="loading" @click="onClick"> Delete! </button>
+ * </runner-pause-action>
+ * ```
+ *
+ */
+export default {
+ name: 'RunnerDeleteAction',
+ components: {
+ RunnerDeleteModal,
+ },
+ props: {
+ runner: {
+ type: Object,
+ required: true,
+ validator: (runner) => {
+ return runner?.id && runner?.shortSha;
+ },
+ },
+ },
+ emits: ['done'],
+ data() {
+ return {
+ loading: false,
+ };
+ },
+ computed: {
+ runnerId() {
+ return getIdFromGraphQLId(this.runner.id);
+ },
+ runnerName() {
+ return `#${this.runnerId} (${this.runner.shortSha})`;
+ },
+ runnerManagersCount() {
+ return this.runner.managers?.count || 0;
+ },
+ runnerDeleteModalId() {
+ return `delete-runner-modal-${this.runnerId}`;
+ },
+ },
+ methods: {
+ onClick() {
+ this.$refs.modal.show();
+ },
+ async onDelete() {
+ // "loading" stays "true" until this row is removed,
+ // should only change back if the operation fails.
+ this.loading = true;
+ try {
+ await this.$apollo.mutate({
+ mutation: runnerDeleteMutation,
+ variables: {
+ input: {
+ id: this.runner.id,
+ },
+ },
+ update: (cache, { data }) => {
+ const { errors } = data.runnerDelete;
+
+ if (errors?.length) {
+ this.onError(new Error(errors.join(' ')));
+ return;
+ }
+
+ this.$emit('done', {
+ message: sprintf(I18N_DELETED_TOAST, { name: this.runnerName }),
+ });
+
+ // Remove deleted runner from the cache
+ const cacheId = cache.identify(this.runner);
+ cache.evict({ id: cacheId });
+ cache.gc();
+ },
+ });
+ } catch (e) {
+ this.onError(e);
+ }
+ },
+ onError(error) {
+ this.loading = false;
+ const { message } = error;
+ const title = sprintf(s__('Runners|Runner %{runnerName} failed to delete'), {
+ runnerName: this.runnerName,
+ });
+
+ createAlert({ title, message });
+ captureException({ error, component: this.$options.name });
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <slot :loading="loading" :on-click="onClick"></slot>
+ <runner-delete-modal
+ ref="modal"
+ :modal-id="runnerDeleteModalId"
+ :runner-name="runnerName"
+ :managers-count="runnerManagersCount"
+ @primary="onDelete"
+ />
+ </div>
+</template>
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 3560521e8d7..d228a022032 100644
--- a/app/assets/javascripts/ci/runner/components/runner_delete_button.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_delete_button.vue
@@ -1,30 +1,21 @@
<script>
-import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
-import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutation.graphql';
-import { createAlert } from '~/alert';
-import { sprintf, s__ } from '~/locale';
-import { captureException } from '~/ci/runner/sentry_utils';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { I18N_DELETE_RUNNER, I18N_DELETED_TOAST } from '../constants';
-import RunnerDeleteModal from './runner_delete_modal.vue';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { I18N_DELETE_RUNNER } from '../constants';
+import RunnerDeleteAction from './runner_delete_action.vue';
export default {
name: 'RunnerDeleteButton',
components: {
GlButton,
- RunnerDeleteModal,
+ RunnerDeleteAction,
},
directives: {
GlTooltip: GlTooltipDirective,
- GlModal: GlModalDirective,
},
props: {
runner: {
type: Object,
required: true,
- validator: (runner) => {
- return runner?.id && runner?.shortSha;
- },
},
compact: {
type: Boolean,
@@ -39,17 +30,11 @@ export default {
};
},
computed: {
- runnerId() {
- return getIdFromGraphQLId(this.runner.id);
- },
- runnerName() {
- return `#${this.runnerId} (${this.runner.shortSha})`;
- },
- runnerManagersCount() {
- return this.runner.managers?.count || 0;
- },
- runnerDeleteModalId() {
- return `delete-runner-modal-${this.runnerId}`;
+ buttonContent() {
+ if (this.compact) {
+ return null;
+ }
+ return I18N_DELETE_RUNNER;
},
icon() {
if (this.compact) {
@@ -57,12 +42,6 @@ export default {
}
return '';
},
- buttonContent() {
- if (this.compact) {
- return null;
- }
- return I18N_DELETE_RUNNER;
- },
buttonClass() {
// Ensure a square button is shown when compact: true.
// Without this class we will have distorted/rectangular button.
@@ -78,83 +57,36 @@ export default {
return null;
},
tooltip() {
- // Only show basic "delete" tooltip when compact.
- // Also prevent a "sticky" tooltip: If this button is
- // loading, mouseout listeners don't run leaving the tooltip stuck
- if (this.compact && !this.deleting) {
+ if (this.compact) {
return I18N_DELETE_RUNNER;
}
return '';
},
},
methods: {
- async onDelete() {
- // Deleting stays "true" until this row is removed,
- // should only change back if the operation fails.
- this.deleting = true;
- try {
- await this.$apollo.mutate({
- mutation: runnerDeleteMutation,
- variables: {
- input: {
- id: this.runner.id,
- },
- },
- update: (cache, { data }) => {
- const { errors } = data.runnerDelete;
-
- if (errors?.length) {
- this.onError(new Error(errors.join(' ')));
- return;
- }
-
- this.$emit('deleted', {
- message: sprintf(I18N_DELETED_TOAST, { name: this.runnerName }),
- });
-
- // Remove deleted runner from the cache
- const cacheId = cache.identify(this.runner);
- cache.evict({ id: cacheId });
- cache.gc();
- },
- });
- } catch (e) {
- this.onError(e);
- }
- },
- onError(error) {
- this.deleting = false;
- const { message } = error;
- const title = sprintf(s__('Runners|Runner %{runnerName} failed to delete'), {
- runnerName: this.runnerName,
- });
-
- createAlert({ title, message });
- captureException({ error, component: this.$options.name });
+ onDone(event) {
+ this.$emit('deleted', event);
},
},
};
</script>
<template>
- <div v-gl-tooltip="tooltip" class="btn-group">
- <gl-button
- v-gl-modal="runnerDeleteModalId"
- :aria-label="ariaLabel"
- :icon="icon"
- :class="buttonClass"
- :loading="deleting"
- variant="danger"
- category="secondary"
- v-bind="$attrs"
- >
- {{ buttonContent }}
- </gl-button>
- <runner-delete-modal
- :modal-id="runnerDeleteModalId"
- :runner-name="runnerName"
- :managers-count="runnerManagersCount"
- @primary="onDelete"
- />
- </div>
+ <runner-delete-action class="btn-group" :runner="runner" @done="onDone">
+ <template #default="{ loading, onClick }">
+ <gl-button
+ v-gl-tooltip="loading ? '' : tooltip"
+ :aria-label="ariaLabel"
+ :icon="icon"
+ :class="buttonClass"
+ :loading="loading"
+ variant="danger"
+ category="secondary"
+ v-bind="$attrs"
+ @click="onClick"
+ >
+ {{ buttonContent }}
+ </gl-button>
+ </template>
+ </runner-delete-action>
</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_delete_disclosure_dropdown_item.vue b/app/assets/javascripts/ci/runner/components/runner_delete_disclosure_dropdown_item.vue
new file mode 100644
index 00000000000..0a81974a6d0
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_delete_disclosure_dropdown_item.vue
@@ -0,0 +1,38 @@
+<script>
+import { GlDisclosureDropdownItem } from '@gitlab/ui';
+import { I18N_DELETE } from '../constants';
+import RunnerDeleteAction from './runner_delete_action.vue';
+
+export default {
+ name: 'RunnerDeleteDisclosureDropdownItem',
+ components: {
+ GlDisclosureDropdownItem,
+ RunnerDeleteAction,
+ },
+ props: {
+ runner: {
+ type: Object,
+ required: true,
+ },
+ },
+ emits: ['deleted'],
+ methods: {
+ onDone(event) {
+ this.$emit('deleted', event);
+ },
+ },
+ I18N_DELETE,
+};
+</script>
+
+<template>
+ <runner-delete-action :runner="runner" @done="onDone">
+ <template #default="{ onClick }">
+ <gl-disclosure-dropdown-item @action="onClick">
+ <template #list-item>
+ <span class="gl-text-red-500">{{ $options.I18N_DELETE }}</span>
+ </template>
+ </gl-disclosure-dropdown-item>
+ </template>
+ </runner-delete-action>
+</template>
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 93f79fd67ea..124ac0b4e73 100644
--- a/app/assets/javascripts/ci/runner/components/runner_delete_modal.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_delete_modal.vue
@@ -52,6 +52,9 @@ export default {
},
},
methods: {
+ show() {
+ this.$refs.modal.show();
+ },
onPrimary() {
this.$refs.modal.hide();
},
diff --git a/app/assets/javascripts/ci/runner/components/runner_detail.vue b/app/assets/javascripts/ci/runner/components/runner_detail.vue
index 9e8055a8432..496985ff7ac 100644
--- a/app/assets/javascripts/ci/runner/components/runner_detail.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_detail.vue
@@ -40,12 +40,12 @@ export default {
<template>
<div class="gl-display-contents">
- <dt class="gl-mb-5 gl-mr-6 gl-max-w-26">
+ <dt class="gl-mb-5 gl-mr-6 gl-max-w-26" data-testid="label-slot">
<template v-if="label || $scopedSlots.label">
<slot name="label">{{ label }}</slot>
</template>
</dt>
- <dd class="gl-mb-5">
+ <dd class="gl-mb-5" data-testid="value-slot">
<template v-if="value || $scopedSlots.value">
<slot name="value">{{ value }}</slot>
</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_edit_button.vue b/app/assets/javascripts/ci/runner/components/runner_edit_button.vue
index 33e0acaf5c0..b4efd72b082 100644
--- a/app/assets/javascripts/ci/runner/components/runner_edit_button.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_edit_button.vue
@@ -9,15 +9,23 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ props: {
+ href: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
I18N_EDIT,
};
</script>
<template>
<gl-button
+ v-if="href"
v-gl-tooltip="$options.I18N_EDIT"
- v-bind="$attrs"
:aria-label="$options.I18N_EDIT"
+ :href="href"
icon="pencil"
v-on="$listeners"
/>
diff --git a/app/assets/javascripts/ci/runner/components/runner_edit_disclosure_dropdown_item.vue b/app/assets/javascripts/ci/runner/components/runner_edit_disclosure_dropdown_item.vue
new file mode 100644
index 00000000000..d0dcc04c3dc
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_edit_disclosure_dropdown_item.vue
@@ -0,0 +1,29 @@
+<script>
+import { GlDisclosureDropdownItem } from '@gitlab/ui';
+
+import { I18N_EDIT } from '../constants';
+
+export default {
+ name: 'RunnerEditDisclosureDropdownItem',
+ components: {
+ GlDisclosureDropdownItem,
+ },
+ props: {
+ href: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ item() {
+ return { text: I18N_EDIT, href: this.href };
+ },
+ },
+ I18N_EDIT,
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown-item v-if="href" :item="item" />
+</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_header.vue b/app/assets/javascripts/ci/runner/components/runner_header.vue
index f46e894bf2e..55a33ef2074 100644
--- a/app/assets/javascripts/ci/runner/components/runner_header.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_header.vue
@@ -32,31 +32,29 @@ export default {
};
</script>
<template>
- <div
- class="gl-display-flex gl-justify-content-space-between gl-align-items-flex-start gl-gap-3 gl-flex-wrap gl-py-5"
- >
- <div>
+ <div class="gl-py-5">
+ <div class="gl-display-flex gl-justify-content-space-between">
<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"
- name="lock"
- :aria-label="$options.I18N_LOCKED_RUNNER_DESCRIPTION"
- />
- </template>
- <template #timeago>
- <time-ago :time="runner.createdAt" />
- </template>
- </gl-sprintf>
- </span>
- </div>
+ <slot name="actions"></slot>
+ </div>
+ <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"
+ name="lock"
+ :aria-label="$options.I18N_LOCKED_RUNNER_DESCRIPTION"
+ />
+ </template>
+ <template #timeago>
+ <time-ago :time="runner.createdAt" />
+ </template>
+ </gl-sprintf>
+ </span>
</div>
- <div class="gl-display-flex gl-gap-3 gl-flex-wrap"><slot name="actions"></slot></div>
</div>
</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_header_actions.vue b/app/assets/javascripts/ci/runner/components/runner_header_actions.vue
new file mode 100644
index 00000000000..bc6f184bd4d
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_header_actions.vue
@@ -0,0 +1,80 @@
+<script>
+import { GlDisclosureDropdown } from '@gitlab/ui';
+
+import RunnerDeleteButton from './runner_delete_button.vue';
+import RunnerEditButton from './runner_edit_button.vue';
+import RunnerPauseButton from './runner_pause_button.vue';
+
+import RunnerEditDisclosureDropdownItem from './runner_edit_disclosure_dropdown_item.vue';
+import RunnerPauseDisclosureDropdownItem from './runner_pause_disclosure_dropdown_item.vue';
+import RunnerDeleteDisclosureDropdownItem from './runner_delete_disclosure_dropdown_item.vue';
+
+export default {
+ name: 'RunnerHeaderActions',
+ components: {
+ GlDisclosureDropdown,
+
+ RunnerDeleteButton,
+ RunnerEditButton,
+ RunnerPauseButton,
+
+ RunnerEditDisclosureDropdownItem,
+ RunnerPauseDisclosureDropdownItem,
+ RunnerDeleteDisclosureDropdownItem,
+ },
+ props: {
+ runner: {
+ type: Object,
+ required: true,
+ },
+ editPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ canUpdate() {
+ return this.runner.userPermissions?.updateRunner;
+ },
+ canDelete() {
+ return this.runner.userPermissions?.deleteRunner;
+ },
+ },
+ methods: {
+ onDeleted(event) {
+ this.$emit('deleted', event);
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="canUpdate || canDelete">
+ <!-- sm and up screens -->
+ <div class="gl-display-none gl-sm-display-flex gl-gap-3">
+ <runner-edit-button v-if="canUpdate" :href="editPath" />
+ <runner-pause-button v-if="canUpdate" :runner="runner" />
+ <runner-delete-button v-if="canDelete" :runner="runner" @deleted="onDeleted" />
+ </div>
+
+ <!-- xs screens -->
+ <div class="gl-sm-display-none">
+ <gl-disclosure-dropdown
+ icon="ellipsis_v"
+ :toggle-text="s__('Runner|Runner actions')"
+ text-sr-only
+ category="tertiary"
+ no-caret
+ >
+ <runner-edit-disclosure-dropdown-item v-if="canUpdate" :href="editPath" />
+ <runner-pause-disclosure-dropdown-item v-if="canUpdate" :runner="runner" />
+ <runner-delete-disclosure-dropdown-item
+ v-if="canDelete"
+ :runner="runner"
+ @deleted="onDeleted"
+ />
+ </gl-disclosure-dropdown>
+ </div>
+ </div>
+</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 d2836962a97..a4a489074c3 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
@@ -11,7 +11,6 @@ import {
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';
@@ -44,15 +43,6 @@ export default {
default: null,
},
},
- computed: {
- shouldShowCreateRunnerWorkflow() {
- // create_runner_workflow_for_admin or create_runner_workflow_for_namespace
- return (
- this.glFeatures?.createRunnerWorkflowForAdmin ||
- this.glFeatures?.createRunnerWorkflowForNamespace
- );
- },
- },
modalId: 'runners-empty-state-instructions-modal',
svgHeight: 145,
EMPTY_STATE_SVG_URL,
@@ -63,7 +53,6 @@ export default {
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,
};
@@ -85,39 +74,22 @@ export default {
>
<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
- v-else-if="registrationToken"
- :message="$options.I18N_FOLLOW_REGISTRATION_INSTRUCTIONS"
- >
+ <gl-sprintf v-if="newRunnerPath" :message="$options.I18N_CREATE_RUNNER_LINK">
<template #link="{ content }">
- <gl-link v-gl-modal="$options.modalId">{{ content }}</gl-link>
- <runner-instructions-modal
- :modal-id="$options.modalId"
- :registration-token="registrationToken"
- />
+ <gl-link :href="newRunnerPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
- <template v-else>
+ <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>
diff --git a/app/assets/javascripts/ci/runner/components/runner_pause_action.vue b/app/assets/javascripts/ci/runner/components/runner_pause_action.vue
new file mode 100644
index 00000000000..184d6a83381
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_pause_action.vue
@@ -0,0 +1,89 @@
+<script>
+import runnerTogglePausedMutation from '~/ci/runner/graphql/shared/runner_toggle_paused.mutation.graphql';
+import { createAlert } from '~/alert';
+import { captureException } from '~/ci/runner/sentry_utils';
+
+/**
+ * Renderless component that wraps a GraphQL pause mutation for the
+ * runner, given its id and current "paused" value.
+ *
+ * You can use the slot to define a presentation for the delete action,
+ * like a button or dropdown item.
+
+ * Usage:
+ *
+ * ```vue
+ * <runner-pause-action
+ * #default="{ loading, onClick }"
+ * :runner="runner"
+ * @done="onToggled"
+ * >
+ * <button :disabled="loading" @click="onClick">{{ runner.paused ? 'Go!' : 'Stop!' }}</button>
+ * </runner-pause-action>
+ * ```
+ *
+ */
+export default {
+ name: 'RunnerPauseAction',
+ props: {
+ runner: {
+ type: Object,
+ required: true,
+ },
+ compact: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ emits: ['done'],
+ data() {
+ return {
+ loading: false,
+ };
+ },
+ methods: {
+ async onClick() {
+ this.loading = true;
+ try {
+ const input = {
+ id: this.runner.id,
+ paused: !this.runner.paused,
+ };
+
+ const {
+ data: {
+ runnerUpdate: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: runnerTogglePausedMutation,
+ variables: {
+ input,
+ },
+ });
+
+ if (errors && errors.length) {
+ throw new Error(errors.join(' '));
+ }
+ this.$emit('done');
+ } catch (e) {
+ this.onError(e);
+ } finally {
+ this.loading = false;
+ }
+ },
+ onError(error) {
+ const { message } = error;
+
+ createAlert({ message });
+ captureException({ error, component: this.$options.name });
+ },
+ },
+ render() {
+ return this.$scopedSlots.default({
+ onClick: this.onClick,
+ loading: this.loading,
+ });
+ },
+};
+</script>
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 d16c8f98bad..15bb54027c7 100644
--- a/app/assets/javascripts/ci/runner/components/runner_pause_button.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_pause_button.vue
@@ -1,14 +1,14 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
-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';
+
+import { I18N_RESUME, I18N_PAUSE, I18N_PAUSE_TOOLTIP, I18N_RESUME_TOOLTIP } from '../constants';
+import RunnerPauseAction from './runner_pause_action.vue';
export default {
name: 'RunnerPauseButton',
components: {
GlButton,
+ RunnerPauseAction,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -25,96 +25,47 @@ export default {
},
},
emits: ['toggledPaused'],
- data() {
- return {
- updating: false,
- };
- },
computed: {
isPaused() {
return this.runner.paused;
},
+ tooltip() {
+ return this.isPaused ? I18N_RESUME_TOOLTIP : I18N_PAUSE_TOOLTIP;
+ },
icon() {
return this.isPaused ? 'play' : 'pause';
},
label() {
return this.isPaused ? I18N_RESUME : I18N_PAUSE;
},
- buttonContent() {
- if (this.compact) {
- return null;
- }
- return this.label;
- },
ariaLabel() {
if (this.compact) {
return this.label;
}
return null;
},
- tooltip() {
- // Prevent a "sticky" tooltip: If this button is disabled,
- // mouseout listeners don't run leaving the tooltip stuck
- if (!this.updating) {
- return this.isPaused ? I18N_RESUME_TOOLTIP : I18N_PAUSE_TOOLTIP;
- }
- return '';
- },
- },
- methods: {
- async onToggle() {
- this.updating = true;
- try {
- const input = {
- id: this.runner.id,
- paused: !this.isPaused,
- };
-
- const {
- data: {
- runnerUpdate: { errors },
- },
- } = await this.$apollo.mutate({
- mutation: runnerTogglePausedMutation,
- variables: {
- input,
- },
- });
-
- if (errors && errors.length) {
- throw new Error(errors.join(' '));
- }
- this.$emit('toggledPaused');
- } catch (e) {
- this.onError(e);
- } finally {
- this.updating = false;
+ buttonContent() {
+ if (this.compact) {
+ return null;
}
- },
- onError(error) {
- const { message } = error;
-
- createAlert({ message });
- captureException({ error, component: this.$options.name });
+ return this.label;
},
},
};
</script>
<template>
- <gl-button
- v-gl-tooltip="tooltip"
- v-bind="$attrs"
- :aria-label="ariaLabel"
- :icon="icon"
- :loading="updating"
- @click="onToggle"
- v-on="$listeners"
- >
- <!--
- Use <template v-if> to ensure a square button is shown when compact: true.
- Sending empty content will still show a distorted/rectangular button.
- -->
- <template v-if="buttonContent">{{ buttonContent }}</template>
- </gl-button>
+ <runner-pause-action :runner="runner" @done="$emit('toggledPaused')">
+ <template #default="{ loading, onClick }">
+ <gl-button
+ v-gl-tooltip="loading ? '' : tooltip"
+ :icon="icon"
+ :aria-label="ariaLabel"
+ :loading="loading"
+ @click="onClick"
+ >
+ <template v-if="buttonContent">{{ buttonContent }}</template>
+ </gl-button>
+ </template>
+ </runner-pause-action>
</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_pause_disclosure_dropdown_item.vue b/app/assets/javascripts/ci/runner/components/runner_pause_disclosure_dropdown_item.vue
new file mode 100644
index 00000000000..3dd5e227a4a
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_pause_disclosure_dropdown_item.vue
@@ -0,0 +1,34 @@
+<script>
+import { GlDisclosureDropdownItem } from '@gitlab/ui';
+
+import { I18N_RESUME, I18N_PAUSE } from '../constants';
+import RunnerPauseAction from './runner_pause_action.vue';
+
+export default {
+ name: 'RunnerPauseDisclosureDropdownItem',
+ components: {
+ GlDisclosureDropdownItem,
+ RunnerPauseAction,
+ },
+ props: {
+ runner: {
+ type: Object,
+ required: true,
+ },
+ },
+ emits: ['toggledPaused'],
+ computed: {
+ item() {
+ return { text: this.runner.paused ? I18N_RESUME : I18N_PAUSE };
+ },
+ },
+};
+</script>
+
+<template>
+ <runner-pause-action :runner="runner" @done="$emit('toggledPaused')">
+ <template #default="{ onClick }">
+ <gl-disclosure-dropdown-item :item="item" @action="onClick" />
+ </template>
+ </runner-pause-action>
+</template>
diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js
index 40841696ead..203f97876de 100644
--- a/app/assets/javascripts/ci/runner/constants.js
+++ b/app/assets/javascripts/ci/runner/constants.js
@@ -1,4 +1,5 @@
import { __, s__ } from '~/locale';
+import { DOCS_URL } from 'jh_else_ce/lib/utils/url_utility';
export const RUNNER_TYPENAME = 'CiRunner'; // __typename
@@ -90,6 +91,7 @@ export const I18N_PAUSED_DESCRIPTION = s__('Runners|Not accepting jobs');
export const I18N_RESUME = __('Resume');
export const I18N_RESUME_TOOLTIP = s__('Runners|Resume accepting jobs');
+export const I18N_DELETE = s__('Runners|Delete');
export const I18N_DELETE_RUNNER = s__('Runners|Delete runner');
export const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted');
@@ -117,9 +119,6 @@ export const I18N_STILL_USING_REGISTRATION_TOKENS = s__('Runners|Still using reg
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');
@@ -271,12 +270,10 @@ export const DEFAULT_PLATFORM = LINUX_PLATFORM;
// Runner docs are in a separate repository and are not shipped with GitLab
// they are rendered as external URLs.
-export const INSTALL_HELP_URL = 'https://docs.gitlab.com/runner/install';
-export const EXECUTORS_HELP_URL = 'https://docs.gitlab.com/runner/executors/';
-export const SERVICE_COMMANDS_HELP_URL =
- 'https://docs.gitlab.com/runner/commands/#service-related-commands';
-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';
+export const INSTALL_HELP_URL = `${DOCS_URL}/runner/install`;
+export const EXECUTORS_HELP_URL = `${DOCS_URL}/runner/executors/`;
+export const SERVICE_COMMANDS_HELP_URL = `${DOCS_URL}/runner/commands/#service-related-commands`;
+export const CHANGELOG_URL = `https://gitlab.com/gitlab-org/gitlab-runner/blob/main/CHANGELOG.md`;
+export const DOCKER_HELP_URL = `${DOCS_URL}/runner/install/docker.html`;
+export const KUBERNETES_HELP_URL = `${DOCS_URL}/runner/install/kubernetes.html`;
+export const RUNNER_MANAGERS_HELP_URL = `${DOCS_URL}/runner/fleet_scaling/#workers-executors-and-autoscaling-capabilities`;
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 c0b888e758b..7ad9605d0a4 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
@@ -6,7 +6,6 @@ fragment ListItemShared on CiRunner {
runnerType
shortSha
version
- ipAddress
paused
locked
jobCount
@@ -22,8 +21,11 @@ fragment ListItemShared on CiRunner {
updateRunner
deleteRunner
}
- managers {
+ managers(first: 1) {
count
+ nodes {
+ ipAddress
+ }
}
groups(first: 1) {
nodes {
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 e885cf45c5a..4b570db772f 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
@@ -4,10 +4,8 @@ import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { visitUrl } from '~/lib/utils/url_utility';
-import RunnerDeleteButton from '../components/runner_delete_button.vue';
-import RunnerEditButton from '../components/runner_edit_button.vue';
-import RunnerPauseButton from '../components/runner_pause_button.vue';
import RunnerHeader from '../components/runner_header.vue';
+import RunnerHeaderActions from '../components/runner_header_actions.vue';
import RunnerDetailsTabs from '../components/runner_details_tabs.vue';
import { I18N_FETCH_ERROR } from '../constants';
@@ -18,10 +16,8 @@ import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_lo
export default {
name: 'GroupRunnerShowApp',
components: {
- RunnerDeleteButton,
- RunnerEditButton,
- RunnerPauseButton,
RunnerHeader,
+ RunnerHeaderActions,
RunnerDetailsTabs,
},
props: {
@@ -85,9 +81,11 @@ export default {
<div>
<runner-header v-if="runner" :runner="runner">
<template #actions>
- <runner-edit-button v-if="canUpdate && editGroupRunnerPath" :href="editGroupRunnerPath" />
- <runner-pause-button v-if="canUpdate" :runner="runner" />
- <runner-delete-button v-if="canDelete" :runner="runner" @deleted="onDeleted" />
+ <runner-header-actions
+ :runner="runner"
+ :edit-path="editGroupRunnerPath"
+ @deleted="onDeleted"
+ />
</template>
</runner-header>
diff --git a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
index 74523bc335f..71584c40a38 100644
--- a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
@@ -155,10 +155,6 @@ export default {
isSearchFiltered() {
return isSearchFiltered(this.search);
},
- shouldShowCreateRunnerWorkflow() {
- // create_runner_workflow_for_namespace feature flag
- return this.glFeatures.createRunnerWorkflowForNamespace;
- },
},
watch: {
search: {
@@ -231,11 +227,7 @@ export default {
/>
<div class="gl-w-full gl-md-w-auto gl-display-flex">
- <gl-button
- v-if="shouldShowCreateRunnerWorkflow && newRunnerPath"
- :href="newRunnerPath"
- variant="confirm"
- >
+ <gl-button v-if="newRunnerPath" :href="newRunnerPath" variant="confirm">
{{ s__('Runners|New group runner') }}
</gl-button>
<registration-dropdown
@@ -243,7 +235,7 @@ export default {
class="gl-ml-3"
:registration-token="registrationToken"
:type="$options.GROUP_TYPE"
- right
+ placement="right"
/>
</div>
</div>