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>2022-06-10 18:09:22 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-06-10 18:09:22 +0300
commit37140013714814d8ffe662a372697c56eea2fde0 (patch)
treeb25c0bfc62da359f97b8b3742007c07723242f93 /app/assets/javascripts
parent948023c9c900344aa1e2f334bcaae5a194873b0d (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/awards_handler.js40
-rw-r--r--app/assets/javascripts/emoji/constants.js2
-rw-r--r--app/assets/javascripts/emoji/index.js18
-rw-r--r--app/assets/javascripts/emoji/utils.js8
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue48
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue55
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue18
-rw-r--r--app/assets/javascripts/runner/admin_runners/index.js4
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_status_cell.vue7
-rw-r--r--app/assets/javascripts/runner/components/runner_list.vue2
-rw-r--r--app/assets/javascripts/runner/components/runner_list_empty_state.vue68
-rw-r--r--app/assets/javascripts/runner/graphql/list/admin_runners.query.graphql2
-rw-r--r--app/assets/javascripts/runner/graphql/list/group_runners.query.graphql2
-rw-r--r--app/assets/javascripts/runner/graphql/list/list_item.fragment.graphql21
-rw-r--r--app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql20
-rw-r--r--app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue114
-rw-r--r--app/assets/javascripts/runner/group_runner_show/index.js36
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue17
-rw-r--r--app/assets/javascripts/runner/group_runners/index.js4
-rw-r--r--app/assets/javascripts/runner/runner_search_utils.js14
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue4
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)"
>