Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
948023c9c9
commit
3714001371
|
@ -1 +1 @@
|
||||||
70d6aa021ebfc05d9d727a7eb4c9ff4782db4c30
|
30b922784b9d0492ba525a35ec09782dd2bcace3
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
|
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { uniq } from 'lodash';
|
import { uniq } from 'lodash';
|
||||||
|
import { getEmojiScoreWithIntent } from '~/emoji/utils';
|
||||||
import { getCookie, setCookie, scrollToElement } from '~/lib/utils/common_utils';
|
import { getCookie, setCookie, scrollToElement } from '~/lib/utils/common_utils';
|
||||||
import * as Emoji from '~/emoji';
|
import * as Emoji from '~/emoji';
|
||||||
|
|
||||||
import { dispose, fixTitle } from '~/tooltips';
|
import { dispose, fixTitle } from '~/tooltips';
|
||||||
import createFlash from './flash';
|
import createFlash from './flash';
|
||||||
import axios from './lib/utils/axios_utils';
|
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) {
|
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 $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
|
||||||
const $matchingElements = $emojiElements.filter(
|
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
|
/* showMenuElement and hideMenuElement are performance optimizations. We use
|
||||||
|
|
|
@ -19,3 +19,5 @@ export const CATEGORY_ROW_HEIGHT = 37;
|
||||||
|
|
||||||
export const CACHE_VERSION_KEY = 'gl-emoji-map-version';
|
export const CACHE_VERSION_KEY = 'gl-emoji-map-version';
|
||||||
export const CACHE_KEY = 'gl-emoji-map';
|
export const CACHE_KEY = 'gl-emoji-map';
|
||||||
|
|
||||||
|
export const NEUTRAL_INTENT_MULTIPLIER = 1;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { escape, minBy } from 'lodash';
|
||||||
import emojiRegexFactory from 'emoji-regex';
|
import emojiRegexFactory from 'emoji-regex';
|
||||||
import emojiAliases from 'emojis/aliases.json';
|
import emojiAliases from 'emojis/aliases.json';
|
||||||
import { setAttributes } from '~/lib/utils/dom_utils';
|
import { setAttributes } from '~/lib/utils/dom_utils';
|
||||||
|
import { getEmojiScoreWithIntent } from '~/emoji/utils';
|
||||||
import AccessorUtilities from '../lib/utils/accessor';
|
import AccessorUtilities from '../lib/utils/accessor';
|
||||||
import axios from '../lib/utils/axios_utils';
|
import axios from '../lib/utils/axios_utils';
|
||||||
import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants';
|
import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants';
|
||||||
|
@ -144,6 +145,11 @@ function getNameMatch(emoji, query) {
|
||||||
return null;
|
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) {
|
export function searchEmoji(query) {
|
||||||
const lowercaseQuery = query ? `${query}`.toLowerCase() : '';
|
const lowercaseQuery = query ? `${query}`.toLowerCase() : '';
|
||||||
|
|
||||||
|
@ -156,16 +162,14 @@ export function searchEmoji(query) {
|
||||||
getDescriptionMatch(emoji, lowercaseQuery),
|
getDescriptionMatch(emoji, lowercaseQuery),
|
||||||
getAliasMatch(emoji, matchingAliases),
|
getAliasMatch(emoji, matchingAliases),
|
||||||
getNameMatch(emoji, lowercaseQuery),
|
getNameMatch(emoji, lowercaseQuery),
|
||||||
].filter(Boolean);
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((x) => ({ ...x, score: getEmojiScoreWithIntent(x.emoji.name, x.score) }));
|
||||||
|
|
||||||
return minBy(matches, (x) => x.score);
|
return minBy(matches, (x) => x.score);
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean)
|
||||||
}
|
.sort(sortEmoji);
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CATEGORY_NAMES = Object.keys(CATEGORY_ICON_MAP);
|
export const CATEGORY_NAMES = Object.keys(CATEGORY_ICON_MAP);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -897,7 +897,7 @@ GfmAutoComplete.Emoji = {
|
||||||
return Emoji.searchEmoji(query);
|
return Emoji.searchEmoji(query);
|
||||||
},
|
},
|
||||||
sorter(items) {
|
sorter(items) {
|
||||||
return Emoji.sortEmoji(items);
|
return items.sort(Emoji.sortEmoji);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// Team Members
|
// Team Members
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlTooltipDirective } from '@gitlab/ui';
|
import { GlTooltipDirective, GlFormCheckbox } from '@gitlab/ui';
|
||||||
import { createNamespacedHelpers } from 'vuex';
|
import { createNamespacedHelpers } from 'vuex';
|
||||||
import { s__ } from '~/locale';
|
import { s__ } from '~/locale';
|
||||||
|
|
||||||
|
@ -8,19 +8,20 @@ const { mapActions: mapCommitActions, mapGetters: mapCommitGetters } = createNam
|
||||||
);
|
);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: { GlFormCheckbox },
|
||||||
directives: {
|
directives: {
|
||||||
GlTooltip: GlTooltipDirective,
|
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: {
|
computed: {
|
||||||
...mapCommitGetters(['shouldHideNewMrOption', 'shouldDisableNewMrOption', 'shouldCreateMR']),
|
...mapCommitGetters(['shouldHideNewMrOption', 'shouldDisableNewMrOption', 'shouldCreateMR']),
|
||||||
tooltipText() {
|
tooltipText() {
|
||||||
if (this.shouldDisableNewMrOption) {
|
return this.shouldDisableNewMrOption ? this.$options.i18n.tooltipText : null;
|
||||||
return s__(
|
|
||||||
'IDE|This option is disabled because you are not allowed to create merge requests in this project.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -30,22 +31,23 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<fieldset v-if="!shouldHideNewMrOption">
|
<fieldset
|
||||||
<hr class="my-2" />
|
v-if="!shouldHideNewMrOption"
|
||||||
<label
|
v-gl-tooltip="tooltipText"
|
||||||
v-gl-tooltip="tooltipText"
|
data-testid="new-merge-request-fieldset"
|
||||||
class="mb-0 js-ide-commit-new-mr"
|
class="js-ide-commit-new-mr"
|
||||||
:class="{ 'is-disabled': shouldDisableNewMrOption }"
|
:class="{ 'is-disabled': shouldDisableNewMrOption }"
|
||||||
|
>
|
||||||
|
<hr class="gl-mt-3 gl-mb-4" />
|
||||||
|
|
||||||
|
<gl-form-checkbox
|
||||||
|
:disabled="shouldDisableNewMrOption"
|
||||||
|
:checked="shouldCreateMR"
|
||||||
|
@change="toggleShouldCreateMR"
|
||||||
>
|
>
|
||||||
<input
|
<span class="ide-option-label">
|
||||||
:disabled="shouldDisableNewMrOption"
|
{{ $options.i18n.newMrText }}
|
||||||
:checked="shouldCreateMR"
|
|
||||||
type="checkbox"
|
|
||||||
@change="toggleShouldCreateMR"
|
|
||||||
/>
|
|
||||||
<span class="gl-ml-3 ide-option-label">
|
|
||||||
{{ __('Start a new merge request') }}
|
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</gl-form-checkbox>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,8 +1,20 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlTooltipDirective } from '@gitlab/ui';
|
import {
|
||||||
|
GlTooltipDirective,
|
||||||
|
GlFormRadio,
|
||||||
|
GlFormRadioGroup,
|
||||||
|
GlFormGroup,
|
||||||
|
GlFormInput,
|
||||||
|
} from '@gitlab/ui';
|
||||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
GlFormRadio,
|
||||||
|
GlFormRadioGroup,
|
||||||
|
GlFormGroup,
|
||||||
|
GlFormInput,
|
||||||
|
},
|
||||||
directives: {
|
directives: {
|
||||||
GlTooltip: GlTooltipDirective,
|
GlTooltip: GlTooltipDirective,
|
||||||
},
|
},
|
||||||
|
@ -51,35 +63,42 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<fieldset>
|
<fieldset class="gl-mb-2">
|
||||||
<label
|
<gl-form-radio-group
|
||||||
v-gl-tooltip="tooltipTitle"
|
v-gl-tooltip="tooltipTitle"
|
||||||
|
:checked="commitAction"
|
||||||
:class="{
|
:class="{
|
||||||
'is-disabled': disabled,
|
'is-disabled': disabled,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<input
|
<gl-form-radio
|
||||||
:value="value"
|
:value="value"
|
||||||
:checked="commitAction === value"
|
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
type="radio"
|
|
||||||
name="commit-action"
|
name="commit-action"
|
||||||
data-qa-selector="commit_type_radio"
|
data-qa-selector="commit_type_radio"
|
||||||
@change="updateCommitAction($event.target.value)"
|
@change="updateCommitAction(value)"
|
||||||
/>
|
>
|
||||||
<span class="gl-ml-3">
|
<span v-if="label" class="ide-option-label">
|
||||||
<span v-if="label" class="ide-option-label"> {{ label }} </span> <slot v-else></slot>
|
{{ label }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
<slot v-else></slot>
|
||||||
<div v-if="commitAction === value && showInput" class="ide-commit-new-branch">
|
</gl-form-radio>
|
||||||
<input
|
</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"
|
:placeholder="placeholderBranchName"
|
||||||
:value="newBranchName"
|
:value="newBranchName"
|
||||||
|
:disabled="disabled"
|
||||||
data-testid="ide-new-branch-name"
|
data-testid="ide-new-branch-name"
|
||||||
type="text"
|
class="gl-font-monospace"
|
||||||
class="form-control monospace"
|
@input="updateBranchName($event)"
|
||||||
@input="updateBranchName($event.target.value)"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</gl-form-group>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import RegistrationDropdown from '../components/registration/registration_dropdo
|
||||||
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
|
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
|
||||||
import RunnerBulkDelete from '../components/runner_bulk_delete.vue';
|
import RunnerBulkDelete from '../components/runner_bulk_delete.vue';
|
||||||
import RunnerList from '../components/runner_list.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 RunnerName from '../components/runner_name.vue';
|
||||||
import RunnerStats from '../components/stat/runner_stats.vue';
|
import RunnerStats from '../components/stat/runner_stats.vue';
|
||||||
import RunnerPagination from '../components/runner_pagination.vue';
|
import RunnerPagination from '../components/runner_pagination.vue';
|
||||||
|
@ -35,6 +36,7 @@ import {
|
||||||
fromUrlQueryToSearch,
|
fromUrlQueryToSearch,
|
||||||
fromSearchToUrl,
|
fromSearchToUrl,
|
||||||
fromSearchToVariables,
|
fromSearchToVariables,
|
||||||
|
isSearchFiltered,
|
||||||
} from '../runner_search_utils';
|
} from '../runner_search_utils';
|
||||||
import { captureException } from '../sentry_utils';
|
import { captureException } from '../sentry_utils';
|
||||||
|
|
||||||
|
@ -91,6 +93,7 @@ export default {
|
||||||
RunnerFilteredSearchBar,
|
RunnerFilteredSearchBar,
|
||||||
RunnerBulkDelete,
|
RunnerBulkDelete,
|
||||||
RunnerList,
|
RunnerList,
|
||||||
|
RunnerListEmptyState,
|
||||||
RunnerName,
|
RunnerName,
|
||||||
RunnerStats,
|
RunnerStats,
|
||||||
RunnerPagination,
|
RunnerPagination,
|
||||||
|
@ -98,7 +101,7 @@ export default {
|
||||||
RunnerActionsCell,
|
RunnerActionsCell,
|
||||||
},
|
},
|
||||||
mixins: [glFeatureFlagMixin()],
|
mixins: [glFeatureFlagMixin()],
|
||||||
inject: ['localMutations'],
|
inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath', 'localMutations'],
|
||||||
props: {
|
props: {
|
||||||
registrationToken: {
|
registrationToken: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -190,6 +193,9 @@ export default {
|
||||||
// Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/353981
|
// Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/353981
|
||||||
return this.glFeatures.adminRunnersBulkDelete;
|
return this.glFeatures.adminRunnersBulkDelete;
|
||||||
},
|
},
|
||||||
|
isSearchFiltered() {
|
||||||
|
return isSearchFiltered(this.search);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
search: {
|
search: {
|
||||||
|
@ -298,9 +304,13 @@ export default {
|
||||||
:stale-runners-count="staleRunnersTotal"
|
:stale-runners-count="staleRunnersTotal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="noRunnersFound" class="gl-text-center gl-p-5">
|
<runner-list-empty-state
|
||||||
{{ __('No runners found') }}
|
v-if="noRunnersFound"
|
||||||
</div>
|
:registration-token="registrationToken"
|
||||||
|
:is-search-filtered="isSearchFiltered"
|
||||||
|
:svg-path="emptyStateSvgPath"
|
||||||
|
:filtered-svg-path="emptyStateFilteredSvgPath"
|
||||||
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<runner-bulk-delete v-if="isBulkDeleteEnabled" />
|
<runner-bulk-delete v-if="isBulkDeleteEnabled" />
|
||||||
<runner-list
|
<runner-list
|
||||||
|
|
|
@ -34,6 +34,8 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
|
||||||
registrationToken,
|
registrationToken,
|
||||||
onlineContactTimeoutSecs,
|
onlineContactTimeoutSecs,
|
||||||
staleTimeoutSecs,
|
staleTimeoutSecs,
|
||||||
|
emptyStateSvgPath,
|
||||||
|
emptyStateFilteredSvgPath,
|
||||||
} = el.dataset;
|
} = el.dataset;
|
||||||
|
|
||||||
const { cacheConfig, typeDefs, localMutations } = createLocalState();
|
const { cacheConfig, typeDefs, localMutations } = createLocalState();
|
||||||
|
@ -50,6 +52,8 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
|
||||||
localMutations,
|
localMutations,
|
||||||
onlineContactTimeoutSecs,
|
onlineContactTimeoutSecs,
|
||||||
staleTimeoutSecs,
|
staleTimeoutSecs,
|
||||||
|
emptyStateSvgPath,
|
||||||
|
emptyStateFilteredSvgPath,
|
||||||
},
|
},
|
||||||
render(h) {
|
render(h) {
|
||||||
return h(AdminRunnersApp, {
|
return h(AdminRunnersApp, {
|
||||||
|
|
|
@ -7,6 +7,8 @@ import RunnerPausedBadge from '../runner_paused_badge.vue';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
RunnerStatusBadge,
|
RunnerStatusBadge,
|
||||||
|
RunnerUpgradeStatusBadge: () =>
|
||||||
|
import('ee_component/runner/components/runner_upgrade_status_badge.vue'),
|
||||||
RunnerPausedBadge,
|
RunnerPausedBadge,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
|
@ -33,6 +35,11 @@ export default {
|
||||||
size="sm"
|
size="sm"
|
||||||
class="gl-display-inline-block gl-max-w-full gl-text-truncate"
|
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
|
<runner-paused-badge
|
||||||
v-if="paused"
|
v-if="paused"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
|
@ -12,7 +12,7 @@ import RunnerStatusCell from './cells/runner_status_cell.vue';
|
||||||
import RunnerTags from './runner_tags.vue';
|
import RunnerTags from './runner_tags.vue';
|
||||||
|
|
||||||
const defaultFields = [
|
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: 'summary', label: s__('Runners|Runner'), thClasses: ['gl-lg-w-25p'] }),
|
||||||
tableField({ key: 'version', label: __('Version') }),
|
tableField({ key: 'version', label: __('Version') }),
|
||||||
tableField({ key: 'jobCount', label: __('Jobs') }),
|
tableField({ key: 'jobCount', label: __('Jobs') }),
|
||||||
|
|
|
@ -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>
|
|
@ -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"
|
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
|
||||||
|
|
||||||
query getRunners(
|
query getRunners(
|
||||||
|
|
|
@ -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"
|
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
|
||||||
|
|
||||||
query getGroupRunners(
|
query getGroupRunners(
|
||||||
|
|
|
@ -1,20 +1,5 @@
|
||||||
|
#import "./list_item_shared.fragment.graphql"
|
||||||
|
|
||||||
fragment ListItem on CiRunner {
|
fragment ListItem on CiRunner {
|
||||||
__typename
|
...ListItemShared
|
||||||
id
|
|
||||||
description
|
|
||||||
runnerType
|
|
||||||
shortSha
|
|
||||||
version
|
|
||||||
revision
|
|
||||||
ipAddress
|
|
||||||
active
|
|
||||||
locked
|
|
||||||
jobCount
|
|
||||||
tagList
|
|
||||||
contactedAt
|
|
||||||
status(legacyMode: null)
|
|
||||||
userPermissions {
|
|
||||||
updateRunner
|
|
||||||
deleteRunner
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -8,6 +8,7 @@ import { fetchPolicies } from '~/lib/graphql';
|
||||||
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
|
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
|
||||||
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
|
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
|
||||||
import RunnerList from '../components/runner_list.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 RunnerName from '../components/runner_name.vue';
|
||||||
import RunnerStats from '../components/stat/runner_stats.vue';
|
import RunnerStats from '../components/stat/runner_stats.vue';
|
||||||
import RunnerPagination from '../components/runner_pagination.vue';
|
import RunnerPagination from '../components/runner_pagination.vue';
|
||||||
|
@ -31,6 +32,7 @@ import {
|
||||||
fromUrlQueryToSearch,
|
fromUrlQueryToSearch,
|
||||||
fromSearchToUrl,
|
fromSearchToUrl,
|
||||||
fromSearchToVariables,
|
fromSearchToVariables,
|
||||||
|
isSearchFiltered,
|
||||||
} from '../runner_search_utils';
|
} from '../runner_search_utils';
|
||||||
import { captureException } from '../sentry_utils';
|
import { captureException } from '../sentry_utils';
|
||||||
|
|
||||||
|
@ -86,12 +88,14 @@ export default {
|
||||||
RegistrationDropdown,
|
RegistrationDropdown,
|
||||||
RunnerFilteredSearchBar,
|
RunnerFilteredSearchBar,
|
||||||
RunnerList,
|
RunnerList,
|
||||||
|
RunnerListEmptyState,
|
||||||
RunnerName,
|
RunnerName,
|
||||||
RunnerStats,
|
RunnerStats,
|
||||||
RunnerPagination,
|
RunnerPagination,
|
||||||
RunnerTypeTabs,
|
RunnerTypeTabs,
|
||||||
RunnerActionsCell,
|
RunnerActionsCell,
|
||||||
},
|
},
|
||||||
|
inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'],
|
||||||
props: {
|
props: {
|
||||||
registrationToken: {
|
registrationToken: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -196,6 +200,9 @@ export default {
|
||||||
filteredSearchNamespace() {
|
filteredSearchNamespace() {
|
||||||
return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`;
|
return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`;
|
||||||
},
|
},
|
||||||
|
isSearchFiltered() {
|
||||||
|
return isSearchFiltered(this.search);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
search: {
|
search: {
|
||||||
|
@ -299,9 +306,13 @@ export default {
|
||||||
:stale-runners-count="staleRunnersTotal"
|
:stale-runners-count="staleRunnersTotal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="noRunnersFound" class="gl-text-center gl-p-5">
|
<runner-list-empty-state
|
||||||
{{ __('No runners found') }}
|
v-if="noRunnersFound"
|
||||||
</div>
|
:registration-token="registrationToken"
|
||||||
|
:is-search-filtered="isSearchFiltered"
|
||||||
|
:svg-path="emptyStateSvgPath"
|
||||||
|
:filtered-svg-path="emptyStateFilteredSvgPath"
|
||||||
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<runner-list :runners="runners.items" :loading="runnersLoading">
|
<runner-list :runners="runners.items" :loading="runnersLoading">
|
||||||
<template #runner-name="{ runner }">
|
<template #runner-name="{ runner }">
|
||||||
|
|
|
@ -22,6 +22,8 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
|
||||||
groupRunnersLimitedCount,
|
groupRunnersLimitedCount,
|
||||||
onlineContactTimeoutSecs,
|
onlineContactTimeoutSecs,
|
||||||
staleTimeoutSecs,
|
staleTimeoutSecs,
|
||||||
|
emptyStateSvgPath,
|
||||||
|
emptyStateFilteredSvgPath,
|
||||||
} = el.dataset;
|
} = el.dataset;
|
||||||
|
|
||||||
const apolloProvider = new VueApollo({
|
const apolloProvider = new VueApollo({
|
||||||
|
@ -36,6 +38,8 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
|
||||||
groupId,
|
groupId,
|
||||||
onlineContactTimeoutSecs: parseInt(onlineContactTimeoutSecs, 10),
|
onlineContactTimeoutSecs: parseInt(onlineContactTimeoutSecs, 10),
|
||||||
staleTimeoutSecs: parseInt(staleTimeoutSecs, 10),
|
staleTimeoutSecs: parseInt(staleTimeoutSecs, 10),
|
||||||
|
emptyStateSvgPath,
|
||||||
|
emptyStateFilteredSvgPath,
|
||||||
},
|
},
|
||||||
render(h) {
|
render(h) {
|
||||||
return h(GroupRunnersApp, {
|
return h(GroupRunnersApp, {
|
||||||
|
|
|
@ -236,3 +236,17 @@ export const fromSearchToVariables = ({
|
||||||
...paginationVariables,
|
...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),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -302,9 +302,11 @@ export default {
|
||||||
<span v-else class="gl-display-flex gl-align-items-center gl-line-height-normal">
|
<span v-else class="gl-display-flex gl-align-items-center gl-line-height-normal">
|
||||||
{{ __('None') }} -
|
{{ __('None') }} -
|
||||||
<gl-button
|
<gl-button
|
||||||
class="gl-ml-2"
|
class="gl-ml-2 gl-reset-color!"
|
||||||
href="#"
|
href="#"
|
||||||
|
category="tertiary"
|
||||||
variant="link"
|
variant="link"
|
||||||
|
size="small"
|
||||||
data-testid="unassigned-users"
|
data-testid="unassigned-users"
|
||||||
@click="updateAlertAssignees(currentUser)"
|
@click="updateAlertAssignees(currentUser)"
|
||||||
>
|
>
|
||||||
|
|
|
@ -247,8 +247,8 @@
|
||||||
z-index: 600;
|
z-index: 600;
|
||||||
width: $contextual-sidebar-width;
|
width: $contextual-sidebar-width;
|
||||||
top: $header-height;
|
top: $header-height;
|
||||||
@include gl-bg-gray-10;
|
background-color: $contextual-sidebar-bg-color;
|
||||||
border-right: 1px solid $gray-50;
|
border-right: 1px solid $contextual-sidebar-border-color;
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
|
|
||||||
&.sidebar-collapsed-desktop {
|
&.sidebar-collapsed-desktop {
|
||||||
|
@ -411,7 +411,7 @@
|
||||||
.toggle-sidebar-button,
|
.toggle-sidebar-button,
|
||||||
.close-nav-button {
|
.close-nav-button {
|
||||||
@include side-panel-toggle;
|
@include side-panel-toggle;
|
||||||
@include gl-bg-gray-10;
|
background-color: $contextual-sidebar-bg-color;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: #{$contextual-sidebar-width - 1px};
|
width: #{$contextual-sidebar-width - 1px};
|
||||||
|
|
|
@ -357,6 +357,8 @@ $border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor);
|
||||||
/*
|
/*
|
||||||
* UI elements
|
* UI elements
|
||||||
*/
|
*/
|
||||||
|
$contextual-sidebar-bg-color: #f5f5f5;
|
||||||
|
$contextual-sidebar-border-color: #e9e9e9;
|
||||||
$border-color: $gray-100;
|
$border-color: $gray-100;
|
||||||
$shadow-color: $t-gray-a-08;
|
$shadow-color: $t-gray-a-08;
|
||||||
$well-expand-item: #e8f2f7 !default;
|
$well-expand-item: #e8f2f7 !default;
|
||||||
|
|
|
@ -563,24 +563,11 @@ $ide-commit-header-height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ide-commit-options {
|
.ide-commit-options {
|
||||||
label {
|
.is-disabled {
|
||||||
font-weight: normal;
|
.ide-option-label {
|
||||||
|
text-decoration: line-through;
|
||||||
&.is-disabled {
|
|
||||||
.ide-option-label {
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-text.text-muted {
|
|
||||||
margin-top: 0;
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ide-commit-new-branch {
|
|
||||||
margin-left: 25px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ide-sidebar-link {
|
.ide-sidebar-link {
|
||||||
|
|
|
@ -1034,8 +1034,8 @@ input {
|
||||||
z-index: 600;
|
z-index: 600;
|
||||||
width: 256px;
|
width: 256px;
|
||||||
top: var(--header-height, 48px);
|
top: var(--header-height, 48px);
|
||||||
background-color: #1f1f1f;
|
background-color: #f5f5f5;
|
||||||
border-right: 1px solid #303030;
|
border-right: 1px solid #e9e9e9;
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
}
|
}
|
||||||
.nav-sidebar.sidebar-collapsed-desktop {
|
.nav-sidebar.sidebar-collapsed-desktop {
|
||||||
|
@ -1402,7 +1402,7 @@ input {
|
||||||
color: #999;
|
color: #999;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #1f1f1f;
|
background-color: #f5f5f5;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 255px;
|
width: 255px;
|
||||||
|
@ -1792,6 +1792,7 @@ body.gl-dark {
|
||||||
.toggle-sidebar-button,
|
.toggle-sidebar-button,
|
||||||
.close-nav-button {
|
.close-nav-button {
|
||||||
background-color: #262626;
|
background-color: #262626;
|
||||||
|
border-right: 1px solid #303030;
|
||||||
}
|
}
|
||||||
.nav-sidebar li a {
|
.nav-sidebar li a {
|
||||||
color: var(--gray-600);
|
color: var(--gray-600);
|
||||||
|
|
|
@ -1019,8 +1019,8 @@ input {
|
||||||
z-index: 600;
|
z-index: 600;
|
||||||
width: 256px;
|
width: 256px;
|
||||||
top: var(--header-height, 48px);
|
top: var(--header-height, 48px);
|
||||||
background-color: #fafafa;
|
background-color: #f5f5f5;
|
||||||
border-right: 1px solid #f0f0f0;
|
border-right: 1px solid #e9e9e9;
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
}
|
}
|
||||||
.nav-sidebar.sidebar-collapsed-desktop {
|
.nav-sidebar.sidebar-collapsed-desktop {
|
||||||
|
@ -1387,7 +1387,7 @@ input {
|
||||||
color: #666;
|
color: #666;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #fafafa;
|
background-color: #f5f5f5;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 255px;
|
width: 255px;
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
.toggle-sidebar-button,
|
.toggle-sidebar-button,
|
||||||
.close-nav-button {
|
.close-nav-button {
|
||||||
background-color: darken($gray-50, 4%);
|
background-color: darken($gray-50, 4%);
|
||||||
|
border-right: 1px solid $gray-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-sidebar {
|
.nav-sidebar {
|
||||||
|
|
|
@ -41,3 +41,5 @@ class Groups::RunnersController < Groups::ApplicationController
|
||||||
params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
|
params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Groups::RunnersController.prepend_mod
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
# current_user - user performing the action. Must have the correct permission level for the group.
|
# current_user - user performing the action. Must have the correct permission level for the group.
|
||||||
# params:
|
# params:
|
||||||
# group: Group, required
|
# group: Group, required
|
||||||
|
# search: String, optional
|
||||||
|
# state: CustomerRelations::ContactStateEnum, optional
|
||||||
module Crm
|
module Crm
|
||||||
class ContactsFinder
|
class ContactsFinder
|
||||||
include Gitlab::Allowable
|
include Gitlab::Allowable
|
||||||
|
@ -21,7 +23,10 @@ module Crm
|
||||||
def execute
|
def execute
|
||||||
return CustomerRelations::Contact.none unless root_group
|
return CustomerRelations::Contact.none unless root_group
|
||||||
|
|
||||||
root_group.contacts
|
contacts = root_group.contacts
|
||||||
|
contacts = by_state(contacts)
|
||||||
|
contacts = by_search(contacts)
|
||||||
|
contacts.sort_by_name
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -35,5 +40,25 @@ module Crm
|
||||||
group
|
group
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def by_search(contacts)
|
||||||
|
return contacts unless search?
|
||||||
|
|
||||||
|
contacts.search(params[:search])
|
||||||
|
end
|
||||||
|
|
||||||
|
def by_state(contacts)
|
||||||
|
return contacts unless state?
|
||||||
|
|
||||||
|
contacts.search_by_state(params[:state])
|
||||||
|
end
|
||||||
|
|
||||||
|
def search?
|
||||||
|
params[:search].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def state?
|
||||||
|
params[:state].present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Finder for retrieving organizations scoped to a group
|
||||||
|
#
|
||||||
|
# Arguments:
|
||||||
|
# current_user - user performing the action. Must have the correct permission level for the group.
|
||||||
|
# params:
|
||||||
|
# group: Group, required
|
||||||
|
# search: String, optional
|
||||||
|
# state: CustomerRelations::OrganizationStateEnum, optional
|
||||||
|
module Crm
|
||||||
|
class OrganizationsFinder
|
||||||
|
include Gitlab::Allowable
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
|
attr_reader :params, :current_user
|
||||||
|
|
||||||
|
def initialize(current_user, params = {})
|
||||||
|
@current_user = current_user
|
||||||
|
@params = params
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute
|
||||||
|
return CustomerRelations::Organization.none unless root_group
|
||||||
|
|
||||||
|
organizations = root_group.organizations
|
||||||
|
organizations = by_search(organizations)
|
||||||
|
organizations = by_state(organizations)
|
||||||
|
organizations.sort_by_name
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def root_group
|
||||||
|
strong_memoize(:root_group) do
|
||||||
|
group = params[:group]&.root_ancestor
|
||||||
|
|
||||||
|
next unless can?(@current_user, :read_crm_organization, group)
|
||||||
|
|
||||||
|
group
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def by_search(organizations)
|
||||||
|
return organizations unless search?
|
||||||
|
|
||||||
|
organizations.search(params[:search])
|
||||||
|
end
|
||||||
|
|
||||||
|
def by_state(organizations)
|
||||||
|
return organizations unless state?
|
||||||
|
|
||||||
|
organizations.search_by_state(params[:state])
|
||||||
|
end
|
||||||
|
|
||||||
|
def search?
|
||||||
|
params[:search].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def state?
|
||||||
|
params[:state].present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -18,11 +18,9 @@ module ResolvesGroups
|
||||||
|
|
||||||
def preloads
|
def preloads
|
||||||
{
|
{
|
||||||
contacts: [:contacts],
|
|
||||||
container_repositories_count: [:container_repositories],
|
container_repositories_count: [:container_repositories],
|
||||||
custom_emoji: [:custom_emoji],
|
custom_emoji: [:custom_emoji],
|
||||||
full_path: [:route],
|
full_path: [:route],
|
||||||
organizations: [:organizations],
|
|
||||||
path: [:route],
|
path: [:route],
|
||||||
dependency_proxy_blob_count: [:dependency_proxy_blobs],
|
dependency_proxy_blob_count: [:dependency_proxy_blobs],
|
||||||
dependency_proxy_blobs: [:dependency_proxy_blobs],
|
dependency_proxy_blobs: [:dependency_proxy_blobs],
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Resolvers
|
||||||
|
module Crm
|
||||||
|
class ContactsResolver < BaseResolver
|
||||||
|
include Gitlab::Graphql::Authorize::AuthorizeResource
|
||||||
|
|
||||||
|
authorize :read_crm_contact
|
||||||
|
|
||||||
|
type Types::CustomerRelations::ContactType, null: true
|
||||||
|
|
||||||
|
argument :search, GraphQL::Types::String,
|
||||||
|
required: false,
|
||||||
|
description: 'Search term to find contacts with.'
|
||||||
|
|
||||||
|
argument :state, Types::CustomerRelations::ContactStateEnum,
|
||||||
|
required: false,
|
||||||
|
description: 'State of the contacts to search for.'
|
||||||
|
|
||||||
|
def resolve(**args)
|
||||||
|
::Crm::ContactsFinder.new(current_user, { group: group }.merge(args)).execute
|
||||||
|
end
|
||||||
|
|
||||||
|
def group
|
||||||
|
object.respond_to?(:sync) ? object.sync : object
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Resolvers
|
||||||
|
module Crm
|
||||||
|
class OrganizationsResolver < BaseResolver
|
||||||
|
include Gitlab::Graphql::Authorize::AuthorizeResource
|
||||||
|
|
||||||
|
authorize :read_crm_organization
|
||||||
|
|
||||||
|
type Types::CustomerRelations::OrganizationType, null: true
|
||||||
|
|
||||||
|
argument :search, GraphQL::Types::String,
|
||||||
|
required: false,
|
||||||
|
description: 'Search term used to find organizations with.'
|
||||||
|
|
||||||
|
argument :state, Types::CustomerRelations::OrganizationStateEnum,
|
||||||
|
required: false,
|
||||||
|
description: 'State of the organization to search for.'
|
||||||
|
|
||||||
|
def resolve(**args)
|
||||||
|
::Crm::OrganizationsFinder.new(current_user, { group: group }.merge(args)).execute
|
||||||
|
end
|
||||||
|
|
||||||
|
def group
|
||||||
|
object.respond_to?(:sync) ? object.sync : object
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Types
|
||||||
|
module CustomerRelations
|
||||||
|
class ContactStateEnum < BaseEnum
|
||||||
|
graphql_name 'CustomerRelationsContactState'
|
||||||
|
|
||||||
|
value 'active',
|
||||||
|
description: "Active contact.",
|
||||||
|
value: :active
|
||||||
|
|
||||||
|
value 'inactive',
|
||||||
|
description: "Inactive contact.",
|
||||||
|
value: :inactive
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Types
|
||||||
|
module CustomerRelations
|
||||||
|
class OrganizationStateEnum < BaseEnum
|
||||||
|
graphql_name 'CustomerRelationsOrganizationState'
|
||||||
|
|
||||||
|
value 'active',
|
||||||
|
description: "Active organization.",
|
||||||
|
value: :active
|
||||||
|
|
||||||
|
value 'inactive',
|
||||||
|
description: "Inactive organization.",
|
||||||
|
value: :inactive
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -201,11 +201,13 @@ module Types
|
||||||
|
|
||||||
field :organizations, Types::CustomerRelations::OrganizationType.connection_type,
|
field :organizations, Types::CustomerRelations::OrganizationType.connection_type,
|
||||||
null: true,
|
null: true,
|
||||||
description: "Find organizations of this group."
|
description: "Find organizations of this group.",
|
||||||
|
resolver: Resolvers::Crm::OrganizationsResolver
|
||||||
|
|
||||||
field :contacts, Types::CustomerRelations::ContactType.connection_type,
|
field :contacts, Types::CustomerRelations::ContactType.connection_type,
|
||||||
null: true,
|
null: true,
|
||||||
description: "Find contacts of this group."
|
description: "Find contacts of this group.",
|
||||||
|
resolver: Resolvers::Crm::ContactsResolver
|
||||||
|
|
||||||
field :work_item_types, Types::WorkItems::TypeType.connection_type,
|
field :work_item_types, Types::WorkItems::TypeType.connection_type,
|
||||||
resolver: Resolvers::WorkItems::TypesResolver,
|
resolver: Resolvers::WorkItems::TypesResolver,
|
||||||
|
|
|
@ -65,7 +65,9 @@ module Ci
|
||||||
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
|
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
|
||||||
registration_token: Gitlab::CurrentSettings.runners_registration_token,
|
registration_token: Gitlab::CurrentSettings.runners_registration_token,
|
||||||
online_contact_timeout_secs: ::Ci::Runner::ONLINE_CONTACT_TIMEOUT.to_i,
|
online_contact_timeout_secs: ::Ci::Runner::ONLINE_CONTACT_TIMEOUT.to_i,
|
||||||
stale_timeout_secs: ::Ci::Runner::STALE_TIMEOUT.to_i
|
stale_timeout_secs: ::Ci::Runner::STALE_TIMEOUT.to_i,
|
||||||
|
empty_state_svg_path: image_path('illustrations/pipelines_empty.svg'),
|
||||||
|
empty_state_filtered_svg_path: image_path('illustrations/magnifying-glass.svg')
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -87,7 +89,9 @@ module Ci
|
||||||
group_full_path: group.full_path,
|
group_full_path: group.full_path,
|
||||||
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
|
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
|
||||||
online_contact_timeout_secs: ::Ci::Runner::ONLINE_CONTACT_TIMEOUT.to_i,
|
online_contact_timeout_secs: ::Ci::Runner::ONLINE_CONTACT_TIMEOUT.to_i,
|
||||||
stale_timeout_secs: ::Ci::Runner::STALE_TIMEOUT.to_i
|
stale_timeout_secs: ::Ci::Runner::STALE_TIMEOUT.to_i,
|
||||||
|
empty_state_svg_path: image_path('illustrations/pipelines_empty.svg'),
|
||||||
|
empty_state_filtered_svg_path: image_path('illustrations/magnifying-glass.svg')
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -808,6 +808,7 @@ module Ci
|
||||||
|
|
||||||
def execute_hooks
|
def execute_hooks
|
||||||
return unless project
|
return unless project
|
||||||
|
return if user&.blocked?
|
||||||
|
|
||||||
project.execute_hooks(build_data.dup, :job_hooks) if project.has_active_hooks?(:job_hooks)
|
project.execute_hooks(build_data.dup, :job_hooks) if project.has_active_hooks?(:job_hooks)
|
||||||
project.execute_integrations(build_data.dup, :job_hooks) if project.has_active_integrations?(:job_hooks)
|
project.execute_integrations(build_data.dup, :job_hooks) if project.has_active_integrations?(:job_hooks)
|
||||||
|
|
|
@ -124,10 +124,10 @@ module Ci
|
||||||
# We will start using this column once we complete https://gitlab.com/gitlab-org/gitlab/-/issues/285597
|
# We will start using this column once we complete https://gitlab.com/gitlab-org/gitlab/-/issues/285597
|
||||||
ignore_column :original_filename, remove_with: '14.7', remove_after: '2022-11-22'
|
ignore_column :original_filename, remove_with: '14.7', remove_after: '2022-11-22'
|
||||||
|
|
||||||
mount_file_store_uploader JobArtifactUploader
|
mount_file_store_uploader JobArtifactUploader, skip_store_file: true
|
||||||
|
|
||||||
skip_callback :save, :after, :store_file!, if: :store_after_commit?
|
after_save :store_file_in_transaction!, unless: :store_after_commit?
|
||||||
after_commit :store_file_after_commit!, on: [:create, :update], if: :store_after_commit?
|
after_commit :store_file_after_transaction!, on: [:create, :update], if: :store_after_commit?
|
||||||
|
|
||||||
validates :file_format, presence: true, unless: :trace?, on: :create
|
validates :file_format, presence: true, unless: :trace?, on: :create
|
||||||
validate :validate_file_format!, unless: :trace?, on: :create
|
validate :validate_file_format!, unless: :trace?, on: :create
|
||||||
|
@ -362,11 +362,24 @@ module Ci
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def store_file_after_commit!
|
def store_file_in_transaction!
|
||||||
return unless previous_changes.key?(:file)
|
store_file_now! if saved_change_to_file?
|
||||||
|
|
||||||
store_file!
|
file_stored_in_transaction_hooks
|
||||||
update_file_store
|
end
|
||||||
|
|
||||||
|
def store_file_after_transaction!
|
||||||
|
store_file_now! if previous_changes.key?(:file)
|
||||||
|
|
||||||
|
file_stored_after_transaction_hooks
|
||||||
|
end
|
||||||
|
|
||||||
|
# method overriden in EE
|
||||||
|
def file_stored_after_transaction_hooks
|
||||||
|
end
|
||||||
|
|
||||||
|
# method overriden in EE
|
||||||
|
def file_stored_in_transaction_hooks
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_size
|
def set_size
|
||||||
|
|
|
@ -240,7 +240,9 @@ module Ci
|
||||||
next if transition.loopback?
|
next if transition.loopback?
|
||||||
|
|
||||||
pipeline.run_after_commit do
|
pipeline.run_after_commit do
|
||||||
PipelineHooksWorker.perform_async(pipeline.id)
|
unless pipeline.user&.blocked?
|
||||||
|
PipelineHooksWorker.perform_async(pipeline.id)
|
||||||
|
end
|
||||||
|
|
||||||
if pipeline.project.jira_subscription_exists?
|
if pipeline.project.jira_subscription_exists?
|
||||||
# Passing the seq-id ensures this is idempotent
|
# Passing the seq-id ensures this is idempotent
|
||||||
|
@ -297,7 +299,12 @@ module Ci
|
||||||
ref_status = pipeline.ci_ref&.update_status_by!(pipeline)
|
ref_status = pipeline.ci_ref&.update_status_by!(pipeline)
|
||||||
|
|
||||||
pipeline.run_after_commit do
|
pipeline.run_after_commit do
|
||||||
PipelineNotificationWorker.perform_async(pipeline.id, ref_status: ref_status)
|
# We don't send notifications for a pipeline dropped due to the
|
||||||
|
# user been blocked.
|
||||||
|
unless pipeline.user&.blocked?
|
||||||
|
PipelineNotificationWorker
|
||||||
|
.perform_async(pipeline.id, ref_status: ref_status)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,16 @@ module FileStoreMounter
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def mount_file_store_uploader(uploader)
|
# When `skip_store_file: true` is used, the model MUST explicitly call `store_file_now!`
|
||||||
|
def mount_file_store_uploader(uploader, skip_store_file: false)
|
||||||
mount_uploader(:file, uploader)
|
mount_uploader(:file, uploader)
|
||||||
|
|
||||||
|
if skip_store_file
|
||||||
|
skip_callback :save, :after, :store_file!
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
# This hook is a no-op when the file is uploaded after_commit
|
# This hook is a no-op when the file is uploaded after_commit
|
||||||
after_save :update_file_store, if: :saved_change_to_file?
|
after_save :update_file_store, if: :saved_change_to_file?
|
||||||
end
|
end
|
||||||
|
@ -16,4 +23,9 @@ module FileStoreMounter
|
||||||
# The file.object_store is set during `uploader.store!` and `uploader.migrate!`
|
# The file.object_store is set during `uploader.store!` and `uploader.migrate!`
|
||||||
update_column(:file_store, file.object_store)
|
update_column(:file_store, file.object_store)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def store_file_now!
|
||||||
|
store_file!
|
||||||
|
update_file_store
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class CustomerRelations::Contact < ApplicationRecord
|
class CustomerRelations::Contact < ApplicationRecord
|
||||||
|
include Gitlab::SQL::Pattern
|
||||||
|
include Sortable
|
||||||
include StripAttribute
|
include StripAttribute
|
||||||
|
|
||||||
self.table_name = "customer_relations_contacts"
|
self.table_name = "customer_relations_contacts"
|
||||||
|
@ -39,6 +41,25 @@ class CustomerRelations::Contact < ApplicationRecord
|
||||||
']'
|
']'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Searches for contacts with a matching first name, last name, email or description.
|
||||||
|
#
|
||||||
|
# This method uses ILIKE on PostgreSQL
|
||||||
|
#
|
||||||
|
# query - The search query as a String
|
||||||
|
#
|
||||||
|
# Returns an ActiveRecord::Relation.
|
||||||
|
def self.search(query)
|
||||||
|
fuzzy_search(query, [:first_name, :last_name, :email, :description], use_minimum_char_limit: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.search_by_state(state)
|
||||||
|
where(state: state)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.sort_by_name
|
||||||
|
order("last_name ASC, first_name ASC")
|
||||||
|
end
|
||||||
|
|
||||||
def self.find_ids_by_emails(group, emails)
|
def self.find_ids_by_emails(group, emails)
|
||||||
raise ArgumentError, "Cannot lookup more than #{MAX_PLUCK} emails" if emails.length > MAX_PLUCK
|
raise ArgumentError, "Cannot lookup more than #{MAX_PLUCK} emails" if emails.length > MAX_PLUCK
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class CustomerRelations::Organization < ApplicationRecord
|
class CustomerRelations::Organization < ApplicationRecord
|
||||||
|
include Gitlab::SQL::Pattern
|
||||||
|
include Sortable
|
||||||
include StripAttribute
|
include StripAttribute
|
||||||
|
|
||||||
self.table_name = "customer_relations_organizations"
|
self.table_name = "customer_relations_organizations"
|
||||||
|
@ -21,6 +23,25 @@ class CustomerRelations::Organization < ApplicationRecord
|
||||||
validates :description, length: { maximum: 1024 }
|
validates :description, length: { maximum: 1024 }
|
||||||
validate :validate_root_group
|
validate :validate_root_group
|
||||||
|
|
||||||
|
# Searches for organizations with a matching name or description.
|
||||||
|
#
|
||||||
|
# This method uses ILIKE on PostgreSQL
|
||||||
|
#
|
||||||
|
# query - The search query as a String
|
||||||
|
#
|
||||||
|
# Returns an ActiveRecord::Relation.
|
||||||
|
def self.search(query)
|
||||||
|
fuzzy_search(query, [:name, :description], use_minimum_char_limit: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.search_by_state(state)
|
||||||
|
where(state: state)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.sort_by_name
|
||||||
|
order(name: :asc)
|
||||||
|
end
|
||||||
|
|
||||||
def self.find_by_name(group_id, name)
|
def self.find_by_name(group_id, name)
|
||||||
where(group: group_id)
|
where(group: group_id)
|
||||||
.where('LOWER(name) = LOWER(?)', name)
|
.where('LOWER(name) = LOWER(?)', name)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
module Projects
|
module Projects
|
||||||
class UpdatePagesService < BaseService
|
class UpdatePagesService < BaseService
|
||||||
InvalidStateError = Class.new(StandardError)
|
InvalidStateError = Class.new(StandardError)
|
||||||
|
WrongUploadedDeploymentSizeError = Class.new(StandardError)
|
||||||
BLOCK_SIZE = 32.kilobytes
|
BLOCK_SIZE = 32.kilobytes
|
||||||
PUBLIC_DIR = 'public'
|
PUBLIC_DIR = 'public'
|
||||||
|
|
||||||
|
@ -39,6 +40,9 @@ module Projects
|
||||||
end
|
end
|
||||||
rescue InvalidStateError => e
|
rescue InvalidStateError => e
|
||||||
error(e.message)
|
error(e.message)
|
||||||
|
rescue WrongUploadedDeploymentSizeError => e
|
||||||
|
error("Uploading artifacts to pages storage failed")
|
||||||
|
raise e
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
error(e.message)
|
error(e.message)
|
||||||
raise e
|
raise e
|
||||||
|
@ -80,6 +84,10 @@ module Projects
|
||||||
ci_build_id: build.id
|
ci_build_id: build.id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if deployment.size != file.size || deployment.file.size != file.size
|
||||||
|
raise(WrongUploadedDeploymentSizeError)
|
||||||
|
end
|
||||||
|
|
||||||
validate_outdated_sha!
|
validate_outdated_sha!
|
||||||
|
|
||||||
project.update_pages_deployment!(deployment)
|
project.update_pages_deployment!(deployment)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
%section.settings.js-service-desk-setting-wrapper.no-animate#js-service-desk{ class: ('expanded' if expanded), data: { qa_selector: 'service_desk_settings_content' } }
|
%section.settings.js-service-desk-setting-wrapper.no-animate#js-service-desk{ class: ('expanded' if expanded), data: { qa_selector: 'service_desk_settings_content' } }
|
||||||
.settings-header
|
.settings-header
|
||||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Service Desk')
|
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Service Desk')
|
||||||
%button.btn.gl-button.btn-default.js-settings-toggle
|
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||||
= expanded ? _('Collapse') : _('Expand')
|
= expanded ? _('Collapse') : _('Expand')
|
||||||
- link_start = "<a href='#{help_page_path('user/project/service_desk')}' target='_blank' rel='noopener noreferrer'>".html_safe
|
- link_start = "<a href='#{help_page_path('user/project/service_desk')}' target='_blank' rel='noopener noreferrer'>".html_safe
|
||||||
%p= _('Enable and disable Service Desk. Some additional configuration might be required. %{link_start}Learn more%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
|
%p= _('Enable and disable Service Desk. Some additional configuration might be required. %{link_start}Learn more%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
|
||||||
|
|
|
@ -8,5 +8,4 @@
|
||||||
- title = s_('TagsPage|Only a project maintainer or owner can delete a protected tag')
|
- title = s_('TagsPage|Only a project maintainer or owner can delete a protected tag')
|
||||||
- disabled = true
|
- disabled = true
|
||||||
|
|
||||||
%button{ type: "button", class: "js-delete-tag-button gl-button btn btn-default btn-icon has-tooltip gl-ml-3\! #{disabled ? 'disabled' : ''}", title: title, disabled: disabled, data: { path: project_tag_path(@project, tag.name), tag_name: tag.name, is_protected: protected_tag?(project, tag).to_s } }
|
= render Pajamas::ButtonComponent.new(variant: :default, icon: 'remove', button_options: { class: "js-delete-tag-button gl-ml-3\!", 'aria-label': s_('TagsPage|Delete tag'), title: title, disabled: disabled, data: { toggle: 'tooltip', container: 'body', path: project_tag_path(@project, tag.name), tag_name: tag.name, is_protected: protected_tag?(project, tag).to_s } })
|
||||||
= sprite_icon('remove', css_class: 'gl-icon')
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ class PipelineHooksWorker # rubocop:disable Scalability/IdempotentWorker
|
||||||
def perform(pipeline_id)
|
def perform(pipeline_id)
|
||||||
pipeline = Ci::Pipeline.find_by_id(pipeline_id)
|
pipeline = Ci::Pipeline.find_by_id(pipeline_id)
|
||||||
return unless pipeline
|
return unless pipeline
|
||||||
|
return if pipeline.user&.blocked?
|
||||||
|
|
||||||
Ci::Pipelines::HookService.new(pipeline).execute
|
Ci::Pipelines::HookService.new(pipeline).execute
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,6 +23,7 @@ class PipelineNotificationWorker # rubocop:disable Scalability/IdempotentWorker
|
||||||
|
|
||||||
pipeline = Ci::Pipeline.find_by_id(pipeline_id)
|
pipeline = Ci::Pipeline.find_by_id(pipeline_id)
|
||||||
return unless pipeline
|
return unless pipeline
|
||||||
|
return if pipeline.user&.blocked?
|
||||||
|
|
||||||
NotificationService.new.pipeline_finished(pipeline, ref_status: ref_status, recipients: recipients)
|
NotificationService.new.pipeline_finished(pipeline, ref_status: ref_status, recipients: recipients)
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: ci_enforce_throttle_pipelines_creation_override
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89518
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/362513
|
||||||
|
milestone: '15.1'
|
||||||
|
type: development
|
||||||
|
group: group::pipeline execution
|
||||||
|
default_enabled: false
|
|
@ -0,0 +1,31 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ScheduleSetLegacyOpenSourceLicenseAvailableForNonPublicProjects < Gitlab::Database::Migration[2.0]
|
||||||
|
MIGRATION = 'SetLegacyOpenSourceLicenseAvailableForNonPublicProjects'
|
||||||
|
INTERVAL = 2.minutes
|
||||||
|
BATCH_SIZE = 5_000
|
||||||
|
SUB_BATCH_SIZE = 200
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||||
|
|
||||||
|
def up
|
||||||
|
return unless Gitlab.com?
|
||||||
|
|
||||||
|
queue_batched_background_migration(
|
||||||
|
MIGRATION,
|
||||||
|
:projects,
|
||||||
|
:id,
|
||||||
|
job_interval: INTERVAL,
|
||||||
|
batch_size: BATCH_SIZE,
|
||||||
|
sub_batch_size: SUB_BATCH_SIZE
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
return unless Gitlab.com?
|
||||||
|
|
||||||
|
delete_batched_background_migration(MIGRATION, :projects, :id, [])
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1 @@
|
||||||
|
5055a0f5fd7125d353654be2425c881afa42a3b09eb0ab34dd0929b3440aa643
|
|
@ -11519,7 +11519,6 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
||||||
| <a id="groupallowstalerunnerpruning"></a>`allowStaleRunnerPruning` | [`Boolean!`](#boolean) | Indicates whether to regularly prune stale group runners. Defaults to false. |
|
| <a id="groupallowstalerunnerpruning"></a>`allowStaleRunnerPruning` | [`Boolean!`](#boolean) | Indicates whether to regularly prune stale group runners. Defaults to false. |
|
||||||
| <a id="groupautodevopsenabled"></a>`autoDevopsEnabled` | [`Boolean`](#boolean) | Indicates whether Auto DevOps is enabled for all projects within this group. |
|
| <a id="groupautodevopsenabled"></a>`autoDevopsEnabled` | [`Boolean`](#boolean) | Indicates whether Auto DevOps is enabled for all projects within this group. |
|
||||||
| <a id="groupavatarurl"></a>`avatarUrl` | [`String`](#string) | Avatar URL of the group. |
|
| <a id="groupavatarurl"></a>`avatarUrl` | [`String`](#string) | Avatar URL of the group. |
|
||||||
| <a id="groupcontacts"></a>`contacts` | [`CustomerRelationsContactConnection`](#customerrelationscontactconnection) | Find contacts of this group. (see [Connections](#connections)) |
|
|
||||||
| <a id="groupcontainerrepositoriescount"></a>`containerRepositoriesCount` | [`Int!`](#int) | Number of container repositories in the group. |
|
| <a id="groupcontainerrepositoriescount"></a>`containerRepositoriesCount` | [`Int!`](#int) | Number of container repositories in the group. |
|
||||||
| <a id="groupcontainslockedprojects"></a>`containsLockedProjects` | [`Boolean!`](#boolean) | Includes at least one project where the repository size exceeds the limit. |
|
| <a id="groupcontainslockedprojects"></a>`containsLockedProjects` | [`Boolean!`](#boolean) | Includes at least one project where the repository size exceeds the limit. |
|
||||||
| <a id="groupcrossprojectpipelineavailable"></a>`crossProjectPipelineAvailable` | [`Boolean!`](#boolean) | Indicates if the cross_project_pipeline feature is available for the namespace. |
|
| <a id="groupcrossprojectpipelineavailable"></a>`crossProjectPipelineAvailable` | [`Boolean!`](#boolean) | Indicates if the cross_project_pipeline feature is available for the namespace. |
|
||||||
|
@ -11546,7 +11545,6 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
||||||
| <a id="grouplfsenabled"></a>`lfsEnabled` | [`Boolean`](#boolean) | Indicates if Large File Storage (LFS) is enabled for namespace. |
|
| <a id="grouplfsenabled"></a>`lfsEnabled` | [`Boolean`](#boolean) | Indicates if Large File Storage (LFS) is enabled for namespace. |
|
||||||
| <a id="groupmentionsdisabled"></a>`mentionsDisabled` | [`Boolean`](#boolean) | Indicates if a group is disabled from getting mentioned. |
|
| <a id="groupmentionsdisabled"></a>`mentionsDisabled` | [`Boolean`](#boolean) | Indicates if a group is disabled from getting mentioned. |
|
||||||
| <a id="groupname"></a>`name` | [`String!`](#string) | Name of the namespace. |
|
| <a id="groupname"></a>`name` | [`String!`](#string) | Name of the namespace. |
|
||||||
| <a id="grouporganizations"></a>`organizations` | [`CustomerRelationsOrganizationConnection`](#customerrelationsorganizationconnection) | Find organizations of this group. (see [Connections](#connections)) |
|
|
||||||
| <a id="grouppackagesettings"></a>`packageSettings` | [`PackageSettings`](#packagesettings) | Package settings for the namespace. |
|
| <a id="grouppackagesettings"></a>`packageSettings` | [`PackageSettings`](#packagesettings) | Package settings for the namespace. |
|
||||||
| <a id="groupparent"></a>`parent` | [`Group`](#group) | Parent group. |
|
| <a id="groupparent"></a>`parent` | [`Group`](#group) | Parent group. |
|
||||||
| <a id="grouppath"></a>`path` | [`String!`](#string) | Path of the namespace. |
|
| <a id="grouppath"></a>`path` | [`String!`](#string) | Path of the namespace. |
|
||||||
|
@ -11644,6 +11642,23 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
||||||
| ---- | ---- | ----------- |
|
| ---- | ---- | ----------- |
|
||||||
| <a id="groupcomplianceframeworksid"></a>`id` | [`ComplianceManagementFrameworkID`](#compliancemanagementframeworkid) | Global ID of a specific compliance framework to return. |
|
| <a id="groupcomplianceframeworksid"></a>`id` | [`ComplianceManagementFrameworkID`](#compliancemanagementframeworkid) | Global ID of a specific compliance framework to return. |
|
||||||
|
|
||||||
|
##### `Group.contacts`
|
||||||
|
|
||||||
|
Find contacts of this group.
|
||||||
|
|
||||||
|
Returns [`CustomerRelationsContactConnection`](#customerrelationscontactconnection).
|
||||||
|
|
||||||
|
This field returns a [connection](#connections). It accepts the
|
||||||
|
four standard [pagination arguments](#connection-pagination-arguments):
|
||||||
|
`before: String`, `after: String`, `first: Int`, `last: Int`.
|
||||||
|
|
||||||
|
###### Arguments
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| ---- | ---- | ----------- |
|
||||||
|
| <a id="groupcontactssearch"></a>`search` | [`String`](#string) | Search term to find contacts with. |
|
||||||
|
| <a id="groupcontactsstate"></a>`state` | [`CustomerRelationsContactState`](#customerrelationscontactstate) | State of the contacts to search for. |
|
||||||
|
|
||||||
##### `Group.containerRepositories`
|
##### `Group.containerRepositories`
|
||||||
|
|
||||||
Container repositories of the group.
|
Container repositories of the group.
|
||||||
|
@ -11983,6 +11998,23 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
||||||
| <a id="groupmilestonestimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. |
|
| <a id="groupmilestonestimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. |
|
||||||
| <a id="groupmilestonestitle"></a>`title` | [`String`](#string) | Title of the milestone. |
|
| <a id="groupmilestonestitle"></a>`title` | [`String`](#string) | Title of the milestone. |
|
||||||
|
|
||||||
|
##### `Group.organizations`
|
||||||
|
|
||||||
|
Find organizations of this group.
|
||||||
|
|
||||||
|
Returns [`CustomerRelationsOrganizationConnection`](#customerrelationsorganizationconnection).
|
||||||
|
|
||||||
|
This field returns a [connection](#connections). It accepts the
|
||||||
|
four standard [pagination arguments](#connection-pagination-arguments):
|
||||||
|
`before: String`, `after: String`, `first: Int`, `last: Int`.
|
||||||
|
|
||||||
|
###### Arguments
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| ---- | ---- | ----------- |
|
||||||
|
| <a id="grouporganizationssearch"></a>`search` | [`String`](#string) | Search term used to find organizations with. |
|
||||||
|
| <a id="grouporganizationsstate"></a>`state` | [`CustomerRelationsOrganizationState`](#customerrelationsorganizationstate) | State of the organization to search for. |
|
||||||
|
|
||||||
##### `Group.packages`
|
##### `Group.packages`
|
||||||
|
|
||||||
Packages of the group.
|
Packages of the group.
|
||||||
|
@ -18492,6 +18524,20 @@ Values for sorting tags.
|
||||||
| <a id="containerrepositorytagsortname_asc"></a>`NAME_ASC` | Ordered by name in ascending order. |
|
| <a id="containerrepositorytagsortname_asc"></a>`NAME_ASC` | Ordered by name in ascending order. |
|
||||||
| <a id="containerrepositorytagsortname_desc"></a>`NAME_DESC` | Ordered by name in descending order. |
|
| <a id="containerrepositorytagsortname_desc"></a>`NAME_DESC` | Ordered by name in descending order. |
|
||||||
|
|
||||||
|
### `CustomerRelationsContactState`
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
| ----- | ----------- |
|
||||||
|
| <a id="customerrelationscontactstateactive"></a>`active` | Active contact. |
|
||||||
|
| <a id="customerrelationscontactstateinactive"></a>`inactive` | Inactive contact. |
|
||||||
|
|
||||||
|
### `CustomerRelationsOrganizationState`
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
| ----- | ----------- |
|
||||||
|
| <a id="customerrelationsorganizationstateactive"></a>`active` | Active organization. |
|
||||||
|
| <a id="customerrelationsorganizationstateinactive"></a>`inactive` | Inactive organization. |
|
||||||
|
|
||||||
### `DastProfileCadenceUnit`
|
### `DastProfileCadenceUnit`
|
||||||
|
|
||||||
Unit for the duration of Dast Profile Cadence.
|
Unit for the duration of Dast Profile Cadence.
|
||||||
|
|
|
@ -230,7 +230,7 @@ include:
|
||||||
file: '/templates/.gitlab-ci-template.yml'
|
file: '/templates/.gitlab-ci-template.yml'
|
||||||
|
|
||||||
- project: 'my-group/my-project'
|
- project: 'my-group/my-project'
|
||||||
ref: v1.0.0
|
ref: v1.0.0 # Git Tag
|
||||||
file: '/templates/.gitlab-ci-template.yml'
|
file: '/templates/.gitlab-ci-template.yml'
|
||||||
|
|
||||||
- project: 'my-group/my-project'
|
- project: 'my-group/my-project'
|
||||||
|
|
|
@ -450,5 +450,26 @@ test-vars-2:
|
||||||
- printenv
|
- printenv
|
||||||
```
|
```
|
||||||
|
|
||||||
You can't reuse a section that already includes a `!reference` tag. Only one level
|
### Nest `!reference` tags in `script`, `before_script`, and `after_script`
|
||||||
of nesting is supported.
|
|
||||||
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74792) in GitLab 14.8.
|
||||||
|
|
||||||
|
You can nest `!reference` tags up to 10 levels deep in `script`, `before_script`, and `after_script` sections. Use nested tags to define reusable sections when building more complex scripts. For example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
.snippets:
|
||||||
|
one:
|
||||||
|
- echo "ONE!"
|
||||||
|
two:
|
||||||
|
- !reference [.snippets, one]
|
||||||
|
- echo "TWO!"
|
||||||
|
three:
|
||||||
|
- !reference [.snippets, two]
|
||||||
|
- echo "THREE!"
|
||||||
|
|
||||||
|
nested-references:
|
||||||
|
script:
|
||||||
|
- !reference [.snippets, three]
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the `nested-references` job runs all three `echo` commands.
|
||||||
|
|
|
@ -24,6 +24,10 @@ when your platform does not support it.
|
||||||
1. Ensure new sprite sheets generated for 1x and 2x
|
1. Ensure new sprite sheets generated for 1x and 2x
|
||||||
- `app/assets/images/emoji.png`
|
- `app/assets/images/emoji.png`
|
||||||
- `app/assets/images/emoji@2x.png`
|
- `app/assets/images/emoji@2x.png`
|
||||||
|
1. Update `fixtures/emojis/intents.json` with any new emoji that we would like to highlight as having positive or negative intent.
|
||||||
|
- Positive intent should be set to `0.5`.
|
||||||
|
- Neutral intent can be set to `1`. This is applied to all emoji automatically so there is no need to set this explicitly.
|
||||||
|
- Negative intent should be set to `1.5`.
|
||||||
1. Ensure you see new individual images copied into `app/assets/images/emoji/`
|
1. Ensure you see new individual images copied into `app/assets/images/emoji/`
|
||||||
1. Ensure you can see the new emojis and their aliases in the GitLab Flavored Markdown (GLFM) Autocomplete
|
1. Ensure you can see the new emojis and their aliases in the GitLab Flavored Markdown (GLFM) Autocomplete
|
||||||
1. Ensure you can see the new emojis and their aliases in the award emoji menu
|
1. Ensure you can see the new emojis and their aliases in the award emoji menu
|
||||||
|
|
|
@ -52,7 +52,10 @@ this inconsistency.
|
||||||
Some places in the code refer to both the GitLab and GitHub specifications
|
Some places in the code refer to both the GitLab and GitHub specifications
|
||||||
simultaneous in the same areas of logic. In these situations,
|
simultaneous in the same areas of logic. In these situations,
|
||||||
_GitHub_ Flavored Markdown may be referred to with variable or constant names like
|
_GitHub_ Flavored Markdown may be referred to with variable or constant names like
|
||||||
`ghfm_` to avoid confusion.
|
`ghfm_` to avoid confusion. For example, we use the `ghfm` acronym for the
|
||||||
|
[`ghfm_spec_v_0.29.txt` GitHub Flavored Markdown specification file](#github-flavored-markdown-specification)
|
||||||
|
which is committed to the `gitlab` repo and used as input to the
|
||||||
|
[`update_specification.rb` script](#update-specificationrb-script).
|
||||||
|
|
||||||
The original CommonMark specification is referred to as _CommonMark_ (no acronym).
|
The original CommonMark specification is referred to as _CommonMark_ (no acronym).
|
||||||
|
|
||||||
|
@ -434,7 +437,7 @@ subgraph script:
|
||||||
A --> B{Backend Markdown API}
|
A --> B{Backend Markdown API}
|
||||||
end
|
end
|
||||||
subgraph input:<br/>input specification files
|
subgraph input:<br/>input specification files
|
||||||
C[gfm_spec_v_0.29.txt] --> A
|
C[ghfm_spec_v_0.29.txt] --> A
|
||||||
D[glfm_intro.txt] --> A
|
D[glfm_intro.txt] --> A
|
||||||
E[glfm_canonical_examples.txt] --> A
|
E[glfm_canonical_examples.txt] --> A
|
||||||
end
|
end
|
||||||
|
@ -572,12 +575,16 @@ updated, as in the case of all GFM files.
|
||||||
|
|
||||||
##### GitHub Flavored Markdown specification
|
##### GitHub Flavored Markdown specification
|
||||||
|
|
||||||
[`glfm_specification/input/github_flavored_markdown/gfm_spec_v_0.29.txt`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/glfm_specification/input/github_flavored_markdown/gfm_spec_v_0.29.txt)
|
[`glfm_specification/input/github_flavored_markdown/ghfm_spec_v_0.29.txt`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/glfm_specification/input/github_flavored_markdown/ghfm_spec_v_0.29.txt)
|
||||||
is the official latest [GFM `spec.txt`](https://github.com/github/cmark-gfm/blob/master/test/spec.txt).
|
is the official latest [GFM `spec.txt`](https://github.com/github/cmark-gfm/blob/master/test/spec.txt).
|
||||||
|
|
||||||
- It is automatically downloaded and updated by `update-specification.rb` script.
|
- It is automatically downloaded and updated by `update-specification.rb` script.
|
||||||
- When it is downloaded, the version number is added to the filename.
|
- When it is downloaded, the version number is added to the filename.
|
||||||
|
|
||||||
|
NOTE:
|
||||||
|
This file uses the `ghfm` acronym instead of `gfm`, as
|
||||||
|
explained in the [Acronyms section](#acronyms-glfm-ghfm-gfm-commonmark).
|
||||||
|
|
||||||
##### `glfm_intro.txt`
|
##### `glfm_intro.txt`
|
||||||
|
|
||||||
[`glfm_specification/input/gitlab_flavored_markdown/glfm_intro.txt`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/glfm_specification/input/gitlab_flavored_markdown/glfm_intro.txt)
|
[`glfm_specification/input/gitlab_flavored_markdown/glfm_intro.txt`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/glfm_specification/input/gitlab_flavored_markdown/glfm_intro.txt)
|
||||||
|
@ -900,12 +907,12 @@ Any exceptions or failures which occur when generating HTML are replaced with an
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
06_04_inlines_emphasis_and_strong_emphasis_1:
|
06_04_inlines_emphasis_and_strong_emphasis_1:
|
||||||
canonical: |
|
canonical: |
|
||||||
<p><em>foo bar</em></p>
|
<p><em>foo bar</em></p>
|
||||||
static: |
|
static: |
|
||||||
<p data-sourcepos="1:1-1:9" dir="auto"><strong>foo bar</strong></p>
|
<p data-sourcepos="1:1-1:9" dir="auto"><strong>foo bar</strong></p>
|
||||||
wysiwyg: |
|
wysiwyg: |
|
||||||
<p><strong>foo bar</strong></p>
|
<p><strong>foo bar</strong></p>
|
||||||
```
|
```
|
||||||
|
|
||||||
NOTE:
|
NOTE:
|
||||||
|
|
|
@ -205,7 +205,7 @@ To assign an alert:
|
||||||
![Alert Details View Assignees](img/alert_details_assignees_v13_1.png)
|
![Alert Details View Assignees](img/alert_details_assignees_v13_1.png)
|
||||||
|
|
||||||
1. If the right sidebar is not expanded, select
|
1. If the right sidebar is not expanded, select
|
||||||
**Expand sidebar** (**{angle-double-right}**) to expand it.
|
**Expand sidebar** (**{chevron-double-lg-right}**) to expand it.
|
||||||
|
|
||||||
1. On the right sidebar, locate the **Assignee**, and then select **Edit**.
|
1. On the right sidebar, locate the **Assignee**, and then select **Edit**.
|
||||||
From the list, select each user you want to assign to the alert.
|
From the list, select each user you want to assign to the alert.
|
||||||
|
|
|
@ -21,3 +21,6 @@ To enable pipeline status emails:
|
||||||
**Notify only broken pipelines**.
|
**Notify only broken pipelines**.
|
||||||
1. Select the branches to send notifications for.
|
1. Select the branches to send notifications for.
|
||||||
1. Select **Save changes**.
|
1. Select **Save changes**.
|
||||||
|
|
||||||
|
In [GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89546)
|
||||||
|
and later, pipeline notifications triggered by blocked users are not delivered.
|
||||||
|
|
|
@ -1050,6 +1050,9 @@ Pipeline events are triggered when the status of a pipeline changes.
|
||||||
In [GitLab 13.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53159)
|
In [GitLab 13.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53159)
|
||||||
and later, the pipeline webhook returns only the latest jobs.
|
and later, the pipeline webhook returns only the latest jobs.
|
||||||
|
|
||||||
|
In [GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89546)
|
||||||
|
and later, pipeline webhooks triggered by blocked users are not processed.
|
||||||
|
|
||||||
Request header:
|
Request header:
|
||||||
|
|
||||||
```plaintext
|
```plaintext
|
||||||
|
@ -1310,6 +1313,9 @@ Job events are triggered when the status of a job changes.
|
||||||
|
|
||||||
The `commit.id` in the payload is the ID of the pipeline, not the ID of the commit.
|
The `commit.id` in the payload is the ID of the pipeline, not the ID of the commit.
|
||||||
|
|
||||||
|
In [GitLab 15.1](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89546)
|
||||||
|
and later, job events triggered by blocked users are not processed.
|
||||||
|
|
||||||
Request header:
|
Request header:
|
||||||
|
|
||||||
```plaintext
|
```plaintext
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 6.4 KiB |
|
@ -87,8 +87,6 @@ method selected, you can accept it **only if a fast-forward merge is possible**.
|
||||||
|
|
||||||
## Rebasing in (semi-)linear merge methods
|
## Rebasing in (semi-)linear merge methods
|
||||||
|
|
||||||
> Rebasing without running a CI/CD pipeline [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118825) in GitLab 14.7.
|
|
||||||
|
|
||||||
In these merge methods, you can merge only when your source branch is up-to-date with the target branch:
|
In these merge methods, you can merge only when your source branch is up-to-date with the target branch:
|
||||||
|
|
||||||
- Merge commit with semi-linear history.
|
- Merge commit with semi-linear history.
|
||||||
|
@ -96,11 +94,7 @@ In these merge methods, you can merge only when your source branch is up-to-date
|
||||||
|
|
||||||
If a fast-forward merge is not possible but a conflict-free rebase is possible,
|
If a fast-forward merge is not possible but a conflict-free rebase is possible,
|
||||||
GitLab offers you the [`/rebase` quick action](../../../../topics/git/git_rebase.md#rebase-from-the-gitlab-ui),
|
GitLab offers you the [`/rebase` quick action](../../../../topics/git/git_rebase.md#rebase-from-the-gitlab-ui),
|
||||||
and the ability to **Rebase** from the user interface:
|
and the ability to select **Rebase** from the user interface.
|
||||||
|
|
||||||
![Fast forward merge request](../img/ff_merge_rebase_v14_9.png)
|
|
||||||
|
|
||||||
In [GitLab 14.7](https://gitlab.com/gitlab-org/gitlab/-/issues/118825) and later, you can also rebase without running a CI/CD pipeline.
|
|
||||||
|
|
||||||
If the target branch is ahead of the source branch and a conflict-free rebase is
|
If the target branch is ahead of the source branch and a conflict-free rebase is
|
||||||
not possible, you must rebase the source branch locally before you can do a fast-forward merge.
|
not possible, you must rebase the source branch locally before you can do a fast-forward merge.
|
||||||
|
@ -110,6 +104,23 @@ not possible, you must rebase the source branch locally before you can do a fast
|
||||||
Rebasing may be required before squashing, even though squashing can itself be
|
Rebasing may be required before squashing, even though squashing can itself be
|
||||||
considered equivalent to rebasing.
|
considered equivalent to rebasing.
|
||||||
|
|
||||||
|
### Rebase without CI/CD pipeline
|
||||||
|
|
||||||
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118825) in GitLab 14.7 [with a flag](../../../../administration/feature_flags.md) named `rebase_without_ci_ui`. Disabled by default.
|
||||||
|
|
||||||
|
FLAG:
|
||||||
|
On GitLab.com and self-managed GitLab, by default this feature is not available. To make it available,
|
||||||
|
ask an administrator to [enable the feature flag](../../../../administration/feature_flags.md) named `rebase_without_ci_ui`.
|
||||||
|
The feature is not ready for production use.
|
||||||
|
|
||||||
|
To rebase a merge request's branch without triggering a CI/CD pipeline, select
|
||||||
|
**Rebase without pipeline** from the merge request reports section.
|
||||||
|
This option is available when fast-forward merge is not possible but a conflict-free
|
||||||
|
rebase is possible.
|
||||||
|
|
||||||
|
Rebasing without a CI/CD pipeline saves resources in projects with a semi-linear
|
||||||
|
workflow that requires frequent rebases.
|
||||||
|
|
||||||
## Related topics
|
## Related topics
|
||||||
|
|
||||||
- [Commits history](../commits.md)
|
- [Commits history](../commits.md)
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"thumbsdown": 1.5,
|
||||||
|
"thumbsdown_tone1": 1.5,
|
||||||
|
"thumbsdown_tone2": 1.5,
|
||||||
|
"thumbsdown_tone3": 1.5,
|
||||||
|
"thumbsdown_tone4": 1.5,
|
||||||
|
"thumbsdown_tone5": 1.5,
|
||||||
|
"thumbsup": 0.5,
|
||||||
|
"thumbsup_tone1": 0.5,
|
||||||
|
"thumbsup_tone2": 0.5,
|
||||||
|
"thumbsup_tone3": 0.5,
|
||||||
|
"thumbsup_tone4": 0.5,
|
||||||
|
"thumbsup_tone5": 0.5,
|
||||||
|
"slight_frown": 1.5,
|
||||||
|
"slight_smile": 0.5
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module BackgroundMigration
|
||||||
|
# Set `project_settings.legacy_open_source_license_available` to false for non-public projects
|
||||||
|
class SetLegacyOpenSourceLicenseAvailableForNonPublicProjects < ::Gitlab::BackgroundMigration::BatchedMigrationJob
|
||||||
|
PUBLIC = 20
|
||||||
|
|
||||||
|
# Migration only version of `project_settings` table
|
||||||
|
class ProjectSetting < ApplicationRecord
|
||||||
|
self.table_name = 'project_settings'
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform
|
||||||
|
each_sub_batch(
|
||||||
|
operation_name: :set_legacy_open_source_license_available,
|
||||||
|
batching_scope: ->(relation) { relation.where.not(visibility_level: PUBLIC) }
|
||||||
|
) do |sub_batch|
|
||||||
|
ProjectSetting.where(project_id: sub_batch).update_all(legacy_open_source_license_available: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,6 +7,7 @@ module Gitlab
|
||||||
module Limit
|
module Limit
|
||||||
class RateLimit < Chain::Base
|
class RateLimit < Chain::Base
|
||||||
include Chain::Helpers
|
include Chain::Helpers
|
||||||
|
include ::Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
def perform!
|
def perform!
|
||||||
# We exclude child-pipelines from the rate limit because they represent
|
# We exclude child-pipelines from the rate limit because they represent
|
||||||
|
@ -41,7 +42,9 @@ module Gitlab
|
||||||
commit_sha: command.sha,
|
commit_sha: command.sha,
|
||||||
current_user_id: current_user.id,
|
current_user_id: current_user.id,
|
||||||
subscription_plan: project.actual_plan_name,
|
subscription_plan: project.actual_plan_name,
|
||||||
message: 'Activated pipeline creation rate limit'
|
message: 'Activated pipeline creation rate limit',
|
||||||
|
throttled: enforce_throttle?,
|
||||||
|
throttle_override: throttle_override?
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -50,9 +53,16 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def enforce_throttle?
|
def enforce_throttle?
|
||||||
::Feature.enabled?(
|
strong_memoize(:enforce_throttle) do
|
||||||
:ci_enforce_throttle_pipelines_creation,
|
::Feature.enabled?(:ci_enforce_throttle_pipelines_creation, project) &&
|
||||||
project)
|
!throttle_override?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def throttle_override?
|
||||||
|
strong_memoize(:throttle_override) do
|
||||||
|
::Feature.enabled?(:ci_enforce_throttle_pipelines_creation_override, project)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17232,7 +17232,7 @@ msgstr ""
|
||||||
msgid "GitLab is open source software to collaborate on code."
|
msgid "GitLab is open source software to collaborate on code."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "GitLab is undergoing maintenance and is operating in read-only mode."
|
msgid "GitLab is undergoing maintenance"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "GitLab logo"
|
msgid "GitLab logo"
|
||||||
|
@ -19088,6 +19088,9 @@ msgstr ""
|
||||||
msgid "IDE|Review"
|
msgid "IDE|Review"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "IDE|Start a new merge request"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "IDE|Successful commit"
|
msgid "IDE|Successful commit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -25758,9 +25761,6 @@ msgstr ""
|
||||||
msgid "No runner executable"
|
msgid "No runner executable"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "No runners found"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "No schedules"
|
msgid "No schedules"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -32765,6 +32765,9 @@ msgstr[1] ""
|
||||||
msgid "Runners|A capacity of 1 enables warm HA through Auto Scaling group re-spawn. A capacity of 2 enables hot HA because the service is available even when a node is lost. A capacity of 3 or more enables hot HA and manual scaling of runner fleet."
|
msgid "Runners|A capacity of 1 enables warm HA through Auto Scaling group re-spawn. A capacity of 2 enables hot HA because the service is available even when a node is lost. A capacity of 3 or more enables hot HA and manual scaling of runner fleet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Runners|A new version is available"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Runners|A periodic background task deletes runners that haven't contacted GitLab in more than %{elapsedTime}. %{linkStart}Can I view how many runners were deleted?%{linkEnd}"
|
msgid "Runners|A periodic background task deletes runners that haven't contacted GitLab in more than %{elapsedTime}. %{linkStart}Can I view how many runners were deleted?%{linkEnd}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -32866,6 +32869,9 @@ msgstr ""
|
||||||
msgid "Runners|Download latest binary"
|
msgid "Runners|Download latest binary"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Runners|Edit your search and try again"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Runners|Enable stale runner cleanup"
|
msgid "Runners|Enable stale runner cleanup"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -32878,6 +32884,9 @@ msgstr ""
|
||||||
msgid "Runners|Executor"
|
msgid "Runners|Executor"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Runners|Get started with runners"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Runners|Group"
|
msgid "Runners|Group"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -32926,6 +32935,9 @@ msgstr ""
|
||||||
msgid "Runners|New registration token generated!"
|
msgid "Runners|New registration token generated!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Runners|No results found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Runners|No spot. Default choice for Windows Shell executor."
|
msgid "Runners|No spot. Default choice for Windows Shell executor."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -33069,6 +33081,9 @@ msgstr ""
|
||||||
msgid "Runners|Runners"
|
msgid "Runners|Runners"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Runners|Runners are the agents that run your CI/CD jobs. Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Runners|Runs untagged jobs"
|
msgid "Runners|Runs untagged jobs"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -33134,6 +33149,9 @@ msgstr ""
|
||||||
msgid "Runners|This runner is available to all projects and subgroups in a group."
|
msgid "Runners|This runner is available to all projects and subgroups in a group."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Runners|This runner is outdated, an upgrade is recommended"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Runners|To install Runner in Kubernetes follow the instructions described in the GitLab documentation."
|
msgid "Runners|To install Runner in Kubernetes follow the instructions described in the GitLab documentation."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -33203,6 +33221,12 @@ msgstr ""
|
||||||
msgid "Runners|stale"
|
msgid "Runners|stale"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Runners|upgrade available"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Runners|upgrade recommended"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Running"
|
msgid "Running"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -36268,9 +36292,6 @@ msgstr ""
|
||||||
msgid "Start a new discussion…"
|
msgid "Start a new discussion…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Start a new merge request"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Start a new merge request with these changes"
|
msgid "Start a new merge request with these changes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -38748,9 +38769,6 @@ msgstr ""
|
||||||
msgid "This GitLab instance is licensed at the %{insufficient_license} tier. Geo is only available for users who have at least a Premium license."
|
msgid "This GitLab instance is licensed at the %{insufficient_license} tier. Geo is only available for users who have at least a Premium license."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "This GitLab instance is undergoing maintenance and is operating in read-only mode."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "This PDF is too large to display. Please download to view."
|
msgid "This PDF is too large to display. Please download to view."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
"@babel/preset-env": "^7.10.1",
|
"@babel/preset-env": "^7.10.1",
|
||||||
"@gitlab/at.js": "1.5.7",
|
"@gitlab/at.js": "1.5.7",
|
||||||
"@gitlab/favicon-overlay": "2.0.0",
|
"@gitlab/favicon-overlay": "2.0.0",
|
||||||
"@gitlab/svgs": "2.17.0",
|
"@gitlab/svgs": "2.18.0",
|
||||||
"@gitlab/ui": "40.7.1",
|
"@gitlab/ui": "40.7.1",
|
||||||
"@gitlab/visual-review-tools": "1.7.3",
|
"@gitlab/visual-review-tools": "1.7.3",
|
||||||
"@rails/actioncable": "6.1.4-7",
|
"@rails/actioncable": "6.1.4-7",
|
||||||
|
|
|
@ -115,13 +115,17 @@ RSpec.describe "Admin Runners" do
|
||||||
expect(page).not_to have_content("runner-bar")
|
expect(page).not_to have_content("runner-bar")
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows no runner when description does not match' do
|
context 'when description does not match' do
|
||||||
input_filtered_search_keys('runner-baz')
|
before do
|
||||||
|
input_filtered_search_keys('runner-baz')
|
||||||
|
end
|
||||||
|
|
||||||
expect(page).to have_link('All 0')
|
it_behaves_like 'shows no runners found'
|
||||||
expect(page).to have_link('Instance 0')
|
|
||||||
|
|
||||||
expect(page).to have_text 'No runners found'
|
it 'shows no runner' do
|
||||||
|
expect(page).to have_link('All 0')
|
||||||
|
expect(page).to have_link('Instance 0')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -190,14 +194,6 @@ RSpec.describe "Admin Runners" do
|
||||||
expect(page).not_to have_content 'runner-never-contacted'
|
expect(page).not_to have_content 'runner-never-contacted'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows no runner when status does not match' do
|
|
||||||
input_filtered_search_filter_is_only('Status', 'Stale')
|
|
||||||
|
|
||||||
expect(page).to have_link('All 0')
|
|
||||||
|
|
||||||
expect(page).to have_text 'No runners found'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'shows correct runner when status is selected and search term is entered' do
|
it 'shows correct runner when status is selected and search term is entered' do
|
||||||
input_filtered_search_filter_is_only('Status', 'Online')
|
input_filtered_search_filter_is_only('Status', 'Online')
|
||||||
input_filtered_search_keys('runner-1')
|
input_filtered_search_keys('runner-1')
|
||||||
|
@ -225,6 +221,18 @@ RSpec.describe "Admin Runners" do
|
||||||
expect(page).to have_selector '.badge', text: 'never contacted'
|
expect(page).to have_selector '.badge', text: 'never contacted'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when status does not match' do
|
||||||
|
before do
|
||||||
|
input_filtered_search_filter_is_only('Status', 'Stale')
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'shows no runners found'
|
||||||
|
|
||||||
|
it 'shows no runner' do
|
||||||
|
expect(page).to have_link('All 0')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'filter by type' do
|
describe 'filter by type' do
|
||||||
|
@ -273,21 +281,6 @@ RSpec.describe "Admin Runners" do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows no runner when type does not match' do
|
|
||||||
visit admin_runners_path
|
|
||||||
|
|
||||||
page.within('[data-testid="runner-type-tabs"]') do
|
|
||||||
click_on 'Instance'
|
|
||||||
|
|
||||||
expect(page).to have_link('Instance', class: 'active')
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(page).not_to have_content 'runner-project'
|
|
||||||
expect(page).not_to have_content 'runner-group'
|
|
||||||
|
|
||||||
expect(page).to have_text 'No runners found'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'shows correct runner when type is selected and search term is entered' do
|
it 'shows correct runner when type is selected and search term is entered' do
|
||||||
create(:ci_runner, :project, description: 'runner-2-project', projects: [project])
|
create(:ci_runner, :project, description: 'runner-2-project', projects: [project])
|
||||||
|
|
||||||
|
@ -327,6 +320,24 @@ RSpec.describe "Admin Runners" do
|
||||||
expect(page).not_to have_content 'runner-group'
|
expect(page).not_to have_content 'runner-group'
|
||||||
expect(page).not_to have_content 'runner-paused-project'
|
expect(page).not_to have_content 'runner-paused-project'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when type does not match' do
|
||||||
|
before do
|
||||||
|
visit admin_runners_path
|
||||||
|
page.within('[data-testid="runner-type-tabs"]') do
|
||||||
|
click_on 'Instance'
|
||||||
|
|
||||||
|
expect(page).to have_link('Instance', class: 'active')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'shows no runners found'
|
||||||
|
|
||||||
|
it 'shows no runner' do
|
||||||
|
expect(page).not_to have_content 'runner-project'
|
||||||
|
expect(page).not_to have_content 'runner-group'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'filter by tag' do
|
describe 'filter by tag' do
|
||||||
|
@ -358,15 +369,6 @@ RSpec.describe "Admin Runners" do
|
||||||
expect(page).not_to have_content 'runner-red'
|
expect(page).not_to have_content 'runner-red'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows no runner when tag does not match' do
|
|
||||||
visit admin_runners_path
|
|
||||||
|
|
||||||
input_filtered_search_filter_is_only('Tags', 'green')
|
|
||||||
|
|
||||||
expect(page).not_to have_content 'runner-blue'
|
|
||||||
expect(page).to have_text 'No runners found'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'shows correct runner when tag is selected and search term is entered' do
|
it 'shows correct runner when tag is selected and search term is entered' do
|
||||||
create(:ci_runner, :instance, description: 'runner-2-blue', tag_list: ['blue'])
|
create(:ci_runner, :instance, description: 'runner-2-blue', tag_list: ['blue'])
|
||||||
|
|
||||||
|
@ -384,6 +386,19 @@ RSpec.describe "Admin Runners" do
|
||||||
expect(page).not_to have_content 'runner-blue'
|
expect(page).not_to have_content 'runner-blue'
|
||||||
expect(page).not_to have_content 'runner-red'
|
expect(page).not_to have_content 'runner-red'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when tag does not match' do
|
||||||
|
before do
|
||||||
|
visit admin_runners_path
|
||||||
|
input_filtered_search_filter_is_only('Tags', 'green')
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'shows no runners found'
|
||||||
|
|
||||||
|
it 'shows no runner' do
|
||||||
|
expect(page).not_to have_content 'runner-blue'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sorts by last contact date' do
|
it 'sorts by last contact date' do
|
||||||
|
@ -419,7 +434,7 @@ RSpec.describe "Admin Runners" do
|
||||||
visit admin_runners_path
|
visit admin_runners_path
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like "shows no runners"
|
it_behaves_like 'shows no runners registered'
|
||||||
|
|
||||||
it 'shows tabs with total counts equal to 0' do
|
it 'shows tabs with total counts equal to 0' do
|
||||||
expect(page).to have_link('All 0')
|
expect(page).to have_link('All 0')
|
||||||
|
|
|
@ -33,7 +33,7 @@ RSpec.describe "Group Runners" do
|
||||||
visit group_runners_path(group)
|
visit group_runners_path(group)
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like "shows no runners"
|
it_behaves_like 'shows no runners registered'
|
||||||
|
|
||||||
it 'shows tabs with total counts equal to 0' do
|
it 'shows tabs with total counts equal to 0' do
|
||||||
expect(page).to have_link('All 0')
|
expect(page).to have_link('All 0')
|
||||||
|
@ -70,6 +70,18 @@ RSpec.describe "Group Runners" do
|
||||||
expect(find_link('Edit')[:href]).to end_with(edit_group_runner_path(group, group_runner))
|
expect(find_link('Edit')[:href]).to end_with(edit_group_runner_path(group, group_runner))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when description does not match' do
|
||||||
|
before do
|
||||||
|
input_filtered_search_keys('runner-baz')
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'shows no runners found'
|
||||||
|
|
||||||
|
it 'shows no runner' do
|
||||||
|
expect(page).not_to have_content 'runner-foo'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "with an online project runner" do
|
context "with an online project runner" do
|
||||||
|
|
|
@ -66,5 +66,83 @@ RSpec.describe Crm::ContactsFinder do
|
||||||
expect(subject).to be_empty
|
expect(subject).to be_empty
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with search informations' do
|
||||||
|
let_it_be(:search_test_group) { create(:group, :crm_enabled) }
|
||||||
|
|
||||||
|
let_it_be(:search_test_a) do
|
||||||
|
create(
|
||||||
|
:contact,
|
||||||
|
group: search_test_group,
|
||||||
|
first_name: "ABC",
|
||||||
|
last_name: "DEF",
|
||||||
|
email: "ghi@test.com",
|
||||||
|
description: "LMNO",
|
||||||
|
state: "inactive"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let_it_be(:search_test_b) do
|
||||||
|
create(
|
||||||
|
:contact,
|
||||||
|
group: search_test_group,
|
||||||
|
first_name: "PQR",
|
||||||
|
last_name: "STU",
|
||||||
|
email: "vwx@test.com",
|
||||||
|
description: "YZ",
|
||||||
|
state: "active"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
search_test_group.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when search term is empty' do
|
||||||
|
it 'returns all group contacts alphabetically ordered' do
|
||||||
|
finder = described_class.new(user, group: search_test_group, search: "")
|
||||||
|
expect(finder.execute).to eq([search_test_a, search_test_b])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when search term is not empty' do
|
||||||
|
it 'searches for first name ignoring casing' do
|
||||||
|
finder = described_class.new(user, group: search_test_group, search: "aBc")
|
||||||
|
expect(finder.execute).to match_array([search_test_a])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'searches for last name ignoring casing' do
|
||||||
|
finder = described_class.new(user, group: search_test_group, search: "StU")
|
||||||
|
expect(finder.execute).to match_array([search_test_b])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'searches for email' do
|
||||||
|
finder = described_class.new(user, group: search_test_group, search: "ghi")
|
||||||
|
expect(finder.execute).to match_array([search_test_a])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'searches for description ignoring casing' do
|
||||||
|
finder = described_class.new(user, group: search_test_group, search: "Yz")
|
||||||
|
expect(finder.execute).to match_array([search_test_b])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fuzzy searches for email and last name' do
|
||||||
|
finder = described_class.new(user, group: search_test_group, search: "s")
|
||||||
|
expect(finder.execute).to match_array([search_test_a, search_test_b])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when searching for contacts state' do
|
||||||
|
it 'returns only inactive contacts' do
|
||||||
|
finder = described_class.new(user, group: search_test_group, state: :inactive)
|
||||||
|
expect(finder.execute).to match_array([search_test_a])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns only active contacts' do
|
||||||
|
finder = described_class.new(user, group: search_test_group, state: :active)
|
||||||
|
expect(finder.execute).to match_array([search_test_b])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Crm::OrganizationsFinder do
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
|
describe '#execute' do
|
||||||
|
subject { described_class.new(user, group: group).execute }
|
||||||
|
|
||||||
|
context 'when customer relations feature is enabled for the group' do
|
||||||
|
let_it_be(:root_group) { create(:group, :crm_enabled) }
|
||||||
|
let_it_be(:group) { create(:group, parent: root_group) }
|
||||||
|
|
||||||
|
let_it_be(:organization_1) { create(:organization, group: root_group) }
|
||||||
|
let_it_be(:organization_2) { create(:organization, group: root_group) }
|
||||||
|
|
||||||
|
context 'when user does not have permissions to see organizations in the group' do
|
||||||
|
it 'returns an empty array' do
|
||||||
|
expect(subject).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is member of the root group' do
|
||||||
|
before do
|
||||||
|
root_group.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when feature flag is disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(customer_relations: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an empty array' do
|
||||||
|
expect(subject).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when feature flag is enabled' do
|
||||||
|
it 'returns all group organizations' do
|
||||||
|
expect(subject).to match_array([organization_1, organization_2])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is member of the sub group' do
|
||||||
|
before do
|
||||||
|
group.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an empty array' do
|
||||||
|
expect(subject).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when customer relations feature is disabled for the group' do
|
||||||
|
let_it_be(:group) { create(:group) }
|
||||||
|
let_it_be(:organization) { create(:organization, group: group) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
group.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an empty array' do
|
||||||
|
expect(subject).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with search informations' do
|
||||||
|
let_it_be(:search_test_group) { create(:group, :crm_enabled) }
|
||||||
|
|
||||||
|
let_it_be(:search_test_a) do
|
||||||
|
create(
|
||||||
|
:organization,
|
||||||
|
group: search_test_group,
|
||||||
|
name: "DEF",
|
||||||
|
description: "ghi_st",
|
||||||
|
state: "inactive"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let_it_be(:search_test_b) do
|
||||||
|
create(
|
||||||
|
:organization,
|
||||||
|
group: search_test_group,
|
||||||
|
name: "ABC_st",
|
||||||
|
description: "JKL",
|
||||||
|
state: "active"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
search_test_group.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when search term is empty' do
|
||||||
|
it 'returns all group organizations alphabetically ordered' do
|
||||||
|
finder = described_class.new(user, group: search_test_group, search: "")
|
||||||
|
expect(finder.execute).to eq([search_test_b, search_test_a])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when search term is not empty' do
|
||||||
|
it 'searches for name' do
|
||||||
|
finder = described_class.new(user, group: search_test_group, search: "aBc")
|
||||||
|
expect(finder.execute).to match_array([search_test_b])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'searches for description' do
|
||||||
|
finder = described_class.new(user, group: search_test_group, search: "ghI")
|
||||||
|
expect(finder.execute).to match_array([search_test_a])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'searches for name and description' do
|
||||||
|
finder = described_class.new(user, group: search_test_group, search: "_st")
|
||||||
|
expect(finder.execute).to eq([search_test_b, search_test_a])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when searching for organizations state' do
|
||||||
|
it 'returns only inactive organizations' do
|
||||||
|
finder = described_class.new(user, group: search_test_group, state: :inactive)
|
||||||
|
expect(finder.execute).to match_array([search_test_a])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns only active organizations' do
|
||||||
|
finder = described_class.new(user, group: search_test_group, state: :active)
|
||||||
|
expect(finder.execute).to match_array([search_test_b])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -58,6 +58,16 @@ export const validEmoji = {
|
||||||
unicodeVersion: '6.0',
|
unicodeVersion: '6.0',
|
||||||
description: 'because it contains multiple zero width joiners',
|
description: 'because it contains multiple zero width joiners',
|
||||||
},
|
},
|
||||||
|
thumbsup: {
|
||||||
|
moji: '👍',
|
||||||
|
unicodeVersion: '6.0',
|
||||||
|
description: 'thumbs up sign',
|
||||||
|
},
|
||||||
|
thumbsdown: {
|
||||||
|
moji: '👎',
|
||||||
|
description: 'thumbs down sign',
|
||||||
|
unicodeVersion: '6.0',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const invalidEmoji = {
|
export const invalidEmoji = {
|
||||||
|
|
|
@ -57,6 +57,18 @@ describe('AwardsHandler', () => {
|
||||||
d: 'white question mark ornament',
|
d: 'white question mark ornament',
|
||||||
u: '6.0',
|
u: '6.0',
|
||||||
},
|
},
|
||||||
|
thumbsup: {
|
||||||
|
c: 'people',
|
||||||
|
e: '👍',
|
||||||
|
d: 'thumbs up sign',
|
||||||
|
u: '6.0',
|
||||||
|
},
|
||||||
|
thumbsdown: {
|
||||||
|
c: 'people',
|
||||||
|
e: '👎',
|
||||||
|
d: 'thumbs down sign',
|
||||||
|
u: '6.0',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const openAndWaitForEmojiMenu = (sel = '.js-add-award') => {
|
const openAndWaitForEmojiMenu = (sel = '.js-add-award') => {
|
||||||
|
@ -296,6 +308,23 @@ describe('AwardsHandler', () => {
|
||||||
awardsHandler.searchEmojis('👼');
|
awardsHandler.searchEmojis('👼');
|
||||||
expect($('[data-name=angel]').is(':visible')).toBe(true);
|
expect($('[data-name=angel]').is(':visible')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show positive intent emoji first', async () => {
|
||||||
|
await openAndWaitForEmojiMenu();
|
||||||
|
|
||||||
|
awardsHandler.searchEmojis('thumb');
|
||||||
|
|
||||||
|
const $menu = $('.emoji-menu');
|
||||||
|
const $thumbsUpItem = $menu.find('[data-name=thumbsup]');
|
||||||
|
const $thumbsDownItem = $menu.find('[data-name=thumbsdown]');
|
||||||
|
|
||||||
|
expect($thumbsUpItem.is(':visible')).toBe(true);
|
||||||
|
expect($thumbsDownItem.is(':visible')).toBe(true);
|
||||||
|
|
||||||
|
expect($thumbsUpItem.parents('.emoji-menu-list-item').index()).toBeLessThan(
|
||||||
|
$thumbsDownItem.parents('.emoji-menu-list-item').index(),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('emoji menu', () => {
|
describe('emoji menu', () => {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import isEmojiUnicodeSupported, {
|
||||||
isHorceRacingSkinToneComboEmoji,
|
isHorceRacingSkinToneComboEmoji,
|
||||||
isPersonZwjEmoji,
|
isPersonZwjEmoji,
|
||||||
} from '~/emoji/support/is_emoji_unicode_supported';
|
} from '~/emoji/support/is_emoji_unicode_supported';
|
||||||
|
import { NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants';
|
||||||
|
|
||||||
const emptySupportMap = {
|
const emptySupportMap = {
|
||||||
personZwj: false,
|
personZwj: false,
|
||||||
|
@ -436,14 +437,28 @@ describe('emoji', () => {
|
||||||
it.each([undefined, null, ''])("should return all emoji when the input is '%s'", (input) => {
|
it.each([undefined, null, ''])("should return all emoji when the input is '%s'", (input) => {
|
||||||
const search = searchEmoji(input);
|
const search = searchEmoji(input);
|
||||||
|
|
||||||
const expected = Object.keys(validEmoji).map((name) => {
|
const expected = Object.keys(validEmoji)
|
||||||
return {
|
.map((name) => {
|
||||||
emoji: mockEmojiData[name],
|
let score = NEUTRAL_INTENT_MULTIPLIER;
|
||||||
field: 'd',
|
|
||||||
fieldValue: mockEmojiData[name].d,
|
// Positive intent value retrieved from ~/emoji/intents.json
|
||||||
score: 0,
|
if (name === 'thumbsup') {
|
||||||
};
|
score = 0.5;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// Negative intent value retrieved from ~/emoji/intents.json
|
||||||
|
if (name === 'thumbsdown') {
|
||||||
|
score = 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
emoji: mockEmojiData[name],
|
||||||
|
field: 'd',
|
||||||
|
fieldValue: mockEmojiData[name].d,
|
||||||
|
score,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort(sortEmoji);
|
||||||
|
|
||||||
expect(search).toEqual(expected);
|
expect(search).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
@ -457,7 +472,7 @@ describe('emoji', () => {
|
||||||
name: 'atom',
|
name: 'atom',
|
||||||
field: 'e',
|
field: 'e',
|
||||||
fieldValue: 'atom',
|
fieldValue: 'atom',
|
||||||
score: 0,
|
score: NEUTRAL_INTENT_MULTIPLIER,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -469,7 +484,7 @@ describe('emoji', () => {
|
||||||
name: 'atom',
|
name: 'atom',
|
||||||
field: 'alias',
|
field: 'alias',
|
||||||
fieldValue: 'atom_symbol',
|
fieldValue: 'atom_symbol',
|
||||||
score: 4,
|
score: 16,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -481,7 +496,7 @@ describe('emoji', () => {
|
||||||
name: 'atom',
|
name: 'atom',
|
||||||
field: 'alias',
|
field: 'alias',
|
||||||
fieldValue: 'atom_symbol',
|
fieldValue: 'atom_symbol',
|
||||||
score: 0,
|
score: NEUTRAL_INTENT_MULTIPLIER,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -509,7 +524,7 @@ describe('emoji', () => {
|
||||||
{
|
{
|
||||||
name: 'atom',
|
name: 'atom',
|
||||||
field: 'd',
|
field: 'd',
|
||||||
score: 0,
|
score: NEUTRAL_INTENT_MULTIPLIER,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -521,7 +536,7 @@ describe('emoji', () => {
|
||||||
{
|
{
|
||||||
name: 'atom',
|
name: 'atom',
|
||||||
field: 'd',
|
field: 'd',
|
||||||
score: 0,
|
score: NEUTRAL_INTENT_MULTIPLIER,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -533,7 +548,7 @@ describe('emoji', () => {
|
||||||
{
|
{
|
||||||
name: 'grey_question',
|
name: 'grey_question',
|
||||||
field: 'name',
|
field: 'name',
|
||||||
score: 5,
|
score: 32,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -544,7 +559,7 @@ describe('emoji', () => {
|
||||||
{
|
{
|
||||||
name: 'grey_question',
|
name: 'grey_question',
|
||||||
field: 'd',
|
field: 'd',
|
||||||
score: 24,
|
score: 16777216,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -552,15 +567,15 @@ describe('emoji', () => {
|
||||||
'searching with query "heart"',
|
'searching with query "heart"',
|
||||||
'heart',
|
'heart',
|
||||||
[
|
[
|
||||||
{
|
|
||||||
name: 'black_heart',
|
|
||||||
field: 'd',
|
|
||||||
score: 6,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'heart',
|
name: 'heart',
|
||||||
field: 'name',
|
field: 'name',
|
||||||
score: 0,
|
score: NEUTRAL_INTENT_MULTIPLIER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'black_heart',
|
||||||
|
field: 'd',
|
||||||
|
score: 64,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -568,15 +583,15 @@ describe('emoji', () => {
|
||||||
'searching with query "HEART"',
|
'searching with query "HEART"',
|
||||||
'HEART',
|
'HEART',
|
||||||
[
|
[
|
||||||
{
|
|
||||||
name: 'black_heart',
|
|
||||||
field: 'd',
|
|
||||||
score: 6,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'heart',
|
name: 'heart',
|
||||||
field: 'name',
|
field: 'name',
|
||||||
score: 0,
|
score: NEUTRAL_INTENT_MULTIPLIER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'black_heart',
|
||||||
|
field: 'd',
|
||||||
|
score: 64,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -584,15 +599,31 @@ describe('emoji', () => {
|
||||||
'searching with query "star"',
|
'searching with query "star"',
|
||||||
'star',
|
'star',
|
||||||
[
|
[
|
||||||
{
|
|
||||||
name: 'custard',
|
|
||||||
field: 'd',
|
|
||||||
score: 2,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'star',
|
name: 'star',
|
||||||
field: 'name',
|
field: 'name',
|
||||||
score: 0,
|
score: NEUTRAL_INTENT_MULTIPLIER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'custard',
|
||||||
|
field: 'd',
|
||||||
|
score: 4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'searching for emoji with intentions assigned',
|
||||||
|
'thumbs',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'thumbsup',
|
||||||
|
field: 'd',
|
||||||
|
score: 0.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'thumbsdown',
|
||||||
|
field: 'd',
|
||||||
|
score: 1.5,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -619,10 +650,10 @@ describe('emoji', () => {
|
||||||
[
|
[
|
||||||
{ score: 10, fieldValue: '', emoji: { name: 'a' } },
|
{ score: 10, fieldValue: '', emoji: { name: 'a' } },
|
||||||
{ score: 5, fieldValue: '', emoji: { name: 'b' } },
|
{ score: 5, fieldValue: '', emoji: { name: 'b' } },
|
||||||
{ score: 0, fieldValue: '', emoji: { name: 'c' } },
|
{ score: 1, fieldValue: '', emoji: { name: 'c' } },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ score: 0, fieldValue: '', emoji: { name: 'c' } },
|
{ score: 1, fieldValue: '', emoji: { name: 'c' } },
|
||||||
{ score: 5, fieldValue: '', emoji: { name: 'b' } },
|
{ score: 5, fieldValue: '', emoji: { name: 'b' } },
|
||||||
{ score: 10, fieldValue: '', emoji: { name: 'a' } },
|
{ score: 10, fieldValue: '', emoji: { name: 'a' } },
|
||||||
],
|
],
|
||||||
|
@ -630,25 +661,25 @@ describe('emoji', () => {
|
||||||
[
|
[
|
||||||
'should correctly sort by fieldValue',
|
'should correctly sort by fieldValue',
|
||||||
[
|
[
|
||||||
{ score: 0, fieldValue: 'y', emoji: { name: 'b' } },
|
{ score: 1, fieldValue: 'y', emoji: { name: 'b' } },
|
||||||
{ score: 0, fieldValue: 'x', emoji: { name: 'a' } },
|
{ score: 1, fieldValue: 'x', emoji: { name: 'a' } },
|
||||||
{ score: 0, fieldValue: 'z', emoji: { name: 'c' } },
|
{ score: 1, fieldValue: 'z', emoji: { name: 'c' } },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ score: 0, fieldValue: 'x', emoji: { name: 'a' } },
|
{ score: 1, fieldValue: 'x', emoji: { name: 'a' } },
|
||||||
{ score: 0, fieldValue: 'y', emoji: { name: 'b' } },
|
{ score: 1, fieldValue: 'y', emoji: { name: 'b' } },
|
||||||
{ score: 0, fieldValue: 'z', emoji: { name: 'c' } },
|
{ score: 1, fieldValue: 'z', emoji: { name: 'c' } },
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'should correctly sort by score and then by fieldValue (in order)',
|
'should correctly sort by score and then by fieldValue (in order)',
|
||||||
[
|
[
|
||||||
{ score: 5, fieldValue: 'y', emoji: { name: 'c' } },
|
{ score: 5, fieldValue: 'y', emoji: { name: 'c' } },
|
||||||
{ score: 0, fieldValue: 'z', emoji: { name: 'a' } },
|
{ score: 1, fieldValue: 'z', emoji: { name: 'a' } },
|
||||||
{ score: 5, fieldValue: 'x', emoji: { name: 'b' } },
|
{ score: 5, fieldValue: 'x', emoji: { name: 'b' } },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ score: 0, fieldValue: 'z', emoji: { name: 'a' } },
|
{ score: 1, fieldValue: 'z', emoji: { name: 'a' } },
|
||||||
{ score: 5, fieldValue: 'x', emoji: { name: 'b' } },
|
{ score: 5, fieldValue: 'x', emoji: { name: 'b' } },
|
||||||
{ score: 5, fieldValue: 'y', emoji: { name: 'c' } },
|
{ score: 5, fieldValue: 'y', emoji: { name: 'c' } },
|
||||||
],
|
],
|
||||||
|
@ -656,7 +687,7 @@ describe('emoji', () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
it.each(testCases)('%s', (_, scoredItems, expected) => {
|
it.each(testCases)('%s', (_, scoredItems, expected) => {
|
||||||
expect(sortEmoji(scoredItems)).toEqual(expected);
|
expect(scoredItems.sort(sortEmoji)).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { getEmojiScoreWithIntent } from '~/emoji/utils';
|
||||||
|
|
||||||
|
describe('Utils', () => {
|
||||||
|
describe('getEmojiScoreWithIntent', () => {
|
||||||
|
it.each`
|
||||||
|
emojiName | baseScore | finalScore
|
||||||
|
${'thumbsup'} | ${1} | ${1}
|
||||||
|
${'thumbsdown'} | ${1} | ${3}
|
||||||
|
${'neutralemoji'} | ${1} | ${2}
|
||||||
|
${'zerobaseemoji'} | ${0} | ${1}
|
||||||
|
`('returns the correct score for $emojiName', ({ emojiName, baseScore, finalScore }) => {
|
||||||
|
expect(getEmojiScoreWithIntent(emojiName, baseScore)).toBe(finalScore);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -26,6 +26,12 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
|
||||||
remove_repository(project)
|
remove_repository(project)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Gitlab::Ci::RunnerUpgradeCheck.instance)
|
||||||
|
.to receive(:check_runner_upgrade_status)
|
||||||
|
.and_return(:not_available)
|
||||||
|
end
|
||||||
|
|
||||||
describe do
|
describe do
|
||||||
before do
|
before do
|
||||||
sign_in(admin)
|
sign_in(admin)
|
||||||
|
|
|
@ -1,193 +1,97 @@
|
||||||
import Vue, { nextTick } from 'vue';
|
import Vue from 'vue';
|
||||||
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
|
import Vuex from 'vuex';
|
||||||
import { projectData, branches } from 'jest/ide/mock_data';
|
import { GlFormCheckbox } from '@gitlab/ui';
|
||||||
|
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue';
|
import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue';
|
||||||
import { PERMISSION_CREATE_MR } from '~/ide/constants';
|
|
||||||
import { createStore } from '~/ide/stores';
|
import { createStore } from '~/ide/stores';
|
||||||
import {
|
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||||
COMMIT_TO_CURRENT_BRANCH,
|
|
||||||
COMMIT_TO_NEW_BRANCH,
|
|
||||||
} from '~/ide/stores/modules/commit/constants';
|
|
||||||
|
|
||||||
describe('create new MR checkbox', () => {
|
Vue.use(Vuex);
|
||||||
|
|
||||||
|
describe('NewMergeRequestOption component', () => {
|
||||||
let store;
|
let store;
|
||||||
let vm;
|
let wrapper;
|
||||||
|
|
||||||
const setMR = () => {
|
const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
|
||||||
vm.$store.state.currentMergeRequestId = '1';
|
const findFieldset = () => wrapper.findByTestId('new-merge-request-fieldset');
|
||||||
vm.$store.state.projects[store.state.currentProjectId].mergeRequests[
|
const findTooltip = () => getBinding(findFieldset().element, 'gl-tooltip');
|
||||||
store.state.currentMergeRequestId
|
|
||||||
] = { foo: 'bar' };
|
|
||||||
};
|
|
||||||
|
|
||||||
const setPermissions = (permissions) => {
|
const createComponent = ({
|
||||||
store.state.projects[store.state.currentProjectId].userPermissions = permissions;
|
shouldHideNewMrOption = false,
|
||||||
};
|
shouldDisableNewMrOption = false,
|
||||||
|
shouldCreateMR = false,
|
||||||
const createComponent = ({ currentBranchId = 'main', createNewBranch = false } = {}) => {
|
} = {}) => {
|
||||||
const Component = Vue.extend(NewMergeRequestOption);
|
|
||||||
|
|
||||||
vm = createComponentWithStore(Component, store);
|
|
||||||
|
|
||||||
vm.$store.state.commit.commitAction = createNewBranch
|
|
||||||
? COMMIT_TO_NEW_BRANCH
|
|
||||||
: COMMIT_TO_CURRENT_BRANCH;
|
|
||||||
|
|
||||||
vm.$store.state.currentBranchId = currentBranchId;
|
|
||||||
|
|
||||||
store.state.projects.abcproject.branches[currentBranchId] = branches.find(
|
|
||||||
(branch) => branch.name === currentBranchId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return vm.$mount();
|
|
||||||
};
|
|
||||||
|
|
||||||
const findInput = () => vm.$el.querySelector('input[type="checkbox"]');
|
|
||||||
const findLabel = () => vm.$el.querySelector('.js-ide-commit-new-mr');
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
store = createStore();
|
store = createStore();
|
||||||
|
|
||||||
store.state.currentProjectId = 'abcproject';
|
wrapper = shallowMountExtended(NewMergeRequestOption, {
|
||||||
|
store: {
|
||||||
const proj = JSON.parse(JSON.stringify(projectData));
|
...store,
|
||||||
proj.userPermissions[PERMISSION_CREATE_MR] = true;
|
getters: {
|
||||||
Vue.set(store.state.projects, 'abcproject', proj);
|
'commit/shouldHideNewMrOption': shouldHideNewMrOption,
|
||||||
});
|
'commit/shouldDisableNewMrOption': shouldDisableNewMrOption,
|
||||||
|
'commit/shouldCreateMR': shouldCreateMR,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
GlTooltip: createMockDirective(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vm.$destroy();
|
wrapper.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('for default branch', () => {
|
describe('when the `shouldHideNewMrOption` getter returns false', () => {
|
||||||
describe('is rendered when pushing to a new branch', () => {
|
beforeEach(() => {
|
||||||
beforeEach(() => {
|
createComponent();
|
||||||
createComponent({
|
jest.spyOn(store, 'dispatch').mockImplementation();
|
||||||
currentBranchId: 'main',
|
|
||||||
createNewBranch: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has NO new MR', () => {
|
|
||||||
expect(vm.$el.textContent).not.toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has new MR', async () => {
|
|
||||||
setMR();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
expect(vm.$el.textContent).not.toBe('');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('is NOT rendered when pushing to the same branch', () => {
|
it('renders an enabled new MR checkbox', () => {
|
||||||
|
expect(findCheckbox().attributes('disabled')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't add `is-disabled` class to the fieldset", () => {
|
||||||
|
expect(findFieldset().classes()).not.toContain('is-disabled');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches toggleShouldCreateMR when clicking checkbox', () => {
|
||||||
|
findCheckbox().vm.$emit('change');
|
||||||
|
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith('commit/toggleShouldCreateMR', undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when user cannot create an MR', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent({
|
createComponent({
|
||||||
currentBranchId: 'main',
|
shouldDisableNewMrOption: true,
|
||||||
createNewBranch: false,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has NO new MR', () => {
|
it('disables the new MR checkbox', () => {
|
||||||
expect(vm.$el.textContent).toBe('');
|
expect(findCheckbox().attributes('disabled')).toBe('true');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has new MR', async () => {
|
it('adds `is-disabled` class to the fieldset', () => {
|
||||||
setMR();
|
expect(findFieldset().classes()).toContain('is-disabled');
|
||||||
|
});
|
||||||
|
|
||||||
await nextTick();
|
it('shows a tooltip', () => {
|
||||||
expect(vm.$el.textContent).toBe('');
|
expect(findTooltip().value).toBe(wrapper.vm.$options.i18n.tooltipText);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('for protected branch', () => {
|
describe('when the `shouldHideNewMrOption` getter returns true', () => {
|
||||||
describe('when user does not have the write access', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
createComponent({
|
|
||||||
currentBranchId: 'protected/no-access',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is rendered if MR does not exists', () => {
|
|
||||||
expect(vm.$el.textContent).not.toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is rendered if MR exists', async () => {
|
|
||||||
setMR();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
expect(vm.$el.textContent).not.toBe('');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when user has the write access', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
createComponent({
|
|
||||||
currentBranchId: 'protected/access',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is rendered if MR does not exist', () => {
|
|
||||||
expect(vm.$el.textContent).not.toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('is hidden if MR exists', async () => {
|
|
||||||
setMR();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
expect(vm.$el.textContent).toBe('');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('for regular branch', () => {
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent({
|
createComponent({
|
||||||
currentBranchId: 'regular',
|
shouldHideNewMrOption: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is rendered if no MR exists', () => {
|
it("doesn't render the new MR checkbox", () => {
|
||||||
expect(vm.$el.textContent).not.toBe('');
|
expect(findCheckbox().exists()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is hidden if MR exists', async () => {
|
|
||||||
setMR();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
expect(vm.$el.textContent).toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows enablded checkbox', () => {
|
|
||||||
expect(findLabel().classList.contains('is-disabled')).toBe(false);
|
|
||||||
expect(findInput().disabled).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when user cannot create MR', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
setPermissions({ [PERMISSION_CREATE_MR]: false });
|
|
||||||
|
|
||||||
createComponent({ currentBranchId: 'regular' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('disabled checkbox', () => {
|
|
||||||
expect(findLabel().classList.contains('is-disabled')).toBe(true);
|
|
||||||
expect(findInput().disabled).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('dispatches toggleShouldCreateMR when clicking checkbox', () => {
|
|
||||||
createComponent({
|
|
||||||
currentBranchId: 'regular',
|
|
||||||
});
|
|
||||||
const el = vm.$el.querySelector('input[type="checkbox"]');
|
|
||||||
jest.spyOn(vm.$store, 'dispatch').mockImplementation(() => {});
|
|
||||||
el.dispatchEvent(new Event('change'));
|
|
||||||
|
|
||||||
expect(vm.$store.dispatch.mock.calls).toEqual(
|
|
||||||
expect.arrayContaining([['commit/toggleShouldCreateMR', expect.any(Object)]]),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,6 +18,7 @@ import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
|
||||||
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
|
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
|
||||||
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
|
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
|
||||||
import RunnerList from '~/runner/components/runner_list.vue';
|
import RunnerList from '~/runner/components/runner_list.vue';
|
||||||
|
import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue';
|
||||||
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
|
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
|
||||||
import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue';
|
import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue';
|
||||||
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
|
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
|
||||||
|
@ -50,6 +51,8 @@ import {
|
||||||
runnersDataPaginated,
|
runnersDataPaginated,
|
||||||
onlineContactTimeoutSecs,
|
onlineContactTimeoutSecs,
|
||||||
staleTimeoutSecs,
|
staleTimeoutSecs,
|
||||||
|
emptyStateSvgPath,
|
||||||
|
emptyStateFilteredSvgPath,
|
||||||
} from '../mock_data';
|
} from '../mock_data';
|
||||||
|
|
||||||
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
|
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
|
||||||
|
@ -78,6 +81,7 @@ describe('AdminRunnersApp', () => {
|
||||||
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
|
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
|
||||||
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
|
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
|
||||||
const findRunnerList = () => wrapper.findComponent(RunnerList);
|
const findRunnerList = () => wrapper.findComponent(RunnerList);
|
||||||
|
const findRunnerListEmptyState = () => wrapper.findComponent(RunnerListEmptyState);
|
||||||
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
|
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
|
||||||
const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page');
|
const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page');
|
||||||
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
|
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
|
||||||
|
@ -106,6 +110,8 @@ describe('AdminRunnersApp', () => {
|
||||||
localMutations,
|
localMutations,
|
||||||
onlineContactTimeoutSecs,
|
onlineContactTimeoutSecs,
|
||||||
staleTimeoutSecs,
|
staleTimeoutSecs,
|
||||||
|
emptyStateSvgPath,
|
||||||
|
emptyStateFilteredSvgPath,
|
||||||
...provide,
|
...provide,
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
|
@ -457,12 +463,28 @@ describe('AdminRunnersApp', () => {
|
||||||
runners: { nodes: [] },
|
runners: { nodes: [] },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
createComponent();
|
createComponent();
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows a message for no results', async () => {
|
it('shows an empty state', () => {
|
||||||
expect(wrapper.text()).toContain('No runners found');
|
expect(findRunnerListEmptyState().props('isSearchFiltered')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when a filter is selected by the user', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
findRunnerFilteredSearchBar().vm.$emit('input', {
|
||||||
|
runnerType: null,
|
||||||
|
filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }],
|
||||||
|
sort: CREATED_ASC,
|
||||||
|
});
|
||||||
|
await waitForPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows an empty state for a filtered search', () => {
|
||||||
|
expect(findRunnerListEmptyState().props('isSearchFiltered')).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import { GlBadge } from '@gitlab/ui';
|
|
||||||
import { mount } from '@vue/test-utils';
|
import { mount } from '@vue/test-utils';
|
||||||
import RunnerStatusCell from '~/runner/components/cells/runner_status_cell.vue';
|
import RunnerStatusCell from '~/runner/components/cells/runner_status_cell.vue';
|
||||||
|
|
||||||
|
import RunnerStatusBadge from '~/runner/components/runner_status_badge.vue';
|
||||||
|
import RunnerPausedBadge from '~/runner/components/runner_paused_badge.vue';
|
||||||
import { INSTANCE_TYPE, STATUS_ONLINE, STATUS_OFFLINE } from '~/runner/constants';
|
import { INSTANCE_TYPE, STATUS_ONLINE, STATUS_OFFLINE } from '~/runner/constants';
|
||||||
|
|
||||||
describe('RunnerTypeCell', () => {
|
describe('RunnerStatusCell', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
const findBadgeAt = (i) => wrapper.findAllComponents(GlBadge).at(i);
|
const findStatusBadge = () => wrapper.findComponent(RunnerStatusBadge);
|
||||||
|
const findPausedBadge = () => wrapper.findComponent(RunnerPausedBadge);
|
||||||
|
|
||||||
const createComponent = ({ runner = {} } = {}) => {
|
const createComponent = ({ runner = {} } = {}) => {
|
||||||
wrapper = mount(RunnerStatusCell, {
|
wrapper = mount(RunnerStatusCell, {
|
||||||
|
@ -29,7 +32,7 @@ describe('RunnerTypeCell', () => {
|
||||||
createComponent();
|
createComponent();
|
||||||
|
|
||||||
expect(wrapper.text()).toMatchInterpolatedText('online');
|
expect(wrapper.text()).toMatchInterpolatedText('online');
|
||||||
expect(findBadgeAt(0).text()).toBe('online');
|
expect(findStatusBadge().text()).toBe('online');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Displays offline status', () => {
|
it('Displays offline status', () => {
|
||||||
|
@ -40,7 +43,7 @@ describe('RunnerTypeCell', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(wrapper.text()).toMatchInterpolatedText('offline');
|
expect(wrapper.text()).toMatchInterpolatedText('offline');
|
||||||
expect(findBadgeAt(0).text()).toBe('offline');
|
expect(findStatusBadge().text()).toBe('offline');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Displays paused status', () => {
|
it('Displays paused status', () => {
|
||||||
|
@ -52,9 +55,7 @@ describe('RunnerTypeCell', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(wrapper.text()).toMatchInterpolatedText('online paused');
|
expect(wrapper.text()).toMatchInterpolatedText('online paused');
|
||||||
|
expect(findPausedBadge().text()).toBe('paused');
|
||||||
expect(findBadgeAt(0).text()).toBe('online');
|
|
||||||
expect(findBadgeAt(1).text()).toBe('paused');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Is empty when data is missing', () => {
|
it('Is empty when data is missing', () => {
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
|
||||||
|
import { s__ } from '~/locale';
|
||||||
|
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
|
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||||
|
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
|
||||||
|
|
||||||
|
import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue';
|
||||||
|
|
||||||
|
const mockSvgPath = 'mock-svg-path.svg';
|
||||||
|
const mockFilteredSvgPath = 'mock-filtered-svg-path.svg';
|
||||||
|
|
||||||
|
describe('RunnerListEmptyState', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
|
||||||
|
const findLink = () => wrapper.findComponent(GlLink);
|
||||||
|
const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
|
||||||
|
|
||||||
|
const createComponent = ({ props, mountFn = shallowMountExtended } = {}) => {
|
||||||
|
wrapper = mountFn(RunnerListEmptyState, {
|
||||||
|
propsData: {
|
||||||
|
svgPath: mockSvgPath,
|
||||||
|
filteredSvgPath: mockFilteredSvgPath,
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
GlModal: createMockDirective(),
|
||||||
|
},
|
||||||
|
stubs: {
|
||||||
|
GlEmptyState,
|
||||||
|
GlSprintf,
|
||||||
|
GlLink,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('when search is not filtered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders an illustration', () => {
|
||||||
|
expect(findEmptyState().props('svgPath')).toBe(mockSvgPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays "no results" text', () => {
|
||||||
|
const title = s__('Runners|Get started with runners');
|
||||||
|
const desc = 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.',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens a runner registration instructions modal with a link', () => {
|
||||||
|
const { value } = getBinding(findLink().element, 'gl-modal');
|
||||||
|
|
||||||
|
expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when search is filtered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createComponent({ props: { isSearchFiltered: true } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a "filtered search" illustration', () => {
|
||||||
|
expect(findEmptyState().props('svgPath')).toBe(mockFilteredSvgPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays "no filtered results" text', () => {
|
||||||
|
expect(findEmptyState().text()).toContain(s__('Runners|No results found'));
|
||||||
|
expect(findEmptyState().text()).toContain(s__('Runners|Edit your search and try again'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -16,6 +16,7 @@ import { updateHistory } from '~/lib/utils/url_utility';
|
||||||
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
|
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
|
||||||
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
|
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
|
||||||
import RunnerList from '~/runner/components/runner_list.vue';
|
import RunnerList from '~/runner/components/runner_list.vue';
|
||||||
|
import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue';
|
||||||
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
|
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
|
||||||
import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue';
|
import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue';
|
||||||
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
|
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
|
||||||
|
@ -48,6 +49,8 @@ import {
|
||||||
groupRunnersCountData,
|
groupRunnersCountData,
|
||||||
onlineContactTimeoutSecs,
|
onlineContactTimeoutSecs,
|
||||||
staleTimeoutSecs,
|
staleTimeoutSecs,
|
||||||
|
emptyStateSvgPath,
|
||||||
|
emptyStateFilteredSvgPath,
|
||||||
} from '../mock_data';
|
} from '../mock_data';
|
||||||
|
|
||||||
Vue.use(VueApollo);
|
Vue.use(VueApollo);
|
||||||
|
@ -75,6 +78,7 @@ describe('GroupRunnersApp', () => {
|
||||||
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
|
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
|
||||||
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
|
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
|
||||||
const findRunnerList = () => wrapper.findComponent(RunnerList);
|
const findRunnerList = () => wrapper.findComponent(RunnerList);
|
||||||
|
const findRunnerListEmptyState = () => wrapper.findComponent(RunnerListEmptyState);
|
||||||
const findRunnerRow = (id) => extendedWrapper(wrapper.findByTestId(`runner-row-${id}`));
|
const findRunnerRow = (id) => extendedWrapper(wrapper.findByTestId(`runner-row-${id}`));
|
||||||
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
|
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
|
||||||
const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page');
|
const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page');
|
||||||
|
@ -103,6 +107,8 @@ describe('GroupRunnersApp', () => {
|
||||||
provide: {
|
provide: {
|
||||||
onlineContactTimeoutSecs,
|
onlineContactTimeoutSecs,
|
||||||
staleTimeoutSecs,
|
staleTimeoutSecs,
|
||||||
|
emptyStateSvgPath,
|
||||||
|
emptyStateFilteredSvgPath,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -388,8 +394,8 @@ describe('GroupRunnersApp', () => {
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows a message for no results', async () => {
|
it('shows an empty state', async () => {
|
||||||
expect(wrapper.text()).toContain('No runners found');
|
expect(findRunnerListEmptyState().exists()).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,9 @@ import groupRunnersCountData from 'test_fixtures/graphql/runner/list/group_runne
|
||||||
export const onlineContactTimeoutSecs = 2 * 60 * 60;
|
export const onlineContactTimeoutSecs = 2 * 60 * 60;
|
||||||
export const staleTimeoutSecs = 7889238; // Ruby's `3.months`
|
export const staleTimeoutSecs = 7889238; // Ruby's `3.months`
|
||||||
|
|
||||||
|
export const emptyStateSvgPath = 'emptyStateSvgPath.svg';
|
||||||
|
export const emptyStateFilteredSvgPath = 'emptyStateFilteredSvgPath.svg';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
runnersData,
|
runnersData,
|
||||||
runnersDataPaginated,
|
runnersDataPaginated,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
fromUrlQueryToSearch,
|
fromUrlQueryToSearch,
|
||||||
fromSearchToUrl,
|
fromSearchToUrl,
|
||||||
fromSearchToVariables,
|
fromSearchToVariables,
|
||||||
|
isSearchFiltered,
|
||||||
} from '~/runner/runner_search_utils';
|
} from '~/runner/runner_search_utils';
|
||||||
|
|
||||||
describe('search_params.js', () => {
|
describe('search_params.js', () => {
|
||||||
|
@ -14,6 +15,7 @@ describe('search_params.js', () => {
|
||||||
urlQuery: '',
|
urlQuery: '',
|
||||||
search: { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' },
|
search: { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' },
|
||||||
graphqlVariables: { sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
|
graphqlVariables: { sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
|
||||||
|
isDefault: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'a single status',
|
name: 'a single status',
|
||||||
|
@ -268,7 +270,7 @@ describe('search_params.js', () => {
|
||||||
describe('fromSearchToUrl', () => {
|
describe('fromSearchToUrl', () => {
|
||||||
examples.forEach(({ name, urlQuery, search }) => {
|
examples.forEach(({ name, urlQuery, search }) => {
|
||||||
it(`Converts ${name} to a url`, () => {
|
it(`Converts ${name} to a url`, () => {
|
||||||
expect(fromSearchToUrl(search)).toEqual(`http://test.host/${urlQuery}`);
|
expect(fromSearchToUrl(search)).toBe(`http://test.host/${urlQuery}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -280,7 +282,7 @@ describe('search_params.js', () => {
|
||||||
const search = { filters: [], sort: 'CREATED_DESC' };
|
const search = { filters: [], sort: 'CREATED_DESC' };
|
||||||
const expectedUrl = `http://test.host/`;
|
const expectedUrl = `http://test.host/`;
|
||||||
|
|
||||||
expect(fromSearchToUrl(search, initalUrl)).toEqual(expectedUrl);
|
expect(fromSearchToUrl(search, initalUrl)).toBe(expectedUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('When unrelated search parameter is present, it does not get removed', () => {
|
it('When unrelated search parameter is present, it does not get removed', () => {
|
||||||
|
@ -288,7 +290,7 @@ describe('search_params.js', () => {
|
||||||
const search = { filters: [], sort: 'CREATED_DESC' };
|
const search = { filters: [], sort: 'CREATED_DESC' };
|
||||||
const expectedUrl = `http://test.host/?unrelated=UNRELATED`;
|
const expectedUrl = `http://test.host/?unrelated=UNRELATED`;
|
||||||
|
|
||||||
expect(fromSearchToUrl(search, initialUrl)).toEqual(expectedUrl);
|
expect(fromSearchToUrl(search, initialUrl)).toBe(expectedUrl);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -331,4 +333,16 @@ describe('search_params.js', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isSearchFiltered', () => {
|
||||||
|
examples.forEach(({ name, search, isDefault }) => {
|
||||||
|
it(`Given ${name}, evaluates to ${isDefault ? 'not ' : ''}filtered`, () => {
|
||||||
|
expect(isSearchFiltered(search)).toBe(!isDefault);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('given a missing pagination, evaluates as not filtered', () => {
|
||||||
|
expect(isSearchFiltered({ pagination: null })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -207,10 +207,10 @@ export const commit = async ({ newBranch = false, newMR = false, newBranchName =
|
||||||
|
|
||||||
if (!newBranch) {
|
if (!newBranch) {
|
||||||
const option = await screen.findByLabelText(/Commit to .+ branch/);
|
const option = await screen.findByLabelText(/Commit to .+ branch/);
|
||||||
option.click();
|
await option.click();
|
||||||
} else {
|
} else {
|
||||||
const option = await screen.findByLabelText('Create a new branch');
|
const option = await screen.findByLabelText('Create a new branch');
|
||||||
option.click();
|
await option.click();
|
||||||
|
|
||||||
const branchNameInput = await screen.findByTestId('ide-new-branch-name');
|
const branchNameInput = await screen.findByTestId('ide-new-branch-name');
|
||||||
fireEvent.input(branchNameInput, { target: { value: newBranchName } });
|
fireEvent.input(branchNameInput, { target: { value: newBranchName } });
|
||||||
|
|
|
@ -27,11 +27,9 @@ RSpec.describe ResolvesGroups do
|
||||||
|
|
||||||
let_it_be(:lookahead_fields) do
|
let_it_be(:lookahead_fields) do
|
||||||
<<~FIELDS
|
<<~FIELDS
|
||||||
contacts { nodes { id } }
|
|
||||||
containerRepositoriesCount
|
containerRepositoriesCount
|
||||||
customEmoji { nodes { id } }
|
customEmoji { nodes { id } }
|
||||||
fullPath
|
fullPath
|
||||||
organizations { nodes { id } }
|
|
||||||
path
|
path
|
||||||
dependencyProxyBlobCount
|
dependencyProxyBlobCount
|
||||||
dependencyProxyBlobs { nodes { fileName } }
|
dependencyProxyBlobs { nodes { fileName } }
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Resolvers::Crm::ContactsResolver do
|
||||||
|
include GraphqlHelpers
|
||||||
|
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
let_it_be(:group) { create(:group, :crm_enabled) }
|
||||||
|
|
||||||
|
let_it_be(:contact_a) do
|
||||||
|
create(
|
||||||
|
:contact,
|
||||||
|
group: group,
|
||||||
|
first_name: "ABC",
|
||||||
|
last_name: "DEF",
|
||||||
|
email: "ghi@test.com",
|
||||||
|
description: "LMNO",
|
||||||
|
state: "inactive"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let_it_be(:contact_b) do
|
||||||
|
create(
|
||||||
|
:contact,
|
||||||
|
group: group,
|
||||||
|
first_name: "PQR",
|
||||||
|
last_name: "STU",
|
||||||
|
email: "vwx@test.com",
|
||||||
|
description: "YZ",
|
||||||
|
state: "active"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#resolve' do
|
||||||
|
context 'with unauthorized user' do
|
||||||
|
it 'does not rise an error and returns no contacts' do
|
||||||
|
expect { resolve_contacts(group) }.not_to raise_error
|
||||||
|
expect(resolve_contacts(group)).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with authorized user' do
|
||||||
|
it 'does not rise an error and returns all contacts' do
|
||||||
|
group.add_reporter(user)
|
||||||
|
|
||||||
|
expect { resolve_contacts(group) }.not_to raise_error
|
||||||
|
expect(resolve_contacts(group)).to eq([contact_a, contact_b])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without parent' do
|
||||||
|
it 'returns no contacts' do
|
||||||
|
expect(resolve_contacts(nil)).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a group parent' do
|
||||||
|
before do
|
||||||
|
group.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when no filter is provided' do
|
||||||
|
it 'returns all the contacts' do
|
||||||
|
expect(resolve_contacts(group)).to match_array([contact_a, contact_b])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when search term is provided' do
|
||||||
|
it 'returns the correct contacts' do
|
||||||
|
expect(resolve_contacts(group, { search: "x@test.com" })).to match_array([contact_b])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when state is provided' do
|
||||||
|
it 'returns the correct contacts' do
|
||||||
|
expect(resolve_contacts(group, { state: :inactive })).to match_array([contact_a])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve_contacts(parent, args = {}, context = { current_user: user })
|
||||||
|
resolve(described_class, obj: parent, args: args, ctx: context)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,80 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Resolvers::Crm::OrganizationsResolver do
|
||||||
|
include GraphqlHelpers
|
||||||
|
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
let_it_be(:group) { create(:group, :crm_enabled) }
|
||||||
|
|
||||||
|
let_it_be(:organization_a) do
|
||||||
|
create(
|
||||||
|
:organization,
|
||||||
|
group: group,
|
||||||
|
name: "ABC",
|
||||||
|
state: "inactive"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let_it_be(:organization_b) do
|
||||||
|
create(
|
||||||
|
:organization,
|
||||||
|
group: group,
|
||||||
|
name: "DEF",
|
||||||
|
state: "active"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#resolve' do
|
||||||
|
context 'with unauthorized user' do
|
||||||
|
it 'does not rise an error and returns no organizations' do
|
||||||
|
expect { resolve_organizations(group) }.not_to raise_error
|
||||||
|
expect(resolve_organizations(group)).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with authorized user' do
|
||||||
|
it 'does not rise an error and returns all organizations' do
|
||||||
|
group.add_reporter(user)
|
||||||
|
|
||||||
|
expect { resolve_organizations(group) }.not_to raise_error
|
||||||
|
expect(resolve_organizations(group)).to eq([organization_a, organization_b])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without parent' do
|
||||||
|
it 'returns no organizations' do
|
||||||
|
expect(resolve_organizations(nil)).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a group parent' do
|
||||||
|
before do
|
||||||
|
group.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when no filter is provided' do
|
||||||
|
it 'returns all the organizations' do
|
||||||
|
expect(resolve_organizations(group)).to match_array([organization_a, organization_b])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when search term is provided' do
|
||||||
|
it 'returns the correct organizations' do
|
||||||
|
expect(resolve_organizations(group, { search: "def" })).to match_array([organization_b])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when state is provided' do
|
||||||
|
it 'returns the correct organizations' do
|
||||||
|
expect(resolve_organizations(group, { state: :inactive })).to match_array([organization_a])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve_organizations(parent, args = {}, context = { current_user: user })
|
||||||
|
resolve(described_class, obj: parent, args: args, ctx: context)
|
||||||
|
end
|
||||||
|
end
|
|
@ -84,12 +84,14 @@ RSpec.describe Ci::RunnersHelper do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the data in format' do
|
it 'returns the data in format' do
|
||||||
expect(helper.admin_runners_data_attributes).to eq({
|
expect(helper.admin_runners_data_attributes).to include(
|
||||||
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
|
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
|
||||||
registration_token: Gitlab::CurrentSettings.runners_registration_token,
|
registration_token: Gitlab::CurrentSettings.runners_registration_token,
|
||||||
online_contact_timeout_secs: 7200,
|
online_contact_timeout_secs: 7200,
|
||||||
stale_timeout_secs: 7889238
|
stale_timeout_secs: 7889238,
|
||||||
})
|
empty_state_svg_path: start_with('/assets/illustrations/pipelines_empty'),
|
||||||
|
empty_state_filtered_svg_path: start_with('/assets/illustrations/magnifying-glass')
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -130,14 +132,16 @@ RSpec.describe Ci::RunnersHelper do
|
||||||
let(:group) { create(:group) }
|
let(:group) { create(:group) }
|
||||||
|
|
||||||
it 'returns group data to render a runner list' do
|
it 'returns group data to render a runner list' do
|
||||||
expect(helper.group_runners_data_attributes(group)).to eq({
|
expect(helper.group_runners_data_attributes(group)).to include(
|
||||||
registration_token: group.runners_token,
|
registration_token: group.runners_token,
|
||||||
group_id: group.id,
|
group_id: group.id,
|
||||||
group_full_path: group.full_path,
|
group_full_path: group.full_path,
|
||||||
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
|
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
|
||||||
online_contact_timeout_secs: 7200,
|
online_contact_timeout_secs: 7200,
|
||||||
stale_timeout_secs: 7889238
|
stale_timeout_secs: 7889238,
|
||||||
})
|
empty_state_svg_path: start_with('/assets/illustrations/pipelines_empty'),
|
||||||
|
empty_state_filtered_svg_path: start_with('/assets/illustrations/magnifying-glass')
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Gitlab::BackgroundMigration::SetLegacyOpenSourceLicenseAvailableForNonPublicProjects,
|
||||||
|
:migration,
|
||||||
|
schema: 20220520040416 do
|
||||||
|
let(:namespaces_table) { table(:namespaces) }
|
||||||
|
let(:projects_table) { table(:projects) }
|
||||||
|
let(:project_settings_table) { table(:project_settings) }
|
||||||
|
|
||||||
|
subject(:perform_migration) do
|
||||||
|
described_class.new(start_id: 1,
|
||||||
|
end_id: 30,
|
||||||
|
batch_table: :projects,
|
||||||
|
batch_column: :id,
|
||||||
|
sub_batch_size: 2,
|
||||||
|
pause_ms: 0,
|
||||||
|
connection: ActiveRecord::Base.connection)
|
||||||
|
.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:queries) { ActiveRecord::QueryRecorder.new { perform_migration } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
namespaces_table.create!(id: 1, name: 'namespace', path: 'namespace-path-1')
|
||||||
|
namespaces_table.create!(id: 2, name: 'namespace', path: 'namespace-path-2', type: 'Project')
|
||||||
|
namespaces_table.create!(id: 3, name: 'namespace', path: 'namespace-path-3', type: 'Project')
|
||||||
|
namespaces_table.create!(id: 4, name: 'namespace', path: 'namespace-path-4', type: 'Project')
|
||||||
|
|
||||||
|
projects_table
|
||||||
|
.create!(id: 11, name: 'proj-1', path: 'path-1', namespace_id: 1, project_namespace_id: 2, visibility_level: 0)
|
||||||
|
projects_table
|
||||||
|
.create!(id: 12, name: 'proj-2', path: 'path-2', namespace_id: 1, project_namespace_id: 3, visibility_level: 10)
|
||||||
|
projects_table
|
||||||
|
.create!(id: 13, name: 'proj-3', path: 'path-3', namespace_id: 1, project_namespace_id: 4, visibility_level: 20)
|
||||||
|
|
||||||
|
project_settings_table.create!(project_id: 11, legacy_open_source_license_available: true)
|
||||||
|
project_settings_table.create!(project_id: 12, legacy_open_source_license_available: true)
|
||||||
|
project_settings_table.create!(project_id: 13, legacy_open_source_license_available: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets `legacy_open_source_license_available` attribute to false for non-public projects', :aggregate_failures do
|
||||||
|
expect(queries.count).to eq(3)
|
||||||
|
|
||||||
|
expect(migrated_attribute(11)).to be_falsey
|
||||||
|
expect(migrated_attribute(12)).to be_falsey
|
||||||
|
expect(migrated_attribute(13)).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
def migrated_attribute(project_id)
|
||||||
|
project_settings_table.find(project_id).legacy_open_source_license_available
|
||||||
|
end
|
||||||
|
end
|
|
@ -31,6 +31,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::RateLimit, :freeze_time, :c
|
||||||
context 'when the limit is exceeded' do
|
context 'when the limit is exceeded' do
|
||||||
before do
|
before do
|
||||||
stub_application_setting(pipeline_limit_per_project_user_sha: 1)
|
stub_application_setting(pipeline_limit_per_project_user_sha: 1)
|
||||||
|
stub_feature_flags(ci_enforce_throttle_pipelines_creation_override: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not persist the pipeline' do
|
it 'does not persist the pipeline' do
|
||||||
|
@ -52,7 +53,9 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::RateLimit, :freeze_time, :c
|
||||||
class: described_class.name,
|
class: described_class.name,
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
subscription_plan: project.actual_plan_name,
|
subscription_plan: project.actual_plan_name,
|
||||||
commit_sha: command.sha
|
commit_sha: command.sha,
|
||||||
|
throttled: true,
|
||||||
|
throttle_override: false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -121,7 +124,42 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::RateLimit, :freeze_time, :c
|
||||||
class: described_class.name,
|
class: described_class.name,
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
subscription_plan: project.actual_plan_name,
|
subscription_plan: project.actual_plan_name,
|
||||||
commit_sha: command.sha
|
commit_sha: command.sha,
|
||||||
|
throttled: false,
|
||||||
|
throttle_override: false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
perform
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when ci_enforce_throttle_pipelines_creation_override is enabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(ci_enforce_throttle_pipelines_creation_override: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not break the chain' do
|
||||||
|
perform
|
||||||
|
|
||||||
|
expect(step.break?).to be_falsey
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not invalidate the pipeline' do
|
||||||
|
perform
|
||||||
|
|
||||||
|
expect(pipeline.errors).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a log entry' do
|
||||||
|
expect(Gitlab::AppJsonLogger).to receive(:info).with(
|
||||||
|
a_hash_including(
|
||||||
|
class: described_class.name,
|
||||||
|
project_id: project.id,
|
||||||
|
subscription_plan: project.actual_plan_name,
|
||||||
|
commit_sha: command.sha,
|
||||||
|
throttled: false,
|
||||||
|
throttle_override: true
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
require_migration!
|
||||||
|
|
||||||
|
RSpec.describe ScheduleSetLegacyOpenSourceLicenseAvailableForNonPublicProjects do
|
||||||
|
context 'on gitlab.com' do
|
||||||
|
let(:migration) { described_class::MIGRATION }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Gitlab).to receive(:com?).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#up' do
|
||||||
|
it 'schedules background jobs for each batch of projects' do
|
||||||
|
migrate!
|
||||||
|
|
||||||
|
expect(migration).to(
|
||||||
|
have_scheduled_batched_migration(
|
||||||
|
table_name: :projects,
|
||||||
|
column_name: :id,
|
||||||
|
interval: described_class::INTERVAL,
|
||||||
|
batch_size: described_class::BATCH_SIZE,
|
||||||
|
sub_batch_size: described_class::SUB_BATCH_SIZE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#down' do
|
||||||
|
it 'deletes all batched migration records' do
|
||||||
|
migrate!
|
||||||
|
schema_migrate_down!
|
||||||
|
|
||||||
|
expect(migration).not_to have_scheduled_batched_migration
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'on self-managed instance' do
|
||||||
|
let(:migration) { described_class.new }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Gitlab).to receive(:com?).and_return(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#up' do
|
||||||
|
it 'does not schedule background job' do
|
||||||
|
expect(migration).not_to receive(:queue_batched_background_migration)
|
||||||
|
|
||||||
|
migration.up
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#down' do
|
||||||
|
it 'does not delete background job' do
|
||||||
|
expect(migration).not_to receive(:delete_batched_background_migration)
|
||||||
|
|
||||||
|
migration.down
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -5071,6 +5071,18 @@ RSpec.describe Ci::Build do
|
||||||
|
|
||||||
build.execute_hooks
|
build.execute_hooks
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with blocked users' do
|
||||||
|
before do
|
||||||
|
allow(build).to receive(:user) { FactoryBot.build(:user, :blocked) }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not call project.execute_hooks' do
|
||||||
|
expect(build.project).not_to receive(:execute_hooks)
|
||||||
|
|
||||||
|
build.execute_hooks
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'without project hooks' do
|
context 'without project hooks' do
|
||||||
|
|
|
@ -3056,7 +3056,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'hooks trigerring' do
|
describe 'hooks trigerring' do
|
||||||
let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) }
|
let_it_be_with_reload(:pipeline) { create(:ci_empty_pipeline, :created) }
|
||||||
|
|
||||||
%i[
|
%i[
|
||||||
enqueue
|
enqueue
|
||||||
|
@ -3076,7 +3076,19 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
||||||
it 'schedules a new PipelineHooksWorker job' do
|
it 'schedules a new PipelineHooksWorker job' do
|
||||||
expect(PipelineHooksWorker).to receive(:perform_async).with(pipeline.id)
|
expect(PipelineHooksWorker).to receive(:perform_async).with(pipeline.id)
|
||||||
|
|
||||||
pipeline.reload.public_send(pipeline_action)
|
pipeline.public_send(pipeline_action)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with blocked users' do
|
||||||
|
before do
|
||||||
|
allow(pipeline).to receive(:user) { build(:user, :blocked) }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not schedule a new PipelineHooksWorker job' do
|
||||||
|
expect(PipelineHooksWorker).not_to receive(:perform_async)
|
||||||
|
|
||||||
|
pipeline.public_send(pipeline_action)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -3636,6 +3648,18 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
||||||
pipeline.succeed!
|
pipeline.succeed!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the user is blocked' do
|
||||||
|
before do
|
||||||
|
pipeline.user.block!
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not enqueue PipelineNotificationWorker' do
|
||||||
|
expect(PipelineNotificationWorker).not_to receive(:perform_async)
|
||||||
|
|
||||||
|
pipeline.succeed
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with failed pipeline' do
|
context 'with failed pipeline' do
|
||||||
|
@ -3656,6 +3680,18 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
||||||
|
|
||||||
pipeline.drop
|
pipeline.drop
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the user is blocked' do
|
||||||
|
before do
|
||||||
|
pipeline.user.block!
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not enqueue PipelineNotificationWorker' do
|
||||||
|
expect(PipelineNotificationWorker).not_to receive(:perform_async)
|
||||||
|
|
||||||
|
pipeline.drop
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with skipped pipeline' do
|
context 'with skipped pipeline' do
|
||||||
|
|
|
@ -11,6 +11,7 @@ RSpec.describe Ci::CreatePipelineService, :freeze_time, :clean_gitlab_redis_rate
|
||||||
before do
|
before do
|
||||||
stub_ci_pipeline_yaml_file(gitlab_ci_yaml)
|
stub_ci_pipeline_yaml_file(gitlab_ci_yaml)
|
||||||
stub_application_setting(pipeline_limit_per_project_user_sha: 1)
|
stub_application_setting(pipeline_limit_per_project_user_sha: 1)
|
||||||
|
stub_feature_flags(ci_enforce_throttle_pipelines_creation_override: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user is under the limit' do
|
context 'when user is under the limit' do
|
||||||
|
|
|
@ -205,6 +205,25 @@ RSpec.describe Projects::UpdatePagesService do
|
||||||
include_examples 'fails with outdated reference message'
|
include_examples 'fails with outdated reference message'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when uploaded deployment size is wrong' do
|
||||||
|
it 'raises an error' do
|
||||||
|
allow_next_instance_of(PagesDeployment) do |deployment|
|
||||||
|
allow(deployment)
|
||||||
|
.to receive(:size)
|
||||||
|
.and_return(file.size + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect do
|
||||||
|
expect(execute).not_to eq(:success)
|
||||||
|
|
||||||
|
expect(GenericCommitStatus.last.description).to eq("Error: The uploaded artifact size does not match the expected value.")
|
||||||
|
project.pages_metadatum.reload
|
||||||
|
expect(project.pages_metadatum).not_to be_deployed
|
||||||
|
expect(project.pages_metadatum.pages_deployment).to be_ni
|
||||||
|
end.to raise_error(Projects::UpdatePagesService::WrongUploadedDeploymentSizeError)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,7 @@ RSpec.shared_examples 'shows and resets runner registration token' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
RSpec.shared_examples 'shows no runners' do
|
RSpec.shared_examples 'shows no runners registered' do
|
||||||
it 'shows counts with 0' do
|
it 'shows counts with 0' do
|
||||||
expect(page).to have_text "Online runners 0"
|
expect(page).to have_text "Online runners 0"
|
||||||
expect(page).to have_text "Offline runners 0"
|
expect(page).to have_text "Offline runners 0"
|
||||||
|
@ -70,13 +70,19 @@ RSpec.shared_examples 'shows no runners' do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows "no runners" message' do
|
it 'shows "no runners" message' do
|
||||||
expect(page).to have_text 'No runners found'
|
expect(page).to have_text s_('Runners|Get started with runners')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.shared_examples 'shows no runners found' do
|
||||||
|
it 'shows "no runners" message' do
|
||||||
|
expect(page).to have_text s_('Runners|No results found')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
RSpec.shared_examples 'shows runner in list' do
|
RSpec.shared_examples 'shows runner in list' do
|
||||||
it 'does not show empty state' do
|
it 'does not show empty state' do
|
||||||
expect(page).not_to have_content 'No runners found'
|
expect(page).not_to have_content s_('Runners|Get started with runners')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows runner row' do
|
it 'shows runner row' do
|
||||||
|
|
|
@ -25,6 +25,16 @@ RSpec.describe PipelineHooksWorker do
|
||||||
.not_to raise_error
|
.not_to raise_error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the user is blocked' do
|
||||||
|
let(:pipeline) { create(:ci_pipeline, user: create(:user, :blocked)) }
|
||||||
|
|
||||||
|
it 'returns early without executing' do
|
||||||
|
expect(Ci::Pipelines::HookService).not_to receive(:new)
|
||||||
|
|
||||||
|
described_class.new.perform(pipeline.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'worker with data consistency',
|
it_behaves_like 'worker with data consistency',
|
||||||
|
|
|
@ -21,6 +21,20 @@ RSpec.describe PipelineNotificationWorker, :mailer do
|
||||||
subject.perform(non_existing_record_id)
|
subject.perform(non_existing_record_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the user is blocked' do
|
||||||
|
before do
|
||||||
|
expect_next_found_instance_of(Ci::Pipeline) do |pipeline|
|
||||||
|
allow(pipeline).to receive(:user) { build(:user, :blocked) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does nothing' do
|
||||||
|
expect(NotificationService).not_to receive(:new)
|
||||||
|
|
||||||
|
subject.perform(pipeline.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it_behaves_like 'worker with data consistency',
|
it_behaves_like 'worker with data consistency',
|
||||||
described_class,
|
described_class,
|
||||||
data_consistency: :delayed
|
data_consistency: :delayed
|
||||||
|
|
|
@ -963,10 +963,10 @@
|
||||||
stylelint-declaration-strict-value "1.8.0"
|
stylelint-declaration-strict-value "1.8.0"
|
||||||
stylelint-scss "4.1.0"
|
stylelint-scss "4.1.0"
|
||||||
|
|
||||||
"@gitlab/svgs@2.17.0":
|
"@gitlab/svgs@2.18.0":
|
||||||
version "2.17.0"
|
version "2.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.17.0.tgz#56d0d11744859b3e1da80dedab2396a95cd01a02"
|
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.18.0.tgz#aafff929bc5365f7cad736b6d061895b3f9aa381"
|
||||||
integrity sha512-+cmn4ptdOFjSC8ByqD41kj1xSQ9/YFYLq/Es+jy5t12HmUtvYL8YRfNTlvApReSJ8SM7scwleVy4S19M15Siqw==
|
integrity sha512-Okbm4dAAf/aiaRojUT57yfqY/TVka/zAXN4T+hOx/Yho6wUT2eAJ8CcFpctPdt3kUNM4bHU2CZYoGqklbtXkmg==
|
||||||
|
|
||||||
"@gitlab/ui@40.7.1":
|
"@gitlab/ui@40.7.1":
|
||||||
version "40.7.1"
|
version "40.7.1"
|
||||||
|
|
Loading…
Reference in New Issue