diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-10 18:09:22 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-10 18:09:22 +0300 |
commit | 37140013714814d8ffe662a372697c56eea2fde0 (patch) | |
tree | b25c0bfc62da359f97b8b3742007c07723242f93 /app/assets/javascripts | |
parent | 948023c9c900344aa1e2f334bcaae5a194873b0d (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
22 files changed, 424 insertions, 82 deletions
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index aa735df7da5..a030797c698 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -3,9 +3,9 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import $ from 'jquery'; import { uniq } from 'lodash'; +import { getEmojiScoreWithIntent } from '~/emoji/utils'; import { getCookie, setCookie, scrollToElement } from '~/lib/utils/common_utils'; import * as Emoji from '~/emoji'; - import { dispose, fixTitle } from '~/tooltips'; import createFlash from './flash'; import axios from './lib/utils/axios_utils'; @@ -559,13 +559,45 @@ export class AwardsHandler { } } + getEmojiScore(emojis, value) { + const elem = $(value).find('[data-name]').get(0); + const emoji = emojis.filter((x) => x.emoji.name === elem.dataset.name)[0]; + elem.dataset.score = emoji.score; + + return emoji.score; + } + + sortEmojiElements(emojis, $elements) { + const scores = new WeakMap(); + + return $elements.sort((a, b) => { + let aScore = scores.get(a); + let bScore = scores.get(b); + + if (!aScore) { + aScore = this.getEmojiScore(emojis, a); + scores.set(a, aScore); + } + + if (!bScore) { + bScore = this.getEmojiScore(emojis, b); + scores.set(b, bScore); + } + + return aScore - bScore; + }); + } + findMatchingEmojiElements(query) { - const emojiMatches = this.emoji.searchEmoji(query).map((x) => x.emoji.name); + const matchingEmoji = this.emoji + .searchEmoji(query) + .map((x) => ({ ...x, score: getEmojiScoreWithIntent(x.emoji.name, x.score) })); + const matchingEmojiNames = matchingEmoji.map((x) => x.emoji.name); const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]'); const $matchingElements = $emojiElements.filter( - (i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0, + (i, elm) => matchingEmojiNames.indexOf(elm.dataset.name) >= 0, ); - return $matchingElements.closest('li').clone(); + return this.sortEmojiElements(matchingEmoji, $matchingElements.closest('li').clone()); } /* showMenuElement and hideMenuElement are performance optimizations. We use diff --git a/app/assets/javascripts/emoji/constants.js b/app/assets/javascripts/emoji/constants.js index a6eb4256561..7970a932095 100644 --- a/app/assets/javascripts/emoji/constants.js +++ b/app/assets/javascripts/emoji/constants.js @@ -19,3 +19,5 @@ export const CATEGORY_ROW_HEIGHT = 37; export const CACHE_VERSION_KEY = 'gl-emoji-map-version'; export const CACHE_KEY = 'gl-emoji-map'; + +export const NEUTRAL_INTENT_MULTIPLIER = 1; diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js index 4fdcdcc1b04..b9392fabcbd 100644 --- a/app/assets/javascripts/emoji/index.js +++ b/app/assets/javascripts/emoji/index.js @@ -2,6 +2,7 @@ import { escape, minBy } from 'lodash'; import emojiRegexFactory from 'emoji-regex'; import emojiAliases from 'emojis/aliases.json'; import { setAttributes } from '~/lib/utils/dom_utils'; +import { getEmojiScoreWithIntent } from '~/emoji/utils'; import AccessorUtilities from '../lib/utils/accessor'; import axios from '../lib/utils/axios_utils'; import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants'; @@ -144,6 +145,11 @@ function getNameMatch(emoji, query) { return null; } +// Sort emoji by emoji score falling back to a string comparison +export function sortEmoji(a, b) { + return a.score - b.score || a.fieldValue.localeCompare(b.fieldValue); +} + export function searchEmoji(query) { const lowercaseQuery = query ? `${query}`.toLowerCase() : ''; @@ -156,16 +162,14 @@ export function searchEmoji(query) { getDescriptionMatch(emoji, lowercaseQuery), getAliasMatch(emoji, matchingAliases), getNameMatch(emoji, lowercaseQuery), - ].filter(Boolean); + ] + .filter(Boolean) + .map((x) => ({ ...x, score: getEmojiScoreWithIntent(x.emoji.name, x.score) })); return minBy(matches, (x) => x.score); }) - .filter(Boolean); -} - -export function sortEmoji(items) { - // Sort results by index of and string comparison - return [...items].sort((a, b) => a.score - b.score || a.fieldValue.localeCompare(b.fieldValue)); + .filter(Boolean) + .sort(sortEmoji); } export const CATEGORY_NAMES = Object.keys(CATEGORY_ICON_MAP); diff --git a/app/assets/javascripts/emoji/utils.js b/app/assets/javascripts/emoji/utils.js new file mode 100644 index 00000000000..eb3dcea73c0 --- /dev/null +++ b/app/assets/javascripts/emoji/utils.js @@ -0,0 +1,8 @@ +import emojiIntents from 'emojis/intents.json'; +import { NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants'; + +export function getEmojiScoreWithIntent(emojiName, baseScore) { + const intentMultiplier = emojiIntents[emojiName] || NEUTRAL_INTENT_MULTIPLIER; + + return 2 ** baseScore * intentMultiplier; +} diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 146255df31f..d4dafbdc94f 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -897,7 +897,7 @@ GfmAutoComplete.Emoji = { return Emoji.searchEmoji(query); }, sorter(items) { - return Emoji.sortEmoji(items); + return items.sort(Emoji.sortEmoji); }, }; // Team Members diff --git a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue index 43bf2e1a90c..0a8fec49aac 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue @@ -1,5 +1,5 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; +import { GlTooltipDirective, GlFormCheckbox } from '@gitlab/ui'; import { createNamespacedHelpers } from 'vuex'; import { s__ } from '~/locale'; @@ -8,19 +8,20 @@ const { mapActions: mapCommitActions, mapGetters: mapCommitGetters } = createNam ); export default { + components: { GlFormCheckbox }, directives: { GlTooltip: GlTooltipDirective, }, + i18n: { + newMrText: s__('IDE|Start a new merge request'), + tooltipText: s__( + 'IDE|This option is disabled because you are not allowed to create merge requests in this project.', + ), + }, computed: { ...mapCommitGetters(['shouldHideNewMrOption', 'shouldDisableNewMrOption', 'shouldCreateMR']), tooltipText() { - if (this.shouldDisableNewMrOption) { - return s__( - 'IDE|This option is disabled because you are not allowed to create merge requests in this project.', - ); - } - - return ''; + return this.shouldDisableNewMrOption ? this.$options.i18n.tooltipText : null; }, }, methods: { @@ -30,22 +31,23 @@ export default { </script> <template> - <fieldset v-if="!shouldHideNewMrOption"> - <hr class="my-2" /> - <label - v-gl-tooltip="tooltipText" - class="mb-0 js-ide-commit-new-mr" - :class="{ 'is-disabled': shouldDisableNewMrOption }" + <fieldset + v-if="!shouldHideNewMrOption" + v-gl-tooltip="tooltipText" + data-testid="new-merge-request-fieldset" + class="js-ide-commit-new-mr" + :class="{ 'is-disabled': shouldDisableNewMrOption }" + > + <hr class="gl-mt-3 gl-mb-4" /> + + <gl-form-checkbox + :disabled="shouldDisableNewMrOption" + :checked="shouldCreateMR" + @change="toggleShouldCreateMR" > - <input - :disabled="shouldDisableNewMrOption" - :checked="shouldCreateMR" - type="checkbox" - @change="toggleShouldCreateMR" - /> - <span class="gl-ml-3 ide-option-label"> - {{ __('Start a new merge request') }} + <span class="ide-option-label"> + {{ $options.i18n.newMrText }} </span> - </label> + </gl-form-checkbox> </fieldset> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue index 870355e884e..bd5d28dbb56 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue @@ -1,8 +1,20 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; +import { + GlTooltipDirective, + GlFormRadio, + GlFormRadioGroup, + GlFormGroup, + GlFormInput, +} from '@gitlab/ui'; import { mapActions, mapState, mapGetters } from 'vuex'; export default { + components: { + GlFormRadio, + GlFormRadioGroup, + GlFormGroup, + GlFormInput, + }, directives: { GlTooltip: GlTooltipDirective, }, @@ -51,35 +63,42 @@ export default { </script> <template> - <fieldset> - <label + <fieldset class="gl-mb-2"> + <gl-form-radio-group v-gl-tooltip="tooltipTitle" + :checked="commitAction" :class="{ 'is-disabled': disabled, }" > - <input + <gl-form-radio :value="value" - :checked="commitAction === value" :disabled="disabled" - type="radio" name="commit-action" data-qa-selector="commit_type_radio" - @change="updateCommitAction($event.target.value)" - /> - <span class="gl-ml-3"> - <span v-if="label" class="ide-option-label"> {{ label }} </span> <slot v-else></slot> - </span> - </label> - <div v-if="commitAction === value && showInput" class="ide-commit-new-branch"> - <input + @change="updateCommitAction(value)" + > + <span v-if="label" class="ide-option-label"> + {{ label }} + </span> + <slot v-else></slot> + </gl-form-radio> + </gl-form-radio-group> + + <gl-form-group + v-if="commitAction === value && showInput" + :label="placeholderBranchName" + :label-sr-only="true" + class="gl-ml-6 gl-mb-0" + > + <gl-form-input :placeholder="placeholderBranchName" :value="newBranchName" + :disabled="disabled" data-testid="ide-new-branch-name" - type="text" - class="form-control monospace" - @input="updateBranchName($event.target.value)" + class="gl-font-monospace" + @input="updateBranchName($event)" /> - </div> + </gl-form-group> </fieldset> </template> 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 c2bb635e056..a90ef2d3530 100644 --- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue +++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue @@ -10,6 +10,7 @@ import RegistrationDropdown from '../components/registration/registration_dropdo import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; import RunnerBulkDelete from '../components/runner_bulk_delete.vue'; import RunnerList from '../components/runner_list.vue'; +import RunnerListEmptyState from '../components/runner_list_empty_state.vue'; import RunnerName from '../components/runner_name.vue'; import RunnerStats from '../components/stat/runner_stats.vue'; import RunnerPagination from '../components/runner_pagination.vue'; @@ -35,6 +36,7 @@ import { fromUrlQueryToSearch, fromSearchToUrl, fromSearchToVariables, + isSearchFiltered, } from '../runner_search_utils'; import { captureException } from '../sentry_utils'; @@ -91,6 +93,7 @@ export default { RunnerFilteredSearchBar, RunnerBulkDelete, RunnerList, + RunnerListEmptyState, RunnerName, RunnerStats, RunnerPagination, @@ -98,7 +101,7 @@ export default { RunnerActionsCell, }, mixins: [glFeatureFlagMixin()], - inject: ['localMutations'], + inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath', 'localMutations'], props: { registrationToken: { type: String, @@ -190,6 +193,9 @@ export default { // Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/353981 return this.glFeatures.adminRunnersBulkDelete; }, + isSearchFiltered() { + return isSearchFiltered(this.search); + }, }, watch: { search: { @@ -298,9 +304,13 @@ export default { :stale-runners-count="staleRunnersTotal" /> - <div v-if="noRunnersFound" class="gl-text-center gl-p-5"> - {{ __('No runners found') }} - </div> + <runner-list-empty-state + v-if="noRunnersFound" + :registration-token="registrationToken" + :is-search-filtered="isSearchFiltered" + :svg-path="emptyStateSvgPath" + :filtered-svg-path="emptyStateFilteredSvgPath" + /> <template v-else> <runner-bulk-delete v-if="isBulkDeleteEnabled" /> <runner-list diff --git a/app/assets/javascripts/runner/admin_runners/index.js b/app/assets/javascripts/runner/admin_runners/index.js index b1d8442bb32..7bb6cd5689e 100644 --- a/app/assets/javascripts/runner/admin_runners/index.js +++ b/app/assets/javascripts/runner/admin_runners/index.js @@ -34,6 +34,8 @@ export const initAdminRunners = (selector = '#js-admin-runners') => { registrationToken, onlineContactTimeoutSecs, staleTimeoutSecs, + emptyStateSvgPath, + emptyStateFilteredSvgPath, } = el.dataset; const { cacheConfig, typeDefs, localMutations } = createLocalState(); @@ -50,6 +52,8 @@ export const initAdminRunners = (selector = '#js-admin-runners') => { localMutations, onlineContactTimeoutSecs, staleTimeoutSecs, + emptyStateSvgPath, + emptyStateFilteredSvgPath, }, render(h) { return h(AdminRunnersApp, { 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 93f86ae2a2c..a48db9f8ac8 100644 --- a/app/assets/javascripts/runner/components/cells/runner_status_cell.vue +++ b/app/assets/javascripts/runner/components/cells/runner_status_cell.vue @@ -7,6 +7,8 @@ import RunnerPausedBadge from '../runner_paused_badge.vue'; export default { components: { RunnerStatusBadge, + RunnerUpgradeStatusBadge: () => + import('ee_component/runner/components/runner_upgrade_status_badge.vue'), RunnerPausedBadge, }, directives: { @@ -33,6 +35,11 @@ export default { 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" diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue index dcfd4b84dd2..f1f99c728c5 100644 --- a/app/assets/javascripts/runner/components/runner_list.vue +++ b/app/assets/javascripts/runner/components/runner_list.vue @@ -12,7 +12,7 @@ import RunnerStatusCell from './cells/runner_status_cell.vue'; import RunnerTags from './runner_tags.vue'; const defaultFields = [ - tableField({ key: 'status', label: s__('Runners|Status') }), + 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') }), diff --git a/app/assets/javascripts/runner/components/runner_list_empty_state.vue b/app/assets/javascripts/runner/components/runner_list_empty_state.vue new file mode 100644 index 00000000000..cddd51a351c --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_list_empty_state.vue @@ -0,0 +1,68 @@ +<script> +import { GlEmptyState, GlLink, GlSprintf, GlModalDirective } from '@gitlab/ui'; +import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; + +export default { + components: { + GlEmptyState, + GlLink, + GlSprintf, + RunnerInstructionsModal, + }, + directives: { + GlModal: GlModalDirective, + }, + props: { + isSearchFiltered: { + type: Boolean, + required: false, + default: false, + }, + svgPath: { + type: String, + required: false, + default: '', + }, + filteredSvgPath: { + type: String, + required: false, + default: '', + }, + registrationToken: { + type: String, + required: false, + default: null, + }, + }, + modalId: 'runners-empty-state-instructions-modal', +}; +</script> + +<template> + <gl-empty-state + v-if="isSearchFiltered" + :title="s__('Runners|No results found')" + :svg-path="filteredSvgPath" + :description="s__('Runners|Edit your search and try again')" + /> + <gl-empty-state v-else :title="s__('Runners|Get started with runners')" :svg-path="svgPath"> + <template #description> + <gl-sprintf + :message=" + s__( + 'Runners|Runners are the agents that run your CI/CD jobs. Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.', + ) + " + > + <template #link="{ content }"> + <gl-link v-gl-modal="$options.modalId">{{ content }}</gl-link> + </template> + </gl-sprintf> + + <runner-instructions-modal + :modal-id="$options.modalId" + :registration-token="registrationToken" + /> + </template> + </gl-empty-state> +</template> diff --git a/app/assets/javascripts/runner/graphql/list/admin_runners.query.graphql b/app/assets/javascripts/runner/graphql/list/admin_runners.query.graphql index 5d0450e7418..61bfe03bf6e 100644 --- a/app/assets/javascripts/runner/graphql/list/admin_runners.query.graphql +++ b/app/assets/javascripts/runner/graphql/list/admin_runners.query.graphql @@ -1,4 +1,4 @@ -#import "~/runner/graphql/list/list_item.fragment.graphql" +#import "ee_else_ce/runner/graphql/list/list_item.fragment.graphql" #import "~/graphql_shared/fragments/page_info.fragment.graphql" query getRunners( diff --git a/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql b/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql index b4f2b5cd8c8..8755636a7ad 100644 --- a/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql +++ b/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql @@ -1,4 +1,4 @@ -#import "~/runner/graphql/list/list_item.fragment.graphql" +#import "ee_else_ce/runner/graphql/list/list_item.fragment.graphql" #import "~/graphql_shared/fragments/page_info.fragment.graphql" query getGroupRunners( diff --git a/app/assets/javascripts/runner/graphql/list/list_item.fragment.graphql b/app/assets/javascripts/runner/graphql/list/list_item.fragment.graphql index 620c18c5bc0..19a5a48ea75 100644 --- a/app/assets/javascripts/runner/graphql/list/list_item.fragment.graphql +++ b/app/assets/javascripts/runner/graphql/list/list_item.fragment.graphql @@ -1,20 +1,5 @@ +#import "./list_item_shared.fragment.graphql" + fragment ListItem on CiRunner { - __typename - id - description - runnerType - shortSha - version - revision - ipAddress - active - locked - jobCount - tagList - contactedAt - status(legacyMode: null) - userPermissions { - updateRunner - deleteRunner - } + ...ListItemShared } 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 new file mode 100644 index 00000000000..cf925359ffb --- /dev/null +++ b/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql @@ -0,0 +1,20 @@ +fragment ListItemShared on CiRunner { + __typename + id + description + runnerType + shortSha + version + revision + ipAddress + active + locked + jobCount + tagList + contactedAt + status(legacyMode: null) + userPermissions { + updateRunner + deleteRunner + } +} diff --git a/app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue b/app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue new file mode 100644 index 00000000000..c336e091fdf --- /dev/null +++ b/app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue @@ -0,0 +1,114 @@ +<script> +import { GlBadge, GlTab, GlTooltipDirective } from '@gitlab/ui'; +import { createAlert, VARIANT_SUCCESS } from '~/flash'; +import { TYPE_CI_RUNNER } from '~/graphql_shared/constants'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { redirectTo } from '~/lib/utils/url_utility'; +import { formatJobCount } from '../utils'; +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 RunnerDetails from '../components/runner_details.vue'; +import RunnerJobs from '../components/runner_jobs.vue'; +import { I18N_FETCH_ERROR } from '../constants'; +import runnerQuery from '../graphql/show/runner.query.graphql'; +import { captureException } from '../sentry_utils'; +import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage'; + +export default { + name: 'GroupRunnerShowApp', + components: { + GlBadge, + GlTab, + RunnerDeleteButton, + RunnerEditButton, + RunnerPauseButton, + RunnerHeader, + RunnerDetails, + RunnerJobs, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + runnerId: { + type: String, + required: true, + }, + runnersPath: { + type: String, + required: true, + }, + }, + data() { + return { + runner: null, + }; + }, + apollo: { + runner: { + query: runnerQuery, + variables() { + return { + id: convertToGraphQLId(TYPE_CI_RUNNER, this.runnerId), + }; + }, + error(error) { + createAlert({ message: I18N_FETCH_ERROR }); + + this.reportToSentry(error); + }, + }, + }, + computed: { + canUpdate() { + return this.runner.userPermissions?.updateRunner; + }, + canDelete() { + return this.runner.userPermissions?.deleteRunner; + }, + jobCount() { + return formatJobCount(this.runner?.jobCount); + }, + }, + errorCaptured(error) { + this.reportToSentry(error); + }, + methods: { + reportToSentry(error) { + captureException({ error, component: this.$options.name }); + }, + onDeleted({ message }) { + saveAlertToLocalStorage({ message, variant: VARIANT_SUCCESS }); + redirectTo(this.runnersPath); + }, + }, +}; +</script> +<template> + <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" /> + </template> + </runner-header> + + <runner-details :runner="runner"> + <template #jobs-tab> + <gl-tab> + <template #title> + {{ s__('Runners|Jobs') }} + <gl-badge v-if="jobCount" data-testid="job-count-badge" class="gl-ml-1" size="sm"> + {{ jobCount }} + </gl-badge> + </template> + + <runner-jobs v-if="runner" :runner="runner" /> + </gl-tab> + </template> + </runner-details> + </div> +</template> diff --git a/app/assets/javascripts/runner/group_runner_show/index.js b/app/assets/javascripts/runner/group_runner_show/index.js new file mode 100644 index 00000000000..d1b87c8e427 --- /dev/null +++ b/app/assets/javascripts/runner/group_runner_show/index.js @@ -0,0 +1,36 @@ +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 initAdminRunnerShow = (selector = '#js-group-runner-show') => { + showAlertFromLocalStorage(); + + const el = document.querySelector(selector); + + if (!el) { + return null; + } + + const { runnerId, runnersPath } = el.dataset; + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + return new Vue({ + el, + apolloProvider, + render(h) { + return h(GroupRunnerShowApp, { + props: { + runnerId, + runnersPath, + }, + }); + }, + }); +}; 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 b5bd4b111fd..641b3a8f560 100644 --- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue +++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue @@ -8,6 +8,7 @@ import { fetchPolicies } from '~/lib/graphql'; import RegistrationDropdown from '../components/registration/registration_dropdown.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'; import RunnerName from '../components/runner_name.vue'; import RunnerStats from '../components/stat/runner_stats.vue'; import RunnerPagination from '../components/runner_pagination.vue'; @@ -31,6 +32,7 @@ import { fromUrlQueryToSearch, fromSearchToUrl, fromSearchToVariables, + isSearchFiltered, } from '../runner_search_utils'; import { captureException } from '../sentry_utils'; @@ -86,12 +88,14 @@ export default { RegistrationDropdown, RunnerFilteredSearchBar, RunnerList, + RunnerListEmptyState, RunnerName, RunnerStats, RunnerPagination, RunnerTypeTabs, RunnerActionsCell, }, + inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'], props: { registrationToken: { type: String, @@ -196,6 +200,9 @@ export default { filteredSearchNamespace() { return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`; }, + isSearchFiltered() { + return isSearchFiltered(this.search); + }, }, watch: { search: { @@ -299,9 +306,13 @@ export default { :stale-runners-count="staleRunnersTotal" /> - <div v-if="noRunnersFound" class="gl-text-center gl-p-5"> - {{ __('No runners found') }} - </div> + <runner-list-empty-state + v-if="noRunnersFound" + :registration-token="registrationToken" + :is-search-filtered="isSearchFiltered" + :svg-path="emptyStateSvgPath" + :filtered-svg-path="emptyStateFilteredSvgPath" + /> <template v-else> <runner-list :runners="runners.items" :loading="runnersLoading"> <template #runner-name="{ runner }"> diff --git a/app/assets/javascripts/runner/group_runners/index.js b/app/assets/javascripts/runner/group_runners/index.js index 0dade30f820..feed6b0ceb7 100644 --- a/app/assets/javascripts/runner/group_runners/index.js +++ b/app/assets/javascripts/runner/group_runners/index.js @@ -22,6 +22,8 @@ export const initGroupRunners = (selector = '#js-group-runners') => { groupRunnersLimitedCount, onlineContactTimeoutSecs, staleTimeoutSecs, + emptyStateSvgPath, + emptyStateFilteredSvgPath, } = el.dataset; const apolloProvider = new VueApollo({ @@ -36,6 +38,8 @@ export const initGroupRunners = (selector = '#js-group-runners') => { groupId, onlineContactTimeoutSecs: parseInt(onlineContactTimeoutSecs, 10), staleTimeoutSecs: parseInt(staleTimeoutSecs, 10), + emptyStateSvgPath, + emptyStateFilteredSvgPath, }, render(h) { return h(GroupRunnersApp, { diff --git a/app/assets/javascripts/runner/runner_search_utils.js b/app/assets/javascripts/runner/runner_search_utils.js index 0d688ed65ef..e01878f355a 100644 --- a/app/assets/javascripts/runner/runner_search_utils.js +++ b/app/assets/javascripts/runner/runner_search_utils.js @@ -236,3 +236,17 @@ export const fromSearchToVariables = ({ ...paginationVariables, }; }; + +/** + * Decides whether or not a search object is the "default" or empty. + * + * A search is filtered if the user has entered filtering criteria. + * + * @param {Object} search + * @returns true if this search is filtered, false otherwise + */ +export const isSearchFiltered = ({ runnerType = null, filters = [], pagination = {} } = {}) => { + return Boolean( + runnerType !== null || filters?.length !== 0 || (pagination && pagination?.page !== 1), + ); +}; diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue index 489d4afa41f..72dcc16b57a 100644 --- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue +++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue @@ -302,9 +302,11 @@ export default { <span v-else class="gl-display-flex gl-align-items-center gl-line-height-normal"> {{ __('None') }} - <gl-button - class="gl-ml-2" + class="gl-ml-2 gl-reset-color!" href="#" + category="tertiary" variant="link" + size="small" data-testid="unassigned-users" @click="updateAlertAssignees(currentUser)" > |