diff options
Diffstat (limited to 'app/assets/javascripts/runner')
28 files changed, 445 insertions, 185 deletions
diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue index 777a332333d..f5620876783 100644 --- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue +++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue @@ -15,6 +15,7 @@ import allRunnersQuery from 'ee_else_ce/runner/graphql/list/all_runners.query.gr import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_count.query.graphql'; import RegistrationDropdown from '../components/registration/registration_dropdown.vue'; +import RunnerStackedLayoutBanner from '../components/runner_stacked_layout_banner.vue'; import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; import RunnerBulkDelete from '../components/runner_bulk_delete.vue'; import RunnerBulkDeleteCheckbox from '../components/runner_bulk_delete_checkbox.vue'; @@ -37,6 +38,7 @@ export default { components: { GlLink, RegistrationDropdown, + RunnerStackedLayoutBanner, RunnerFilteredSearchBar, RunnerBulkDelete, RunnerBulkDeleteCheckbox, @@ -169,6 +171,8 @@ export default { </script> <template> <div> + <runner-stacked-layout-banner /> + <div class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0" > diff --git a/app/assets/javascripts/runner/components/cells/runner_stacked_summary_cell.vue b/app/assets/javascripts/runner/components/cells/runner_stacked_summary_cell.vue new file mode 100644 index 00000000000..e5d49eb7c8e --- /dev/null +++ b/app/assets/javascripts/runner/components/cells/runner_stacked_summary_cell.vue @@ -0,0 +1,112 @@ +<script> +import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; + +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; +import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; +import RunnerName from '../runner_name.vue'; +import RunnerTags from '../runner_tags.vue'; +import RunnerTypeBadge from '../runner_type_badge.vue'; + +import { formatJobCount } from '../../utils'; +import { + I18N_LOCKED_RUNNER_DESCRIPTION, + I18N_VERSION_LABEL, + I18N_LAST_CONTACT_LABEL, + I18N_CREATED_AT_LABEL, +} from '../../constants'; +import RunnerSummaryField from './runner_summary_field.vue'; + +export default { + components: { + GlIcon, + GlSprintf, + TimeAgo, + RunnerSummaryField, + RunnerName, + RunnerTags, + RunnerTypeBadge, + RunnerUpgradeStatusIcon: () => + import('ee_component/runner/components/runner_upgrade_status_icon.vue'), + TooltipOnTruncate, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + runner: { + type: Object, + required: true, + }, + }, + computed: { + jobCount() { + return formatJobCount(this.runner.jobCount); + }, + }, + i18n: { + I18N_LOCKED_RUNNER_DESCRIPTION, + I18N_VERSION_LABEL, + I18N_LAST_CONTACT_LABEL, + I18N_CREATED_AT_LABEL, + }, +}; +</script> + +<template> + <div> + <div> + <slot :runner="runner" name="runner-name"> + <runner-name :runner="runner" /> + </slot> + <gl-icon + v-if="runner.locked" + v-gl-tooltip + :title="$options.i18n.I18N_LOCKED_RUNNER_DESCRIPTION" + name="lock" + /> + <runner-type-badge :type="runner.runnerType" size="sm" class="gl-vertical-align-middle" /> + </div> + + <div class="gl-ml-auto gl-display-inline-flex gl-max-w-full gl-py-2"> + <div class="gl-flex-shrink-0"> + <runner-upgrade-status-icon :runner="runner" /> + <gl-sprintf v-if="runner.version" :message="$options.i18n.I18N_VERSION_LABEL"> + <template #version>{{ runner.version }}</template> + </gl-sprintf> + </div> + <div class="gl-text-secondary gl-mx-2" aria-hidden="true">·</div> + <tooltip-on-truncate class="gl-text-truncate gl-display-block" :title="runner.description"> + {{ runner.description }} + </tooltip-on-truncate> + </div> + + <div> + <runner-summary-field icon="clock"> + <gl-sprintf :message="$options.i18n.I18N_LAST_CONTACT_LABEL"> + <template #timeAgo> + <time-ago v-if="runner.contactedAt" :time="runner.contactedAt" /> + <template v-else>{{ __('Never') }}</template> + </template> + </gl-sprintf> + </runner-summary-field> + + <runner-summary-field v-if="runner.ipAddress" icon="disk" :tooltip="__('IP Address')"> + {{ runner.ipAddress }} + </runner-summary-field> + + <runner-summary-field icon="pipeline" data-testid="job-count" :tooltip="__('Jobs')"> + {{ jobCount }} + </runner-summary-field> + + <runner-summary-field icon="calendar"> + <gl-sprintf :message="$options.i18n.I18N_CREATED_AT_LABEL"> + <template #timeAgo> + <time-ago v-if="runner.createdAt" :time="runner.createdAt" /> + </template> + </gl-sprintf> + </runner-summary-field> + </div> + + <runner-tags class="gl-display-block gl-pt-2" :tag-list="runner.tagList" size="sm" /> + </div> +</template> diff --git a/app/assets/javascripts/runner/components/cells/runner_status_cell.vue b/app/assets/javascripts/runner/components/cells/runner_status_cell.vue index a48db9f8ac8..eb98d4ae2fb 100644 --- a/app/assets/javascripts/runner/components/cells/runner_status_cell.vue +++ b/app/assets/javascripts/runner/components/cells/runner_status_cell.vue @@ -32,17 +32,14 @@ export default { <div> <runner-status-badge :runner="runner" - size="sm" class="gl-display-inline-block gl-max-w-full gl-text-truncate" /> <runner-upgrade-status-badge :runner="runner" - size="sm" class="gl-display-inline-block gl-max-w-full gl-text-truncate" /> <runner-paused-badge v-if="paused" - size="sm" class="gl-display-inline-block gl-max-w-full gl-text-truncate" /> </div> diff --git a/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue b/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue deleted file mode 100644 index 1cd098d6713..00000000000 --- a/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue +++ /dev/null @@ -1,71 +0,0 @@ -<script> -import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; - -import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; -import RunnerName from '../runner_name.vue'; -import RunnerTypeBadge from '../runner_type_badge.vue'; - -import { I18N_LOCKED_RUNNER_DESCRIPTION } from '../../constants'; - -export default { - components: { - GlIcon, - TooltipOnTruncate, - RunnerName, - RunnerTypeBadge, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - props: { - runner: { - type: Object, - required: true, - }, - }, - computed: { - runnerType() { - return this.runner.runnerType; - }, - locked() { - return this.runner.locked; - }, - description() { - return this.runner.description; - }, - ipAddress() { - return this.runner.ipAddress; - }, - }, - i18n: { - I18N_LOCKED_RUNNER_DESCRIPTION, - }, -}; -</script> - -<template> - <div> - <slot :runner="runner" name="runner-name"> - <runner-name :runner="runner" /> - </slot> - - <runner-type-badge :type="runnerType" size="sm" /> - <gl-icon - v-if="locked" - v-gl-tooltip - :title="$options.i18n.I18N_LOCKED_RUNNER_DESCRIPTION" - name="lock" - /> - <tooltip-on-truncate class="gl-display-block gl-text-truncate" :title="description"> - {{ description }} - </tooltip-on-truncate> - <tooltip-on-truncate - v-if="ipAddress" - class="gl-display-block gl-text-truncate" - :title="ipAddress" - > - <span class="gl-md-display-none gl-lg-display-inline">{{ __('IP Address') }}</span> - <strong>{{ ipAddress }}</strong> - </tooltip-on-truncate> - </div> -</template> diff --git a/app/assets/javascripts/runner/components/cells/runner_summary_field.vue b/app/assets/javascripts/runner/components/cells/runner_summary_field.vue new file mode 100644 index 00000000000..1bbbd55089a --- /dev/null +++ b/app/assets/javascripts/runner/components/cells/runner_summary_field.vue @@ -0,0 +1,33 @@ +<script> +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; + +export default { + components: { + GlIcon, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + icon: { + type: String, + required: false, + default: '', + }, + tooltip: { + type: String, + required: false, + default: '', + }, + }, +}; +</script> + +<template> + <div v-gl-tooltip="tooltip" class="gl-display-inline-block gl-text-secondary gl-my-2 gl-mr-2"> + <gl-icon v-if="icon" :name="icon" /> + <!-- display tooltip as a label for screen readers --> + <span class="gl-sr-only">{{ tooltip }}</span> + <slot></slot> + </div> +</template> diff --git a/app/assets/javascripts/runner/components/runner_detail.vue b/app/assets/javascripts/runner/components/runner_detail.vue index 584f77b7648..c260670b517 100644 --- a/app/assets/javascripts/runner/components/runner_detail.vue +++ b/app/assets/javascripts/runner/components/runner_detail.vue @@ -21,7 +21,8 @@ export default { props: { label: { type: String, - required: true, + default: null, + required: false, }, value: { type: String, @@ -39,7 +40,11 @@ export default { <template> <div class="gl-display-contents"> - <dt class="gl-mb-5 gl-mr-6 gl-max-w-26">{{ label }}</dt> + <dt class="gl-mb-5 gl-mr-6 gl-max-w-26"> + <template v-if="label || $scopedSlots.label"> + <slot name="label">{{ label }}</slot> + </template> + </dt> <dd class="gl-mb-5"> <template v-if="value || $scopedSlots.value"> <slot name="value">{{ value }}</slot> diff --git a/app/assets/javascripts/runner/components/runner_details.vue b/app/assets/javascripts/runner/components/runner_details.vue index d5222f39b81..79f934764c6 100644 --- a/app/assets/javascripts/runner/components/runner_details.vue +++ b/app/assets/javascripts/runner/components/runner_details.vue @@ -1,7 +1,10 @@ <script> -import { GlIntersperse } from '@gitlab/ui'; +import { GlIntersperse, GlLink } from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; import { s__ } from '~/locale'; +import HelpPopover from '~/vue_shared/components/help_popover.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants'; import RunnerDetail from './runner_detail.vue'; @@ -12,6 +15,8 @@ import RunnerTags from './runner_tags.vue'; export default { components: { GlIntersperse, + GlLink, + HelpPopover, RunnerDetail, RunnerMaintenanceNoteDetail: () => import('ee_component/runner/components/runner_maintenance_note_detail.vue'), @@ -24,6 +29,7 @@ export default { RunnerTags, TimeAgo, }, + mixins: [glFeatureFlagMixin()], props: { runner: { type: Object, @@ -60,6 +66,16 @@ export default { isProjectRunner() { return this.runner?.runnerType === PROJECT_TYPE; }, + tokenExpirationHelpPopoverOptions() { + return { + title: s__('Runners|Runner authentication token expiration'), + }; + }, + tokenExpirationHelpUrl() { + return helpPagePath('ci/runners/configure_runners', { + anchor: 'authentication-token-security', + }); + }, }, ACCESS_LEVEL_REF_PROTECTED, }; @@ -101,6 +117,34 @@ export default { </template> </runner-detail> <runner-detail :label="s__('Runners|Maximum job timeout')" :value="maximumTimeout" /> + <runner-detail + v-if="glFeatures.enforceRunnerTokenExpiresAt" + :empty-value="s__('Runners|Never expires')" + > + <template #label> + {{ s__('Runners|Token expiry') }} + <help-popover :options="tokenExpirationHelpPopoverOptions"> + <p> + {{ + s__( + 'Runners|Runner authentication tokens will expire based on a set interval. They will automatically rotate once expired.', + ) + }} + </p> + <p class="gl-mb-0"> + <gl-link + :href="tokenExpirationHelpUrl" + target="_blank" + class="gl-reset-font-size" + >{{ __('Learn more') }}</gl-link + > + </p> + </help-popover> + </template> + <template v-if="runner.tokenExpiresAt" #value> + <time-ago :time="runner.tokenExpiresAt" /> + </template> + </runner-detail> <runner-detail :label="s__('Runners|Tags')"> <template v-if="tagList.length" #value> <runner-tags class="gl-vertical-align-middle" :tag-list="tagList" size="sm" /> diff --git a/app/assets/javascripts/runner/components/runner_header.vue b/app/assets/javascripts/runner/components/runner_header.vue index abc07cec1ad..874c234ca4c 100644 --- a/app/assets/javascripts/runner/components/runner_header.vue +++ b/app/assets/javascripts/runner/components/runner_header.vue @@ -38,31 +38,33 @@ export default { </script> <template> <div - class="gl-display-flex gl-align-items-center gl-py-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100" + class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-gap-3 gl-flex-wrap gl-py-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100" > - <div> + <div class="gl-display-flex gl-align-items-flex-start gl-gap-3 gl-flex-wrap"> <runner-status-badge :runner="runner" /> <runner-type-badge v-if="runner" :type="runner.runnerType" /> - <template v-if="runner.createdAt"> - <gl-sprintf :message="__('%{runner} created %{timeago}')"> - <template #runner> - <strong>{{ heading }}</strong> - <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> - </template> - <template v-else> - <strong>{{ heading }}</strong> - </template> + <span> + <template v-if="runner.createdAt"> + <gl-sprintf :message="__('%{runner} created %{timeago}')"> + <template #runner> + <strong>{{ heading }}</strong> + <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> + </template> + <template v-else> + <strong>{{ heading }}</strong> + </template> + </span> </div> - <div class="gl-ml-auto gl-flex-shrink-0"><slot name="actions"></slot></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/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue index 2e406f71792..26f1f3ce08c 100644 --- a/app/assets/javascripts/runner/components/runner_list.vue +++ b/app/assets/javascripts/runner/components/runner_list.vue @@ -1,24 +1,17 @@ <script> import { GlFormCheckbox, GlTableLite, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui'; -import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { __, s__ } from '~/locale'; -import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; +import { s__ } from '~/locale'; import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql'; import { formatJobCount, tableField } from '../utils'; -import RunnerSummaryCell from './cells/runner_summary_cell.vue'; +import RunnerStackedSummaryCell from './cells/runner_stacked_summary_cell.vue'; import RunnerStatusPopover from './runner_status_popover.vue'; import RunnerStatusCell from './cells/runner_status_cell.vue'; -import RunnerTags from './runner_tags.vue'; const defaultFields = [ tableField({ key: 'status', label: s__('Runners|Status'), thClasses: ['gl-w-15p'] }), - tableField({ key: 'summary', label: s__('Runners|Runner'), thClasses: ['gl-lg-w-25p'] }), - tableField({ key: 'version', label: __('Version') }), - tableField({ key: 'jobCount', label: __('Jobs') }), - tableField({ key: 'tagList', label: __('Tags'), thClasses: ['gl-lg-w-25p'] }), - tableField({ key: 'contactedAt', label: __('Last contact') }), - tableField({ key: 'actions', label: '' }), + tableField({ key: 'summary', label: s__('Runners|Runner') }), + tableField({ key: 'actions', label: '', thClasses: ['gl-w-15p'] }), ]; export default { @@ -26,11 +19,8 @@ export default { GlFormCheckbox, GlTableLite, GlSkeletonLoader, - TooltipOnTruncate, - TimeAgo, RunnerStatusPopover, - RunnerSummaryCell, - RunnerTags, + RunnerStackedSummaryCell, RunnerStatusCell, }, directives: { @@ -74,6 +64,8 @@ export default { }; }, fields() { + const fields = defaultFields; + if (this.checkable) { const checkboxField = tableField({ key: 'checkbox', @@ -81,9 +73,9 @@ export default { thClasses: ['gl-w-9'], tdClass: ['gl-text-center'], }); - return [checkboxField, ...defaultFields]; + return [checkboxField, ...fields]; } - return defaultFields; + return fields; }, }, methods: { @@ -141,30 +133,11 @@ export default { </template> <template #cell(summary)="{ item, index }"> - <runner-summary-cell :runner="item"> + <runner-stacked-summary-cell :runner="item"> <template #runner-name="{ runner }"> <slot name="runner-name" :runner="runner" :index="index"></slot> </template> - </runner-summary-cell> - </template> - - <template #cell(version)="{ item: { version } }"> - <tooltip-on-truncate class="gl-display-block gl-text-truncate" :title="version"> - {{ version }} - </tooltip-on-truncate> - </template> - - <template #cell(jobCount)="{ item: { jobCount } }"> - {{ formatJobCount(jobCount) }} - </template> - - <template #cell(tagList)="{ item: { tagList } }"> - <runner-tags :tag-list="tagList" size="sm" /> - </template> - - <template #cell(contactedAt)="{ item: { contactedAt } }"> - <time-ago v-if="contactedAt" :time="contactedAt" /> - <template v-else>{{ __('Never') }}</template> + </runner-stacked-summary-cell> </template> <template #cell(actions)="{ item }"> diff --git a/app/assets/javascripts/runner/components/runner_name.vue b/app/assets/javascripts/runner/components/runner_name.vue index 8e495125e03..d4ecfd2d776 100644 --- a/app/assets/javascripts/runner/components/runner_name.vue +++ b/app/assets/javascripts/runner/components/runner_name.vue @@ -14,5 +14,7 @@ export default { }; </script> <template> - <span>#{{ getIdFromGraphQLId(runner.id) }} ({{ runner.shortSha }})</span> + <span class="gl-font-weight-bold gl-vertical-align-middle" + >#{{ getIdFromGraphQLId(runner.id) }} ({{ runner.shortSha }})</span + > </template> diff --git a/app/assets/javascripts/runner/components/runner_paused_badge.vue b/app/assets/javascripts/runner/components/runner_paused_badge.vue index 27618290ce6..00fd84a48d8 100644 --- a/app/assets/javascripts/runner/components/runner_paused_badge.vue +++ b/app/assets/javascripts/runner/components/runner_paused_badge.vue @@ -1,6 +1,6 @@ <script> import { GlBadge, GlTooltipDirective } from '@gitlab/ui'; -import { I18N_PAUSED_DESCRIPTION } from '../constants'; +import { I18N_PAUSED, I18N_PAUSED_DESCRIPTION } from '../constants'; export default { components: { @@ -9,11 +9,17 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + I18N_PAUSED, I18N_PAUSED_DESCRIPTION, }; </script> <template> - <gl-badge v-gl-tooltip="$options.I18N_PAUSED_DESCRIPTION" variant="danger" v-bind="$attrs"> - {{ s__('Runners|paused') }} + <gl-badge + v-gl-tooltip="$options.I18N_PAUSED_DESCRIPTION" + variant="warning" + icon="status-paused" + v-bind="$attrs" + > + {{ $options.I18N_PAUSED }} </gl-badge> </template> diff --git a/app/assets/javascripts/runner/components/runner_projects.vue b/app/assets/javascripts/runner/components/runner_projects.vue index 2c1d2fc2b10..84008e8eee8 100644 --- a/app/assets/javascripts/runner/components/runner_projects.vue +++ b/app/assets/javascripts/runner/components/runner_projects.vue @@ -1,11 +1,13 @@ <script> -import { GlSkeletonLoader } from '@gitlab/ui'; +import { GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui'; import { sprintf, formatNumber } from '~/locale'; import { createAlert } from '~/flash'; import runnerProjectsQuery from '../graphql/show/runner_projects.query.graphql'; import { I18N_ASSIGNED_PROJECTS, - I18N_NONE, + I18N_CLEAR_FILTER_PROJECTS, + I18N_FILTER_PROJECTS, + I18N_NO_PROJECTS_FOUND, I18N_FETCH_ERROR, RUNNER_DETAILS_PROJECTS_PAGE_SIZE, } from '../constants'; @@ -14,9 +16,12 @@ import { captureException } from '../sentry_utils'; import RunnerAssignedItem from './runner_assigned_item.vue'; import RunnerPagination from './runner_pagination.vue'; +const SHORT_SEARCH_LENGTH = 3; + export default { name: 'RunnerProjects', components: { + GlSearchBoxByType, GlSkeletonLoader, RunnerAssignedItem, RunnerPagination, @@ -35,6 +40,7 @@ export default { pageInfo: {}, count: 0, }, + search: '', pagination: {}, }; }, @@ -61,9 +67,10 @@ export default { }, computed: { variables() { - const { id } = this.runner; + const { search, runner } = this; return { - id, + id: runner.id, + search: search.length >= SHORT_SEARCH_LENGTH ? search : '', ...getPaginationVariables(this.pagination, RUNNER_DETAILS_PROJECTS_PAGE_SIZE), }; }, @@ -80,22 +87,38 @@ export default { isOwner(projectId) { return projectId === this.projects.ownerProjectId; }, + onSearchInput(search) { + this.search = search; + this.pagination = {}; + }, onPaginationInput(value) { this.pagination = value; }, }, - I18N_NONE, + RUNNER_DETAILS_PROJECTS_PAGE_SIZE, + I18N_CLEAR_FILTER_PROJECTS, + I18N_FILTER_PROJECTS, + I18N_NO_PROJECTS_FOUND, }; </script> <template> <div class="gl-border-t-gray-100 gl-border-t-1 gl-border-t-solid"> - <h3 class="gl-font-lg gl-mt-5 gl-mb-0"> + <h3 class="gl-font-lg gl-mt-5"> {{ heading }} </h3> + <gl-search-box-by-type + :is-loading="loading" + :clear-button-title="$options.I18N_CLEAR_FILTER_PROJECTS" + :placeholder="$options.I18N_FILTER_PROJECTS" + debounce="500" + class="gl-w-28" + :value="search" + @input="onSearchInput" + /> - <div v-if="loading" class="gl-py-5"> - <gl-skeleton-loader /> + <div v-if="!projects.items.length && loading" class="gl-py-5"> + <gl-skeleton-loader v-for="i in $options.RUNNER_DETAILS_PROJECTS_PAGE_SIZE" :key="i" /> </div> <template v-else-if="projects.items.length"> <runner-assigned-item @@ -110,7 +133,7 @@ export default { :is-owner="isOwner(project.id)" /> </template> - <span v-else class="gl-text-gray-500">{{ $options.I18N_NONE }}</span> + <div v-else class="gl-py-5 gl-text-gray-500">{{ $options.I18N_NO_PROJECTS_FOUND }}</div> <runner-pagination :disabled="loading" diff --git a/app/assets/javascripts/runner/components/runner_stacked_layout_banner.vue b/app/assets/javascripts/runner/components/runner_stacked_layout_banner.vue new file mode 100644 index 00000000000..e3a9a9fd8a4 --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_stacked_layout_banner.vue @@ -0,0 +1,58 @@ +<script> +import allChangesCommittedSvg from '@gitlab/svgs/dist/illustrations/multi-editor_all_changes_committed_empty.svg'; +import { GlBanner } from '@gitlab/ui'; + +import { s__ } from '~/locale'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; + +const I18N_TITLE = s__("Runners|We've made some changes and want your feedback"); +const I18N_DESCRIPTION = s__( + "Runners|We want you to be able to manage your runners easily and efficiently from this page, and we are making changes to get there. Give us feedback on how we're doing!", +); +const I18N_LINK = s__('Runners|Add your feedback in the issue'); + +// use a data url instead getting it from via HTML data-* attributes to simplify removal of this feature flag +const ILLUSTRATION_URL = `data:image/svg+xml;utf8,${encodeURIComponent(allChangesCommittedSvg)}`; +const ISSUE_URL = 'https://gitlab.com/gitlab-org/gitlab/-/issues/371621'; +const STORAGE_KEY = 'runner_list_stacked_layout_feedback_dismissed'; + +export default { + components: { + GlBanner, + LocalStorageSync, + }, + data() { + return { + isDismissed: false, + }; + }, + methods: { + onClose() { + this.isDismissed = true; + }, + }, + I18N_TITLE, + I18N_DESCRIPTION, + I18N_LINK, + ILLUSTRATION_URL, + ISSUE_URL, + STORAGE_KEY, +}; +</script> + +<template> + <div> + <local-storage-sync v-model="isDismissed" :storage-key="$options.STORAGE_KEY" /> + <gl-banner + v-if="!isDismissed" + :svg-path="$options.ILLUSTRATION_URL" + :title="$options.I18N_TITLE" + :button-text="$options.I18N_LINK" + :button-link="$options.ISSUE_URL" + class="gl-my-5" + @close="onClose" + > + <p>{{ $options.I18N_DESCRIPTION }}</p> + </gl-banner> + </div> +</template> diff --git a/app/assets/javascripts/runner/components/runner_status_badge.vue b/app/assets/javascripts/runner/components/runner_status_badge.vue index 073d0a49f59..d084408781e 100644 --- a/app/assets/javascripts/runner/components/runner_status_badge.vue +++ b/app/assets/javascripts/runner/components/runner_status_badge.vue @@ -1,8 +1,12 @@ <script> import { GlBadge, GlTooltipDirective } from '@gitlab/ui'; -import { __, s__, sprintf } from '~/locale'; +import { __, sprintf } from '~/locale'; import { getTimeago } from '~/lib/utils/datetime_utility'; import { + I18N_STATUS_ONLINE, + I18N_STATUS_NEVER_CONTACTED, + I18N_STATUS_OFFLINE, + I18N_STATUS_STALE, I18N_ONLINE_TIMEAGO_TOOLTIP, I18N_NEVER_CONTACTED_TOOLTIP, I18N_OFFLINE_TIMEAGO_TOOLTIP, @@ -39,26 +43,30 @@ export default { switch (this.runner?.status) { case STATUS_ONLINE: return { + icon: 'status-active', variant: 'success', - label: s__('Runners|online'), + label: I18N_STATUS_ONLINE, tooltip: this.timeAgoTooltip(I18N_ONLINE_TIMEAGO_TOOLTIP), }; case STATUS_NEVER_CONTACTED: return { + icon: 'time-out', variant: 'muted', - label: s__('Runners|never contacted'), + label: I18N_STATUS_NEVER_CONTACTED, tooltip: I18N_NEVER_CONTACTED_TOOLTIP, }; case STATUS_OFFLINE: return { + icon: 'time-out', variant: 'muted', - label: s__('Runners|offline'), + label: I18N_STATUS_OFFLINE, tooltip: this.timeAgoTooltip(I18N_OFFLINE_TIMEAGO_TOOLTIP), }; case STATUS_STALE: return { + icon: 'time-out', variant: 'warning', - label: s__('Runners|stale'), + label: I18N_STATUS_STALE, // runner may have contacted (or not) and be stale: consider both cases. tooltip: this.runner.contactedAt ? this.timeAgoTooltip(I18N_STALE_TIMEAGO_TOOLTIP) @@ -77,7 +85,13 @@ export default { }; </script> <template> - <gl-badge v-if="badge" v-gl-tooltip="badge.tooltip" :variant="badge.variant" v-bind="$attrs"> + <gl-badge + v-if="badge" + v-gl-tooltip="badge.tooltip" + :variant="badge.variant" + :icon="badge.icon" + v-bind="$attrs" + > {{ badge.label }} </gl-badge> </template> diff --git a/app/assets/javascripts/runner/components/runner_tags.vue b/app/assets/javascripts/runner/components/runner_tags.vue index 797d2a35b2c..38e566f9f53 100644 --- a/app/assets/javascripts/runner/components/runner_tags.vue +++ b/app/assets/javascripts/runner/components/runner_tags.vue @@ -20,7 +20,7 @@ export default { }; </script> <template> - <span> + <span v-if="tagList && tagList.length"> <runner-tag v-for="tag in tagList" :key="tag" diff --git a/app/assets/javascripts/runner/components/runner_type_badge.vue b/app/assets/javascripts/runner/components/runner_type_badge.vue index b885dcefdcb..f568f914004 100644 --- a/app/assets/javascripts/runner/components/runner_type_badge.vue +++ b/app/assets/javascripts/runner/components/runner_type_badge.vue @@ -1,26 +1,31 @@ <script> import { GlBadge, GlTooltipDirective } from '@gitlab/ui'; -import { s__ } from '~/locale'; import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE, + I18N_INSTANCE_TYPE, I18N_INSTANCE_RUNNER_DESCRIPTION, + I18N_GROUP_TYPE, I18N_GROUP_RUNNER_DESCRIPTION, + I18N_PROJECT_TYPE, I18N_PROJECT_RUNNER_DESCRIPTION, } from '../constants'; const BADGE_DATA = { [INSTANCE_TYPE]: { - text: s__('Runners|shared'), + icon: 'users', + text: I18N_INSTANCE_TYPE, tooltip: I18N_INSTANCE_RUNNER_DESCRIPTION, }, [GROUP_TYPE]: { - text: s__('Runners|group'), + icon: 'group', + text: I18N_GROUP_TYPE, tooltip: I18N_GROUP_RUNNER_DESCRIPTION, }, [PROJECT_TYPE]: { - text: s__('Runners|specific'), + icon: 'project', + text: I18N_PROJECT_TYPE, tooltip: I18N_PROJECT_RUNNER_DESCRIPTION, }, }; @@ -50,7 +55,13 @@ export default { }; </script> <template> - <gl-badge v-if="badge" v-gl-tooltip="badge.tooltip" variant="info" v-bind="$attrs"> + <gl-badge + v-if="badge" + v-gl-tooltip="badge.tooltip" + variant="muted" + :icon="badge.icon" + v-bind="$attrs" + > {{ badge.text }} </gl-badge> </template> diff --git a/app/assets/javascripts/runner/components/search_tokens/paused_token_config.js b/app/assets/javascripts/runner/components/search_tokens/paused_token_config.js index c1ad5da3ab9..97ee8ec3eef 100644 --- a/app/assets/javascripts/runner/components/search_tokens/paused_token_config.js +++ b/app/assets/javascripts/runner/components/search_tokens/paused_token_config.js @@ -1,7 +1,7 @@ -import { __, s__ } from '~/locale'; +import { __ } from '~/locale'; import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; -import { PARAM_KEY_PAUSED } from '../../constants'; +import { PARAM_KEY_PAUSED, I18N_PAUSED } from '../../constants'; const options = [ { value: 'true', title: __('Yes') }, @@ -10,7 +10,7 @@ const options = [ export const pausedTokenConfig = { icon: 'pause', - title: s__('Runners|Paused'), + title: I18N_PAUSED, type: PARAM_KEY_PAUSED, token: BaseToken, unique: true, diff --git a/app/assets/javascripts/runner/components/search_tokens/status_token_config.js b/app/assets/javascripts/runner/components/search_tokens/status_token_config.js index 9e6f63d3f7c..f5c42d120fb 100644 --- a/app/assets/javascripts/runner/components/search_tokens/status_token_config.js +++ b/app/assets/javascripts/runner/components/search_tokens/status_token_config.js @@ -1,7 +1,11 @@ -import { __, s__ } from '~/locale'; +import { __ } from '~/locale'; import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; import { + I18N_STATUS_ONLINE, + I18N_STATUS_NEVER_CONTACTED, + I18N_STATUS_OFFLINE, + I18N_STATUS_STALE, STATUS_ONLINE, STATUS_OFFLINE, STATUS_NEVER_CONTACTED, @@ -10,10 +14,10 @@ import { } from '../../constants'; const options = [ - { value: STATUS_ONLINE, title: s__('Runners|Online') }, - { value: STATUS_OFFLINE, title: s__('Runners|Offline') }, - { value: STATUS_NEVER_CONTACTED, title: s__('Runners|Never contacted') }, - { value: STATUS_STALE, title: s__('Runners|Stale') }, + { value: STATUS_ONLINE, title: I18N_STATUS_ONLINE }, + { value: STATUS_OFFLINE, title: I18N_STATUS_OFFLINE }, + { value: STATUS_NEVER_CONTACTED, title: I18N_STATUS_NEVER_CONTACTED }, + { value: STATUS_STALE, title: I18N_STATUS_STALE }, ]; export const statusTokenConfig = { diff --git a/app/assets/javascripts/runner/components/stat/runner_stats.vue b/app/assets/javascripts/runner/components/stat/runner_stats.vue index 93e54ebe7f4..4df59f5a0c9 100644 --- a/app/assets/javascripts/runner/components/stat/runner_stats.vue +++ b/app/assets/javascripts/runner/components/stat/runner_stats.vue @@ -1,7 +1,13 @@ <script> -import { s__ } from '~/locale'; import RunnerSingleStat from '~/runner/components/stat/runner_single_stat.vue'; -import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '../../constants'; +import { + I18N_STATUS_ONLINE, + I18N_STATUS_OFFLINE, + I18N_STATUS_STALE, + STATUS_ONLINE, + STATUS_OFFLINE, + STATUS_STALE, +} from '../../constants'; export default { components: { @@ -29,8 +35,8 @@ export default { skip: this.statusCountSkip(STATUS_ONLINE), variables: { ...this.variables, status: STATUS_ONLINE }, variant: 'success', - title: s__('Runners|Online runners'), - metaText: s__('Runners|online'), + title: I18N_STATUS_ONLINE, + metaIcon: 'status-active', }, }, { @@ -39,8 +45,8 @@ export default { skip: this.statusCountSkip(STATUS_OFFLINE), variables: { ...this.variables, status: STATUS_OFFLINE }, variant: 'muted', - title: s__('Runners|Offline runners'), - metaText: s__('Runners|offline'), + title: I18N_STATUS_OFFLINE, + metaIcon: 'status-waiting', }, }, { @@ -49,8 +55,8 @@ export default { skip: this.statusCountSkip(STATUS_STALE), variables: { ...this.variables, status: STATUS_STALE }, variant: 'warning', - title: s__('Runners|Stale runners'), - metaText: s__('Runners|stale'), + title: I18N_STATUS_STALE, + metaIcon: 'time-out', }, }, ]; diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js index ed1afcbf691..3009577599f 100644 --- a/app/assets/javascripts/runner/constants.js +++ b/app/assets/javascripts/runner/constants.js @@ -23,6 +23,12 @@ export const I18N_GROUP_RUNNER_DESCRIPTION = s__( ); export const I18N_PROJECT_RUNNER_DESCRIPTION = s__('Runners|Associated with one or more projects'); +// Status +export const I18N_STATUS_ONLINE = s__('Runners|Online'); +export const I18N_STATUS_NEVER_CONTACTED = s__('Runners|Never contacted'); +export const I18N_STATUS_OFFLINE = s__('Runners|Offline'); +export const I18N_STATUS_STALE = s__('Runners|Stale'); + // Status help popover export const I18N_STATUS_POPOVER_TITLE = s__('Runners|Runner statuses'); @@ -62,6 +68,7 @@ export const I18N_STALE_NEVER_CONTACTED_TOOLTIP = s__( export const I18N_EDIT = __('Edit'); export const I18N_PAUSE = __('Pause'); +export const I18N_PAUSED = s__('Runners|Paused'); export const I18N_PAUSE_TOOLTIP = s__('Runners|Pause from accepting jobs'); export const I18N_PAUSED_DESCRIPTION = s__('Runners|Not accepting jobs'); @@ -77,20 +84,27 @@ export const I18N_DELETE_DISABLED_UNKNOWN_REASON = s__( ); export const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted'); +// List export const I18N_LOCKED_RUNNER_DESCRIPTION = s__( 'Runners|Runner is locked and available for currently assigned projects only. Only administrators can change the assigned projects.', ); +export const I18N_VERSION_LABEL = s__('Runners|Version %{version}'); +export const I18N_LAST_CONTACT_LABEL = s__('Runners|Last contact: %{timeAgo}'); +export const I18N_CREATED_AT_LABEL = s__('Runners|Created %{timeAgo}'); // Runner details export const I18N_DETAILS = s__('Runners|Details'); export const I18N_ASSIGNED_PROJECTS = s__('Runners|Assigned Projects (%{projectCount})'); +export const I18N_FILTER_PROJECTS = s__('Runners|Filter projects'); +export const I18N_CLEAR_FILTER_PROJECTS = __('Clear'); export const I18N_NONE = __('None'); export const I18N_NO_JOBS_FOUND = s__('Runners|This runner has not run any jobs.'); +export const I18N_NO_PROJECTS_FOUND = __('No projects found'); // Styles -export const RUNNER_TAG_BADGE_VARIANT = 'neutral'; +export const RUNNER_TAG_BADGE_VARIANT = 'info'; export const RUNNER_TAG_BG_CLASS = 'gl-bg-blue-100'; // Filtered search parameter names diff --git a/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql index ce23bddb898..a12ba7a751a 100644 --- a/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql +++ b/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql @@ -9,6 +9,7 @@ fragment ListItemShared on CiRunner { locked jobCount tagList + createdAt contactedAt status(legacyMode: null) userPermissions { diff --git a/app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql index 499c0156770..b5689ff7687 100644 --- a/app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql +++ b/app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql @@ -17,6 +17,7 @@ fragment RunnerDetailsShared on CiRunner { createdAt status(legacyMode: null) contactedAt + tokenExpiresAt version editAdminUrl userPermissions { diff --git a/app/assets/javascripts/runner/graphql/show/runner_projects.query.graphql b/app/assets/javascripts/runner/graphql/show/runner_projects.query.graphql index acc4a641565..e42648b3079 100644 --- a/app/assets/javascripts/runner/graphql/show/runner_projects.query.graphql +++ b/app/assets/javascripts/runner/graphql/show/runner_projects.query.graphql @@ -2,6 +2,7 @@ query getRunnerProjects( $id: CiRunnerID! + $search: String $first: Int $last: Int $before: String @@ -13,7 +14,7 @@ query getRunnerProjects( id } projectCount - projects(first: $first, last: $last, before: $before, after: $after) { + projects(search: $search, first: $first, last: $last, before: $before, after: $after) { nodes { id avatarUrl diff --git a/app/assets/javascripts/runner/group_runner_show/index.js b/app/assets/javascripts/runner/group_runner_show/index.js index 62a0dab9211..e75f337b38e 100644 --- a/app/assets/javascripts/runner/group_runner_show/index.js +++ b/app/assets/javascripts/runner/group_runner_show/index.js @@ -1,11 +1,14 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; +import { showAlertFromLocalStorage } from '../local_storage_alert/show_alert_from_local_storage'; import GroupRunnerShowApp from './group_runner_show_app.vue'; Vue.use(VueApollo); export const initGroupRunnerShow = (selector = '#js-group-runner-show') => { + showAlertFromLocalStorage(); + const el = document.querySelector(selector); if (!el) { diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue index a82411a2120..70826a6bfa1 100644 --- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue +++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue @@ -13,6 +13,7 @@ import { import groupRunnersQuery from 'ee_else_ce/runner/graphql/list/group_runners.query.graphql'; import RegistrationDropdown from '../components/registration/registration_dropdown.vue'; +import RunnerStackedLayoutBanner from '../components/runner_stacked_layout_banner.vue'; import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; import RunnerList from '../components/runner_list.vue'; import RunnerListEmptyState from '../components/runner_list_empty_state.vue'; @@ -37,6 +38,7 @@ export default { components: { GlLink, RegistrationDropdown, + RunnerStackedLayoutBanner, RunnerFilteredSearchBar, RunnerList, RunnerListEmptyState, @@ -50,7 +52,8 @@ export default { props: { registrationToken: { type: String, - required: true, + required: false, + default: null, }, groupFullPath: { type: String, @@ -178,6 +181,8 @@ export default { <template> <div> + <runner-stacked-layout-banner /> + <div class="gl-display-flex gl-align-items-center"> <runner-type-tabs ref="runner-type-tabs" @@ -191,6 +196,7 @@ export default { /> <registration-dropdown + v-if="registrationToken" class="gl-ml-auto" :registration-token="registrationToken" :type="$options.GROUP_TYPE" diff --git a/app/assets/javascripts/runner/admin_runner_edit/index.js b/app/assets/javascripts/runner/runner_edit/index.js index a2ac5731a62..5b2ddb8f68e 100644 --- a/app/assets/javascripts/runner/admin_runner_edit/index.js +++ b/app/assets/javascripts/runner/runner_edit/index.js @@ -1,11 +1,11 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; -import AdminRunnerEditApp from './admin_runner_edit_app.vue'; +import RunnerEditApp from './runner_edit_app.vue'; Vue.use(VueApollo); -export const initAdminRunnerEdit = (selector = '#js-admin-runner-edit') => { +export const initRunnerEdit = (selector) => { const el = document.querySelector(selector); if (!el) { @@ -22,7 +22,7 @@ export const initAdminRunnerEdit = (selector = '#js-admin-runner-edit') => { el, apolloProvider, render(h) { - return h(AdminRunnerEditApp, { + return h(RunnerEditApp, { props: { runnerId, runnerPath, diff --git a/app/assets/javascripts/runner/admin_runner_edit/admin_runner_edit_app.vue b/app/assets/javascripts/runner/runner_edit/runner_edit_app.vue index 40787cf72da..879162916a9 100644 --- a/app/assets/javascripts/runner/admin_runner_edit/admin_runner_edit_app.vue +++ b/app/assets/javascripts/runner/runner_edit/runner_edit_app.vue @@ -9,7 +9,7 @@ import runnerFormQuery from '../graphql/edit/runner_form.query.graphql'; import { captureException } from '../sentry_utils'; export default { - name: 'AdminRunnerEditApp', + name: 'RunnerEditApp', components: { RunnerHeader, RunnerUpdateForm, diff --git a/app/assets/javascripts/runner/utils.js b/app/assets/javascripts/runner/utils.js index cb2917a92fd..1ca0a9e86b5 100644 --- a/app/assets/javascripts/runner/utils.js +++ b/app/assets/javascripts/runner/utils.js @@ -70,3 +70,14 @@ export const getPaginationVariables = (pagination, pageSize = 10) => { // Get the first N items return { first: pageSize }; }; + +/** + * Turns a server-provided interval integer represented as a string into an + * integer that the frontend can use. + * + * @param {String} interval - String to convert + * @returns Parsed integer + */ +export const parseInterval = (interval) => { + return typeof interval === 'string' ? parseInt(interval, 10) : null; +}; |