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 $ from 'jquery';
|
||||
import { uniq } from 'lodash';
|
||||
import { getEmojiScoreWithIntent } from '~/emoji/utils';
|
||||
import { getCookie, setCookie, scrollToElement } from '~/lib/utils/common_utils';
|
||||
import * as Emoji from '~/emoji';
|
||||
|
||||
import { dispose, fixTitle } from '~/tooltips';
|
||||
import createFlash from './flash';
|
||||
import axios from './lib/utils/axios_utils';
|
||||
|
@ -559,13 +559,45 @@ export class AwardsHandler {
|
|||
}
|
||||
}
|
||||
|
||||
getEmojiScore(emojis, value) {
|
||||
const elem = $(value).find('[data-name]').get(0);
|
||||
const emoji = emojis.filter((x) => x.emoji.name === elem.dataset.name)[0];
|
||||
elem.dataset.score = emoji.score;
|
||||
|
||||
return emoji.score;
|
||||
}
|
||||
|
||||
sortEmojiElements(emojis, $elements) {
|
||||
const scores = new WeakMap();
|
||||
|
||||
return $elements.sort((a, b) => {
|
||||
let aScore = scores.get(a);
|
||||
let bScore = scores.get(b);
|
||||
|
||||
if (!aScore) {
|
||||
aScore = this.getEmojiScore(emojis, a);
|
||||
scores.set(a, aScore);
|
||||
}
|
||||
|
||||
if (!bScore) {
|
||||
bScore = this.getEmojiScore(emojis, b);
|
||||
scores.set(b, bScore);
|
||||
}
|
||||
|
||||
return aScore - bScore;
|
||||
});
|
||||
}
|
||||
|
||||
findMatchingEmojiElements(query) {
|
||||
const emojiMatches = this.emoji.searchEmoji(query).map((x) => x.emoji.name);
|
||||
const matchingEmoji = this.emoji
|
||||
.searchEmoji(query)
|
||||
.map((x) => ({ ...x, score: getEmojiScoreWithIntent(x.emoji.name, x.score) }));
|
||||
const matchingEmojiNames = matchingEmoji.map((x) => x.emoji.name);
|
||||
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
|
||||
const $matchingElements = $emojiElements.filter(
|
||||
(i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0,
|
||||
(i, elm) => matchingEmojiNames.indexOf(elm.dataset.name) >= 0,
|
||||
);
|
||||
return $matchingElements.closest('li').clone();
|
||||
return this.sortEmojiElements(matchingEmoji, $matchingElements.closest('li').clone());
|
||||
}
|
||||
|
||||
/* showMenuElement and hideMenuElement are performance optimizations. We use
|
||||
|
|
|
@ -19,3 +19,5 @@ export const CATEGORY_ROW_HEIGHT = 37;
|
|||
|
||||
export const CACHE_VERSION_KEY = 'gl-emoji-map-version';
|
||||
export const CACHE_KEY = 'gl-emoji-map';
|
||||
|
||||
export const NEUTRAL_INTENT_MULTIPLIER = 1;
|
||||
|
|
|
@ -2,6 +2,7 @@ import { escape, minBy } from 'lodash';
|
|||
import emojiRegexFactory from 'emoji-regex';
|
||||
import emojiAliases from 'emojis/aliases.json';
|
||||
import { setAttributes } from '~/lib/utils/dom_utils';
|
||||
import { getEmojiScoreWithIntent } from '~/emoji/utils';
|
||||
import AccessorUtilities from '../lib/utils/accessor';
|
||||
import axios from '../lib/utils/axios_utils';
|
||||
import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants';
|
||||
|
@ -144,6 +145,11 @@ function getNameMatch(emoji, query) {
|
|||
return null;
|
||||
}
|
||||
|
||||
// Sort emoji by emoji score falling back to a string comparison
|
||||
export function sortEmoji(a, b) {
|
||||
return a.score - b.score || a.fieldValue.localeCompare(b.fieldValue);
|
||||
}
|
||||
|
||||
export function searchEmoji(query) {
|
||||
const lowercaseQuery = query ? `${query}`.toLowerCase() : '';
|
||||
|
||||
|
@ -156,16 +162,14 @@ export function searchEmoji(query) {
|
|||
getDescriptionMatch(emoji, lowercaseQuery),
|
||||
getAliasMatch(emoji, matchingAliases),
|
||||
getNameMatch(emoji, lowercaseQuery),
|
||||
].filter(Boolean);
|
||||
]
|
||||
.filter(Boolean)
|
||||
.map((x) => ({ ...x, score: getEmojiScoreWithIntent(x.emoji.name, x.score) }));
|
||||
|
||||
return minBy(matches, (x) => x.score);
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function sortEmoji(items) {
|
||||
// Sort results by index of and string comparison
|
||||
return [...items].sort((a, b) => a.score - b.score || a.fieldValue.localeCompare(b.fieldValue));
|
||||
.filter(Boolean)
|
||||
.sort(sortEmoji);
|
||||
}
|
||||
|
||||
export const CATEGORY_NAMES = Object.keys(CATEGORY_ICON_MAP);
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
sorter(items) {
|
||||
return Emoji.sortEmoji(items);
|
||||
return items.sort(Emoji.sortEmoji);
|
||||
},
|
||||
};
|
||||
// Team Members
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlTooltipDirective, GlFormCheckbox } from '@gitlab/ui';
|
||||
import { createNamespacedHelpers } from 'vuex';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
|
@ -8,19 +8,20 @@ const { mapActions: mapCommitActions, mapGetters: mapCommitGetters } = createNam
|
|||
);
|
||||
|
||||
export default {
|
||||
components: { GlFormCheckbox },
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
i18n: {
|
||||
newMrText: s__('IDE|Start a new merge request'),
|
||||
tooltipText: s__(
|
||||
'IDE|This option is disabled because you are not allowed to create merge requests in this project.',
|
||||
),
|
||||
},
|
||||
computed: {
|
||||
...mapCommitGetters(['shouldHideNewMrOption', 'shouldDisableNewMrOption', 'shouldCreateMR']),
|
||||
tooltipText() {
|
||||
if (this.shouldDisableNewMrOption) {
|
||||
return s__(
|
||||
'IDE|This option is disabled because you are not allowed to create merge requests in this project.',
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
return this.shouldDisableNewMrOption ? this.$options.i18n.tooltipText : null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -30,22 +31,23 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<fieldset v-if="!shouldHideNewMrOption">
|
||||
<hr class="my-2" />
|
||||
<label
|
||||
v-gl-tooltip="tooltipText"
|
||||
class="mb-0 js-ide-commit-new-mr"
|
||||
:class="{ 'is-disabled': shouldDisableNewMrOption }"
|
||||
<fieldset
|
||||
v-if="!shouldHideNewMrOption"
|
||||
v-gl-tooltip="tooltipText"
|
||||
data-testid="new-merge-request-fieldset"
|
||||
class="js-ide-commit-new-mr"
|
||||
:class="{ 'is-disabled': shouldDisableNewMrOption }"
|
||||
>
|
||||
<hr class="gl-mt-3 gl-mb-4" />
|
||||
|
||||
<gl-form-checkbox
|
||||
:disabled="shouldDisableNewMrOption"
|
||||
:checked="shouldCreateMR"
|
||||
@change="toggleShouldCreateMR"
|
||||
>
|
||||
<input
|
||||
:disabled="shouldDisableNewMrOption"
|
||||
:checked="shouldCreateMR"
|
||||
type="checkbox"
|
||||
@change="toggleShouldCreateMR"
|
||||
/>
|
||||
<span class="gl-ml-3 ide-option-label">
|
||||
{{ __('Start a new merge request') }}
|
||||
<span class="ide-option-label">
|
||||
{{ $options.i18n.newMrText }}
|
||||
</span>
|
||||
</label>
|
||||
</gl-form-checkbox>
|
||||
</fieldset>
|
||||
</template>
|
||||
|
|
|
@ -1,8 +1,20 @@
|
|||
<script>
|
||||
import { GlTooltipDirective } from '@gitlab/ui';
|
||||
import {
|
||||
GlTooltipDirective,
|
||||
GlFormRadio,
|
||||
GlFormRadioGroup,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
} from '@gitlab/ui';
|
||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlFormRadio,
|
||||
GlFormRadioGroup,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
|
@ -51,35 +63,42 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<fieldset>
|
||||
<label
|
||||
<fieldset class="gl-mb-2">
|
||||
<gl-form-radio-group
|
||||
v-gl-tooltip="tooltipTitle"
|
||||
:checked="commitAction"
|
||||
:class="{
|
||||
'is-disabled': disabled,
|
||||
}"
|
||||
>
|
||||
<input
|
||||
<gl-form-radio
|
||||
:value="value"
|
||||
:checked="commitAction === value"
|
||||
:disabled="disabled"
|
||||
type="radio"
|
||||
name="commit-action"
|
||||
data-qa-selector="commit_type_radio"
|
||||
@change="updateCommitAction($event.target.value)"
|
||||
/>
|
||||
<span class="gl-ml-3">
|
||||
<span v-if="label" class="ide-option-label"> {{ label }} </span> <slot v-else></slot>
|
||||
</span>
|
||||
</label>
|
||||
<div v-if="commitAction === value && showInput" class="ide-commit-new-branch">
|
||||
<input
|
||||
@change="updateCommitAction(value)"
|
||||
>
|
||||
<span v-if="label" class="ide-option-label">
|
||||
{{ label }}
|
||||
</span>
|
||||
<slot v-else></slot>
|
||||
</gl-form-radio>
|
||||
</gl-form-radio-group>
|
||||
|
||||
<gl-form-group
|
||||
v-if="commitAction === value && showInput"
|
||||
:label="placeholderBranchName"
|
||||
:label-sr-only="true"
|
||||
class="gl-ml-6 gl-mb-0"
|
||||
>
|
||||
<gl-form-input
|
||||
:placeholder="placeholderBranchName"
|
||||
:value="newBranchName"
|
||||
:disabled="disabled"
|
||||
data-testid="ide-new-branch-name"
|
||||
type="text"
|
||||
class="form-control monospace"
|
||||
@input="updateBranchName($event.target.value)"
|
||||
class="gl-font-monospace"
|
||||
@input="updateBranchName($event)"
|
||||
/>
|
||||
</div>
|
||||
</gl-form-group>
|
||||
</fieldset>
|
||||
</template>
|
||||
|
|
|
@ -10,6 +10,7 @@ import RegistrationDropdown from '../components/registration/registration_dropdo
|
|||
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
|
||||
import RunnerBulkDelete from '../components/runner_bulk_delete.vue';
|
||||
import RunnerList from '../components/runner_list.vue';
|
||||
import RunnerListEmptyState from '../components/runner_list_empty_state.vue';
|
||||
import RunnerName from '../components/runner_name.vue';
|
||||
import RunnerStats from '../components/stat/runner_stats.vue';
|
||||
import RunnerPagination from '../components/runner_pagination.vue';
|
||||
|
@ -35,6 +36,7 @@ import {
|
|||
fromUrlQueryToSearch,
|
||||
fromSearchToUrl,
|
||||
fromSearchToVariables,
|
||||
isSearchFiltered,
|
||||
} from '../runner_search_utils';
|
||||
import { captureException } from '../sentry_utils';
|
||||
|
||||
|
@ -91,6 +93,7 @@ export default {
|
|||
RunnerFilteredSearchBar,
|
||||
RunnerBulkDelete,
|
||||
RunnerList,
|
||||
RunnerListEmptyState,
|
||||
RunnerName,
|
||||
RunnerStats,
|
||||
RunnerPagination,
|
||||
|
@ -98,7 +101,7 @@ export default {
|
|||
RunnerActionsCell,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
inject: ['localMutations'],
|
||||
inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath', 'localMutations'],
|
||||
props: {
|
||||
registrationToken: {
|
||||
type: String,
|
||||
|
@ -190,6 +193,9 @@ export default {
|
|||
// Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/353981
|
||||
return this.glFeatures.adminRunnersBulkDelete;
|
||||
},
|
||||
isSearchFiltered() {
|
||||
return isSearchFiltered(this.search);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
search: {
|
||||
|
@ -298,9 +304,13 @@ export default {
|
|||
:stale-runners-count="staleRunnersTotal"
|
||||
/>
|
||||
|
||||
<div v-if="noRunnersFound" class="gl-text-center gl-p-5">
|
||||
{{ __('No runners found') }}
|
||||
</div>
|
||||
<runner-list-empty-state
|
||||
v-if="noRunnersFound"
|
||||
:registration-token="registrationToken"
|
||||
:is-search-filtered="isSearchFiltered"
|
||||
:svg-path="emptyStateSvgPath"
|
||||
:filtered-svg-path="emptyStateFilteredSvgPath"
|
||||
/>
|
||||
<template v-else>
|
||||
<runner-bulk-delete v-if="isBulkDeleteEnabled" />
|
||||
<runner-list
|
||||
|
|
|
@ -34,6 +34,8 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
|
|||
registrationToken,
|
||||
onlineContactTimeoutSecs,
|
||||
staleTimeoutSecs,
|
||||
emptyStateSvgPath,
|
||||
emptyStateFilteredSvgPath,
|
||||
} = el.dataset;
|
||||
|
||||
const { cacheConfig, typeDefs, localMutations } = createLocalState();
|
||||
|
@ -50,6 +52,8 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
|
|||
localMutations,
|
||||
onlineContactTimeoutSecs,
|
||||
staleTimeoutSecs,
|
||||
emptyStateSvgPath,
|
||||
emptyStateFilteredSvgPath,
|
||||
},
|
||||
render(h) {
|
||||
return h(AdminRunnersApp, {
|
||||
|
|
|
@ -7,6 +7,8 @@ import RunnerPausedBadge from '../runner_paused_badge.vue';
|
|||
export default {
|
||||
components: {
|
||||
RunnerStatusBadge,
|
||||
RunnerUpgradeStatusBadge: () =>
|
||||
import('ee_component/runner/components/runner_upgrade_status_badge.vue'),
|
||||
RunnerPausedBadge,
|
||||
},
|
||||
directives: {
|
||||
|
@ -33,6 +35,11 @@ export default {
|
|||
size="sm"
|
||||
class="gl-display-inline-block gl-max-w-full gl-text-truncate"
|
||||
/>
|
||||
<runner-upgrade-status-badge
|
||||
:runner="runner"
|
||||
size="sm"
|
||||
class="gl-display-inline-block gl-max-w-full gl-text-truncate"
|
||||
/>
|
||||
<runner-paused-badge
|
||||
v-if="paused"
|
||||
size="sm"
|
||||
|
|
|
@ -12,7 +12,7 @@ import RunnerStatusCell from './cells/runner_status_cell.vue';
|
|||
import RunnerTags from './runner_tags.vue';
|
||||
|
||||
const defaultFields = [
|
||||
tableField({ key: 'status', label: s__('Runners|Status') }),
|
||||
tableField({ key: 'status', label: s__('Runners|Status'), thClasses: ['gl-w-15p'] }),
|
||||
tableField({ key: 'summary', label: s__('Runners|Runner'), thClasses: ['gl-lg-w-25p'] }),
|
||||
tableField({ key: 'version', label: __('Version') }),
|
||||
tableField({ key: 'jobCount', label: __('Jobs') }),
|
||||
|
|
|
@ -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"
|
||||
|
||||
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"
|
||||
|
||||
query getGroupRunners(
|
||||
|
|
|
@ -1,20 +1,5 @@
|
|||
#import "./list_item_shared.fragment.graphql"
|
||||
|
||||
fragment ListItem on CiRunner {
|
||||
__typename
|
||||
id
|
||||
description
|
||||
runnerType
|
||||
shortSha
|
||||
version
|
||||
revision
|
||||
ipAddress
|
||||
active
|
||||
locked
|
||||
jobCount
|
||||
tagList
|
||||
contactedAt
|
||||
status(legacyMode: null)
|
||||
userPermissions {
|
||||
updateRunner
|
||||
deleteRunner
|
||||
}
|
||||
...ListItemShared
|
||||
}
|
||||
|
|
|
@ -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 RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
|
||||
import RunnerList from '../components/runner_list.vue';
|
||||
import RunnerListEmptyState from '../components/runner_list_empty_state.vue';
|
||||
import RunnerName from '../components/runner_name.vue';
|
||||
import RunnerStats from '../components/stat/runner_stats.vue';
|
||||
import RunnerPagination from '../components/runner_pagination.vue';
|
||||
|
@ -31,6 +32,7 @@ import {
|
|||
fromUrlQueryToSearch,
|
||||
fromSearchToUrl,
|
||||
fromSearchToVariables,
|
||||
isSearchFiltered,
|
||||
} from '../runner_search_utils';
|
||||
import { captureException } from '../sentry_utils';
|
||||
|
||||
|
@ -86,12 +88,14 @@ export default {
|
|||
RegistrationDropdown,
|
||||
RunnerFilteredSearchBar,
|
||||
RunnerList,
|
||||
RunnerListEmptyState,
|
||||
RunnerName,
|
||||
RunnerStats,
|
||||
RunnerPagination,
|
||||
RunnerTypeTabs,
|
||||
RunnerActionsCell,
|
||||
},
|
||||
inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'],
|
||||
props: {
|
||||
registrationToken: {
|
||||
type: String,
|
||||
|
@ -196,6 +200,9 @@ export default {
|
|||
filteredSearchNamespace() {
|
||||
return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`;
|
||||
},
|
||||
isSearchFiltered() {
|
||||
return isSearchFiltered(this.search);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
search: {
|
||||
|
@ -299,9 +306,13 @@ export default {
|
|||
:stale-runners-count="staleRunnersTotal"
|
||||
/>
|
||||
|
||||
<div v-if="noRunnersFound" class="gl-text-center gl-p-5">
|
||||
{{ __('No runners found') }}
|
||||
</div>
|
||||
<runner-list-empty-state
|
||||
v-if="noRunnersFound"
|
||||
:registration-token="registrationToken"
|
||||
:is-search-filtered="isSearchFiltered"
|
||||
:svg-path="emptyStateSvgPath"
|
||||
:filtered-svg-path="emptyStateFilteredSvgPath"
|
||||
/>
|
||||
<template v-else>
|
||||
<runner-list :runners="runners.items" :loading="runnersLoading">
|
||||
<template #runner-name="{ runner }">
|
||||
|
|
|
@ -22,6 +22,8 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
|
|||
groupRunnersLimitedCount,
|
||||
onlineContactTimeoutSecs,
|
||||
staleTimeoutSecs,
|
||||
emptyStateSvgPath,
|
||||
emptyStateFilteredSvgPath,
|
||||
} = el.dataset;
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
|
@ -36,6 +38,8 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
|
|||
groupId,
|
||||
onlineContactTimeoutSecs: parseInt(onlineContactTimeoutSecs, 10),
|
||||
staleTimeoutSecs: parseInt(staleTimeoutSecs, 10),
|
||||
emptyStateSvgPath,
|
||||
emptyStateFilteredSvgPath,
|
||||
},
|
||||
render(h) {
|
||||
return h(GroupRunnersApp, {
|
||||
|
|
|
@ -236,3 +236,17 @@ export const fromSearchToVariables = ({
|
|||
...paginationVariables,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Decides whether or not a search object is the "default" or empty.
|
||||
*
|
||||
* A search is filtered if the user has entered filtering criteria.
|
||||
*
|
||||
* @param {Object} search
|
||||
* @returns true if this search is filtered, false otherwise
|
||||
*/
|
||||
export const isSearchFiltered = ({ runnerType = null, filters = [], pagination = {} } = {}) => {
|
||||
return Boolean(
|
||||
runnerType !== null || filters?.length !== 0 || (pagination && pagination?.page !== 1),
|
||||
);
|
||||
};
|
||||
|
|
|
@ -302,9 +302,11 @@ export default {
|
|||
<span v-else class="gl-display-flex gl-align-items-center gl-line-height-normal">
|
||||
{{ __('None') }} -
|
||||
<gl-button
|
||||
class="gl-ml-2"
|
||||
class="gl-ml-2 gl-reset-color!"
|
||||
href="#"
|
||||
category="tertiary"
|
||||
variant="link"
|
||||
size="small"
|
||||
data-testid="unassigned-users"
|
||||
@click="updateAlertAssignees(currentUser)"
|
||||
>
|
||||
|
|
|
@ -247,8 +247,8 @@
|
|||
z-index: 600;
|
||||
width: $contextual-sidebar-width;
|
||||
top: $header-height;
|
||||
@include gl-bg-gray-10;
|
||||
border-right: 1px solid $gray-50;
|
||||
background-color: $contextual-sidebar-bg-color;
|
||||
border-right: 1px solid $contextual-sidebar-border-color;
|
||||
transform: translate3d(0, 0, 0);
|
||||
|
||||
&.sidebar-collapsed-desktop {
|
||||
|
@ -411,7 +411,7 @@
|
|||
.toggle-sidebar-button,
|
||||
.close-nav-button {
|
||||
@include side-panel-toggle;
|
||||
@include gl-bg-gray-10;
|
||||
background-color: $contextual-sidebar-bg-color;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: #{$contextual-sidebar-width - 1px};
|
||||
|
|
|
@ -357,6 +357,8 @@ $border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor);
|
|||
/*
|
||||
* UI elements
|
||||
*/
|
||||
$contextual-sidebar-bg-color: #f5f5f5;
|
||||
$contextual-sidebar-border-color: #e9e9e9;
|
||||
$border-color: $gray-100;
|
||||
$shadow-color: $t-gray-a-08;
|
||||
$well-expand-item: #e8f2f7 !default;
|
||||
|
|
|
@ -563,24 +563,11 @@ $ide-commit-header-height: 48px;
|
|||
}
|
||||
|
||||
.ide-commit-options {
|
||||
label {
|
||||
font-weight: normal;
|
||||
|
||||
&.is-disabled {
|
||||
.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 {
|
||||
|
|
|
@ -1034,8 +1034,8 @@ input {
|
|||
z-index: 600;
|
||||
width: 256px;
|
||||
top: var(--header-height, 48px);
|
||||
background-color: #1f1f1f;
|
||||
border-right: 1px solid #303030;
|
||||
background-color: #f5f5f5;
|
||||
border-right: 1px solid #e9e9e9;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
.nav-sidebar.sidebar-collapsed-desktop {
|
||||
|
@ -1402,7 +1402,7 @@ input {
|
|||
color: #999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #1f1f1f;
|
||||
background-color: #f5f5f5;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 255px;
|
||||
|
@ -1792,6 +1792,7 @@ body.gl-dark {
|
|||
.toggle-sidebar-button,
|
||||
.close-nav-button {
|
||||
background-color: #262626;
|
||||
border-right: 1px solid #303030;
|
||||
}
|
||||
.nav-sidebar li a {
|
||||
color: var(--gray-600);
|
||||
|
|
|
@ -1019,8 +1019,8 @@ input {
|
|||
z-index: 600;
|
||||
width: 256px;
|
||||
top: var(--header-height, 48px);
|
||||
background-color: #fafafa;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
background-color: #f5f5f5;
|
||||
border-right: 1px solid #e9e9e9;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
.nav-sidebar.sidebar-collapsed-desktop {
|
||||
|
@ -1387,7 +1387,7 @@ input {
|
|||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #fafafa;
|
||||
background-color: #f5f5f5;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 255px;
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
.toggle-sidebar-button,
|
||||
.close-nav-button {
|
||||
background-color: darken($gray-50, 4%);
|
||||
border-right: 1px solid $gray-50;
|
||||
}
|
||||
|
||||
.nav-sidebar {
|
||||
|
|
|
@ -41,3 +41,5 @@ class Groups::RunnersController < Groups::ApplicationController
|
|||
params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
|
||||
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.
|
||||
# params:
|
||||
# group: Group, required
|
||||
# search: String, optional
|
||||
# state: CustomerRelations::ContactStateEnum, optional
|
||||
module Crm
|
||||
class ContactsFinder
|
||||
include Gitlab::Allowable
|
||||
|
@ -21,7 +23,10 @@ module Crm
|
|||
def execute
|
||||
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
|
||||
|
||||
private
|
||||
|
@ -35,5 +40,25 @@ module Crm
|
|||
group
|
||||
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
|
||||
|
|
|
@ -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
|
||||
{
|
||||
contacts: [:contacts],
|
||||
container_repositories_count: [:container_repositories],
|
||||
custom_emoji: [:custom_emoji],
|
||||
full_path: [:route],
|
||||
organizations: [:organizations],
|
||||
path: [:route],
|
||||
dependency_proxy_blob_count: [: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,
|
||||
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,
|
||||
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,
|
||||
resolver: Resolvers::WorkItems::TypesResolver,
|
||||
|
|
|
@ -65,7 +65,9 @@ module Ci
|
|||
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
|
||||
registration_token: Gitlab::CurrentSettings.runners_registration_token,
|
||||
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
|
||||
|
||||
|
@ -87,7 +89,9 @@ module Ci
|
|||
group_full_path: group.full_path,
|
||||
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
|
||||
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
|
||||
|
||||
|
|
|
@ -808,6 +808,7 @@ module Ci
|
|||
|
||||
def execute_hooks
|
||||
return unless project
|
||||
return if user&.blocked?
|
||||
|
||||
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)
|
||||
|
|
|
@ -124,10 +124,10 @@ module Ci
|
|||
# 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'
|
||||
|
||||
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_commit :store_file_after_commit!, on: [:create, :update], if: :store_after_commit?
|
||||
after_save :store_file_in_transaction!, unless: :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
|
||||
validate :validate_file_format!, unless: :trace?, on: :create
|
||||
|
@ -362,11 +362,24 @@ module Ci
|
|||
|
||||
private
|
||||
|
||||
def store_file_after_commit!
|
||||
return unless previous_changes.key?(:file)
|
||||
def store_file_in_transaction!
|
||||
store_file_now! if saved_change_to_file?
|
||||
|
||||
store_file!
|
||||
update_file_store
|
||||
file_stored_in_transaction_hooks
|
||||
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
|
||||
|
||||
def set_size
|
||||
|
|
|
@ -240,7 +240,9 @@ module Ci
|
|||
next if transition.loopback?
|
||||
|
||||
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?
|
||||
# Passing the seq-id ensures this is idempotent
|
||||
|
@ -297,7 +299,12 @@ module Ci
|
|||
ref_status = pipeline.ci_ref&.update_status_by!(pipeline)
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -4,9 +4,16 @@ module FileStoreMounter
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
after_save :update_file_store, if: :saved_change_to_file?
|
||||
end
|
||||
|
@ -16,4 +23,9 @@ module FileStoreMounter
|
|||
# The file.object_store is set during `uploader.store!` and `uploader.migrate!`
|
||||
update_column(:file_store, file.object_store)
|
||||
end
|
||||
|
||||
def store_file_now!
|
||||
store_file!
|
||||
update_file_store
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CustomerRelations::Contact < ApplicationRecord
|
||||
include Gitlab::SQL::Pattern
|
||||
include Sortable
|
||||
include StripAttribute
|
||||
|
||||
self.table_name = "customer_relations_contacts"
|
||||
|
@ -39,6 +41,25 @@ class CustomerRelations::Contact < ApplicationRecord
|
|||
']'
|
||||
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)
|
||||
raise ArgumentError, "Cannot lookup more than #{MAX_PLUCK} emails" if emails.length > MAX_PLUCK
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CustomerRelations::Organization < ApplicationRecord
|
||||
include Gitlab::SQL::Pattern
|
||||
include Sortable
|
||||
include StripAttribute
|
||||
|
||||
self.table_name = "customer_relations_organizations"
|
||||
|
@ -21,6 +23,25 @@ class CustomerRelations::Organization < ApplicationRecord
|
|||
validates :description, length: { maximum: 1024 }
|
||||
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)
|
||||
where(group: group_id)
|
||||
.where('LOWER(name) = LOWER(?)', name)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
module Projects
|
||||
class UpdatePagesService < BaseService
|
||||
InvalidStateError = Class.new(StandardError)
|
||||
WrongUploadedDeploymentSizeError = Class.new(StandardError)
|
||||
BLOCK_SIZE = 32.kilobytes
|
||||
PUBLIC_DIR = 'public'
|
||||
|
||||
|
@ -39,6 +40,9 @@ module Projects
|
|||
end
|
||||
rescue InvalidStateError => e
|
||||
error(e.message)
|
||||
rescue WrongUploadedDeploymentSizeError => e
|
||||
error("Uploading artifacts to pages storage failed")
|
||||
raise e
|
||||
rescue StandardError => e
|
||||
error(e.message)
|
||||
raise e
|
||||
|
@ -80,6 +84,10 @@ module Projects
|
|||
ci_build_id: build.id
|
||||
)
|
||||
|
||||
if deployment.size != file.size || deployment.file.size != file.size
|
||||
raise(WrongUploadedDeploymentSizeError)
|
||||
end
|
||||
|
||||
validate_outdated_sha!
|
||||
|
||||
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' } }
|
||||
.settings-header
|
||||
%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')
|
||||
- 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 }
|
||||
|
|
|
@ -8,5 +8,4 @@
|
|||
- title = s_('TagsPage|Only a project maintainer or owner can delete a protected tag')
|
||||
- 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 } }
|
||||
= sprite_icon('remove', css_class: 'gl-icon')
|
||||
= 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 } })
|
||||
|
|
|
@ -13,6 +13,7 @@ class PipelineHooksWorker # rubocop:disable Scalability/IdempotentWorker
|
|||
def perform(pipeline_id)
|
||||
pipeline = Ci::Pipeline.find_by_id(pipeline_id)
|
||||
return unless pipeline
|
||||
return if pipeline.user&.blocked?
|
||||
|
||||
Ci::Pipelines::HookService.new(pipeline).execute
|
||||
end
|
||||
|
|
|
@ -23,6 +23,7 @@ class PipelineNotificationWorker # rubocop:disable Scalability/IdempotentWorker
|
|||
|
||||
pipeline = Ci::Pipeline.find_by_id(pipeline_id)
|
||||
return unless pipeline
|
||||
return if pipeline.user&.blocked?
|
||||
|
||||
NotificationService.new.pipeline_finished(pipeline, ref_status: ref_status, recipients: recipients)
|
||||
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="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="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="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. |
|
||||
|
@ -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="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="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="groupparent"></a>`parent` | [`Group`](#group) | Parent group. |
|
||||
| <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. |
|
||||
|
||||
##### `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`
|
||||
|
||||
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="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`
|
||||
|
||||
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_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`
|
||||
|
||||
Unit for the duration of Dast Profile Cadence.
|
||||
|
|
|
@ -230,7 +230,7 @@ include:
|
|||
file: '/templates/.gitlab-ci-template.yml'
|
||||
|
||||
- project: 'my-group/my-project'
|
||||
ref: v1.0.0
|
||||
ref: v1.0.0 # Git Tag
|
||||
file: '/templates/.gitlab-ci-template.yml'
|
||||
|
||||
- project: 'my-group/my-project'
|
||||
|
|
|
@ -450,5 +450,26 @@ test-vars-2:
|
|||
- printenv
|
||||
```
|
||||
|
||||
You can't reuse a section that already includes a `!reference` tag. Only one level
|
||||
of nesting is supported.
|
||||
### Nest `!reference` tags in `script`, `before_script`, and `after_script`
|
||||
|
||||
> [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
|
||||
- `app/assets/images/emoji.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 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
|
||||
|
|
|
@ -52,7 +52,10 @@ this inconsistency.
|
|||
Some places in the code refer to both the GitLab and GitHub specifications
|
||||
simultaneous in the same areas of logic. In these situations,
|
||||
_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).
|
||||
|
||||
|
@ -434,7 +437,7 @@ subgraph script:
|
|||
A --> B{Backend Markdown API}
|
||||
end
|
||||
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
|
||||
E[glfm_canonical_examples.txt] --> A
|
||||
end
|
||||
|
@ -572,12 +575,16 @@ updated, as in the case of all GFM files.
|
|||
|
||||
##### 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).
|
||||
|
||||
- It is automatically downloaded and updated by `update-specification.rb` script.
|
||||
- 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_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
|
||||
06_04_inlines_emphasis_and_strong_emphasis_1:
|
||||
canonical: |
|
||||
<p><em>foo bar</em></p>
|
||||
static: |
|
||||
<p data-sourcepos="1:1-1:9" dir="auto"><strong>foo bar</strong></p>
|
||||
wysiwyg: |
|
||||
<p><strong>foo bar</strong></p>
|
||||
canonical: |
|
||||
<p><em>foo bar</em></p>
|
||||
static: |
|
||||
<p data-sourcepos="1:1-1:9" dir="auto"><strong>foo bar</strong></p>
|
||||
wysiwyg: |
|
||||
<p><strong>foo bar</strong></p>
|
||||
```
|
||||
|
||||
NOTE:
|
||||
|
|
|
@ -205,7 +205,7 @@ To assign an alert:
|
|||
![Alert Details View Assignees](img/alert_details_assignees_v13_1.png)
|
||||
|
||||
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**.
|
||||
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**.
|
||||
1. Select the branches to send notifications for.
|
||||
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)
|
||||
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:
|
||||
|
||||
```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.
|
||||
|
||||
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:
|
||||
|
||||
```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 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:
|
||||
|
||||
- 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,
|
||||
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:
|
||||
|
||||
![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.
|
||||
and the ability to select **Rebase** from the user interface.
|
||||
|
||||
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.
|
||||
|
@ -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
|
||||
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
|
||||
|
||||
- [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
|
||||
class RateLimit < Chain::Base
|
||||
include Chain::Helpers
|
||||
include ::Gitlab::Utils::StrongMemoize
|
||||
|
||||
def perform!
|
||||
# We exclude child-pipelines from the rate limit because they represent
|
||||
|
@ -41,7 +42,9 @@ module Gitlab
|
|||
commit_sha: command.sha,
|
||||
current_user_id: current_user.id,
|
||||
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
|
||||
|
||||
|
@ -50,9 +53,16 @@ module Gitlab
|
|||
end
|
||||
|
||||
def enforce_throttle?
|
||||
::Feature.enabled?(
|
||||
:ci_enforce_throttle_pipelines_creation,
|
||||
project)
|
||||
strong_memoize(:enforce_throttle) do
|
||||
::Feature.enabled?(:ci_enforce_throttle_pipelines_creation, 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
|
||||
|
|
|
@ -17232,7 +17232,7 @@ msgstr ""
|
|||
msgid "GitLab is open source software to collaborate on code."
|
||||
msgstr ""
|
||||
|
||||
msgid "GitLab is undergoing maintenance and is operating in read-only mode."
|
||||
msgid "GitLab is undergoing maintenance"
|
||||
msgstr ""
|
||||
|
||||
msgid "GitLab logo"
|
||||
|
@ -19088,6 +19088,9 @@ msgstr ""
|
|||
msgid "IDE|Review"
|
||||
msgstr ""
|
||||
|
||||
msgid "IDE|Start a new merge request"
|
||||
msgstr ""
|
||||
|
||||
msgid "IDE|Successful commit"
|
||||
msgstr ""
|
||||
|
||||
|
@ -25758,9 +25761,6 @@ msgstr ""
|
|||
msgid "No runner executable"
|
||||
msgstr ""
|
||||
|
||||
msgid "No runners found"
|
||||
msgstr ""
|
||||
|
||||
msgid "No schedules"
|
||||
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."
|
||||
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}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -32866,6 +32869,9 @@ msgstr ""
|
|||
msgid "Runners|Download latest binary"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Edit your search and try again"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Enable stale runner cleanup"
|
||||
msgstr ""
|
||||
|
||||
|
@ -32878,6 +32884,9 @@ msgstr ""
|
|||
msgid "Runners|Executor"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Get started with runners"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Group"
|
||||
msgstr ""
|
||||
|
||||
|
@ -32926,6 +32935,9 @@ msgstr ""
|
|||
msgid "Runners|New registration token generated!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|No results found"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|No spot. Default choice for Windows Shell executor."
|
||||
msgstr ""
|
||||
|
||||
|
@ -33069,6 +33081,9 @@ msgstr ""
|
|||
msgid "Runners|Runners"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
|
@ -33134,6 +33149,9 @@ msgstr ""
|
|||
msgid "Runners|This runner is available to all projects and subgroups in a group."
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
|
@ -33203,6 +33221,12 @@ msgstr ""
|
|||
msgid "Runners|stale"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|upgrade available"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|upgrade recommended"
|
||||
msgstr ""
|
||||
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
|
||||
|
@ -36268,9 +36292,6 @@ msgstr ""
|
|||
msgid "Start a new discussion…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Start a new merge request"
|
||||
msgstr ""
|
||||
|
||||
msgid "Start a new merge request with these changes"
|
||||
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."
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
"@babel/preset-env": "^7.10.1",
|
||||
"@gitlab/at.js": "1.5.7",
|
||||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/svgs": "2.17.0",
|
||||
"@gitlab/svgs": "2.18.0",
|
||||
"@gitlab/ui": "40.7.1",
|
||||
"@gitlab/visual-review-tools": "1.7.3",
|
||||
"@rails/actioncable": "6.1.4-7",
|
||||
|
|
|
@ -115,13 +115,17 @@ RSpec.describe "Admin Runners" do
|
|||
expect(page).not_to have_content("runner-bar")
|
||||
end
|
||||
|
||||
it 'shows no runner when description does not match' do
|
||||
input_filtered_search_keys('runner-baz')
|
||||
context 'when description does not match' do
|
||||
before do
|
||||
input_filtered_search_keys('runner-baz')
|
||||
end
|
||||
|
||||
expect(page).to have_link('All 0')
|
||||
expect(page).to have_link('Instance 0')
|
||||
it_behaves_like 'shows no runners found'
|
||||
|
||||
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
|
||||
|
||||
|
@ -190,14 +194,6 @@ RSpec.describe "Admin Runners" do
|
|||
expect(page).not_to have_content 'runner-never-contacted'
|
||||
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
|
||||
input_filtered_search_filter_is_only('Status', 'Online')
|
||||
input_filtered_search_keys('runner-1')
|
||||
|
@ -225,6 +221,18 @@ RSpec.describe "Admin Runners" do
|
|||
expect(page).to have_selector '.badge', text: 'never contacted'
|
||||
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
|
||||
|
||||
describe 'filter by type' do
|
||||
|
@ -273,21 +281,6 @@ RSpec.describe "Admin Runners" do
|
|||
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
|
||||
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-paused-project'
|
||||
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
|
||||
|
||||
describe 'filter by tag' do
|
||||
|
@ -358,15 +369,6 @@ RSpec.describe "Admin Runners" do
|
|||
expect(page).not_to have_content 'runner-red'
|
||||
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
|
||||
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-red'
|
||||
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
|
||||
|
||||
it 'sorts by last contact date' do
|
||||
|
@ -419,7 +434,7 @@ RSpec.describe "Admin Runners" do
|
|||
visit admin_runners_path
|
||||
end
|
||||
|
||||
it_behaves_like "shows no runners"
|
||||
it_behaves_like 'shows no runners registered'
|
||||
|
||||
it 'shows tabs with total counts equal to 0' do
|
||||
expect(page).to have_link('All 0')
|
||||
|
|
|
@ -33,7 +33,7 @@ RSpec.describe "Group Runners" do
|
|||
visit group_runners_path(group)
|
||||
end
|
||||
|
||||
it_behaves_like "shows no runners"
|
||||
it_behaves_like 'shows no runners registered'
|
||||
|
||||
it 'shows tabs with total counts equal to 0' do
|
||||
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))
|
||||
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
|
||||
|
||||
context "with an online project runner" do
|
||||
|
|
|
@ -66,5 +66,83 @@ RSpec.describe Crm::ContactsFinder 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(
|
||||
: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
|
||||
|
|
|
@ -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',
|
||||
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 = {
|
||||
|
|
|
@ -57,6 +57,18 @@ describe('AwardsHandler', () => {
|
|||
d: 'white question mark ornament',
|
||||
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') => {
|
||||
|
@ -296,6 +308,23 @@ describe('AwardsHandler', () => {
|
|||
awardsHandler.searchEmojis('👼');
|
||||
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', () => {
|
||||
|
|
|
@ -24,6 +24,7 @@ import isEmojiUnicodeSupported, {
|
|||
isHorceRacingSkinToneComboEmoji,
|
||||
isPersonZwjEmoji,
|
||||
} from '~/emoji/support/is_emoji_unicode_supported';
|
||||
import { NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants';
|
||||
|
||||
const emptySupportMap = {
|
||||
personZwj: false,
|
||||
|
@ -436,14 +437,28 @@ describe('emoji', () => {
|
|||
it.each([undefined, null, ''])("should return all emoji when the input is '%s'", (input) => {
|
||||
const search = searchEmoji(input);
|
||||
|
||||
const expected = Object.keys(validEmoji).map((name) => {
|
||||
return {
|
||||
emoji: mockEmojiData[name],
|
||||
field: 'd',
|
||||
fieldValue: mockEmojiData[name].d,
|
||||
score: 0,
|
||||
};
|
||||
});
|
||||
const expected = Object.keys(validEmoji)
|
||||
.map((name) => {
|
||||
let score = NEUTRAL_INTENT_MULTIPLIER;
|
||||
|
||||
// Positive intent value retrieved from ~/emoji/intents.json
|
||||
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);
|
||||
});
|
||||
|
@ -457,7 +472,7 @@ describe('emoji', () => {
|
|||
name: 'atom',
|
||||
field: 'e',
|
||||
fieldValue: 'atom',
|
||||
score: 0,
|
||||
score: NEUTRAL_INTENT_MULTIPLIER,
|
||||
},
|
||||
],
|
||||
],
|
||||
|
@ -469,7 +484,7 @@ describe('emoji', () => {
|
|||
name: 'atom',
|
||||
field: 'alias',
|
||||
fieldValue: 'atom_symbol',
|
||||
score: 4,
|
||||
score: 16,
|
||||
},
|
||||
],
|
||||
],
|
||||
|
@ -481,7 +496,7 @@ describe('emoji', () => {
|
|||
name: 'atom',
|
||||
field: 'alias',
|
||||
fieldValue: 'atom_symbol',
|
||||
score: 0,
|
||||
score: NEUTRAL_INTENT_MULTIPLIER,
|
||||
},
|
||||
],
|
||||
],
|
||||
|
@ -509,7 +524,7 @@ describe('emoji', () => {
|
|||
{
|
||||
name: 'atom',
|
||||
field: 'd',
|
||||
score: 0,
|
||||
score: NEUTRAL_INTENT_MULTIPLIER,
|
||||
},
|
||||
],
|
||||
],
|
||||
|
@ -521,7 +536,7 @@ describe('emoji', () => {
|
|||
{
|
||||
name: 'atom',
|
||||
field: 'd',
|
||||
score: 0,
|
||||
score: NEUTRAL_INTENT_MULTIPLIER,
|
||||
},
|
||||
],
|
||||
],
|
||||
|
@ -533,7 +548,7 @@ describe('emoji', () => {
|
|||
{
|
||||
name: 'grey_question',
|
||||
field: 'name',
|
||||
score: 5,
|
||||
score: 32,
|
||||
},
|
||||
],
|
||||
],
|
||||
|
@ -544,7 +559,7 @@ describe('emoji', () => {
|
|||
{
|
||||
name: 'grey_question',
|
||||
field: 'd',
|
||||
score: 24,
|
||||
score: 16777216,
|
||||
},
|
||||
],
|
||||
],
|
||||
|
@ -552,15 +567,15 @@ describe('emoji', () => {
|
|||
'searching with query "heart"',
|
||||
'heart',
|
||||
[
|
||||
{
|
||||
name: 'black_heart',
|
||||
field: 'd',
|
||||
score: 6,
|
||||
},
|
||||
{
|
||||
name: 'heart',
|
||||
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"',
|
||||
'HEART',
|
||||
[
|
||||
{
|
||||
name: 'black_heart',
|
||||
field: 'd',
|
||||
score: 6,
|
||||
},
|
||||
{
|
||||
name: 'heart',
|
||||
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"',
|
||||
'star',
|
||||
[
|
||||
{
|
||||
name: 'custard',
|
||||
field: 'd',
|
||||
score: 2,
|
||||
},
|
||||
{
|
||||
name: 'star',
|
||||
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: 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: 10, fieldValue: '', emoji: { name: 'a' } },
|
||||
],
|
||||
|
@ -630,25 +661,25 @@ describe('emoji', () => {
|
|||
[
|
||||
'should correctly sort by fieldValue',
|
||||
[
|
||||
{ score: 0, fieldValue: 'y', emoji: { name: 'b' } },
|
||||
{ score: 0, fieldValue: 'x', emoji: { name: 'a' } },
|
||||
{ score: 0, fieldValue: 'z', emoji: { name: 'c' } },
|
||||
{ score: 1, fieldValue: 'y', emoji: { name: 'b' } },
|
||||
{ score: 1, fieldValue: 'x', emoji: { name: 'a' } },
|
||||
{ score: 1, fieldValue: 'z', emoji: { name: 'c' } },
|
||||
],
|
||||
[
|
||||
{ score: 0, fieldValue: 'x', emoji: { name: 'a' } },
|
||||
{ score: 0, fieldValue: 'y', emoji: { name: 'b' } },
|
||||
{ score: 0, fieldValue: 'z', emoji: { name: 'c' } },
|
||||
{ score: 1, fieldValue: 'x', emoji: { name: 'a' } },
|
||||
{ score: 1, fieldValue: 'y', emoji: { name: 'b' } },
|
||||
{ score: 1, fieldValue: 'z', emoji: { name: 'c' } },
|
||||
],
|
||||
],
|
||||
[
|
||||
'should correctly sort by score and then by fieldValue (in order)',
|
||||
[
|
||||
{ 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: 0, fieldValue: 'z', emoji: { name: 'a' } },
|
||||
{ score: 1, fieldValue: 'z', emoji: { name: 'a' } },
|
||||
{ score: 5, fieldValue: 'x', emoji: { name: 'b' } },
|
||||
{ score: 5, fieldValue: 'y', emoji: { name: 'c' } },
|
||||
],
|
||||
|
@ -656,7 +687,7 @@ describe('emoji', () => {
|
|||
];
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Gitlab::Ci::RunnerUpgradeCheck.instance)
|
||||
.to receive(:check_runner_upgrade_status)
|
||||
.and_return(:not_available)
|
||||
end
|
||||
|
||||
describe do
|
||||
before do
|
||||
sign_in(admin)
|
||||
|
|
|
@ -1,193 +1,97 @@
|
|||
import Vue, { nextTick } from 'vue';
|
||||
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
|
||||
import { projectData, branches } from 'jest/ide/mock_data';
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
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 { PERMISSION_CREATE_MR } from '~/ide/constants';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import {
|
||||
COMMIT_TO_CURRENT_BRANCH,
|
||||
COMMIT_TO_NEW_BRANCH,
|
||||
} from '~/ide/stores/modules/commit/constants';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
|
||||
describe('create new MR checkbox', () => {
|
||||
Vue.use(Vuex);
|
||||
|
||||
describe('NewMergeRequestOption component', () => {
|
||||
let store;
|
||||
let vm;
|
||||
let wrapper;
|
||||
|
||||
const setMR = () => {
|
||||
vm.$store.state.currentMergeRequestId = '1';
|
||||
vm.$store.state.projects[store.state.currentProjectId].mergeRequests[
|
||||
store.state.currentMergeRequestId
|
||||
] = { foo: 'bar' };
|
||||
};
|
||||
const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
|
||||
const findFieldset = () => wrapper.findByTestId('new-merge-request-fieldset');
|
||||
const findTooltip = () => getBinding(findFieldset().element, 'gl-tooltip');
|
||||
|
||||
const setPermissions = (permissions) => {
|
||||
store.state.projects[store.state.currentProjectId].userPermissions = permissions;
|
||||
};
|
||||
|
||||
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(() => {
|
||||
const createComponent = ({
|
||||
shouldHideNewMrOption = false,
|
||||
shouldDisableNewMrOption = false,
|
||||
shouldCreateMR = false,
|
||||
} = {}) => {
|
||||
store = createStore();
|
||||
|
||||
store.state.currentProjectId = 'abcproject';
|
||||
|
||||
const proj = JSON.parse(JSON.stringify(projectData));
|
||||
proj.userPermissions[PERMISSION_CREATE_MR] = true;
|
||||
Vue.set(store.state.projects, 'abcproject', proj);
|
||||
});
|
||||
wrapper = shallowMountExtended(NewMergeRequestOption, {
|
||||
store: {
|
||||
...store,
|
||||
getters: {
|
||||
'commit/shouldHideNewMrOption': shouldHideNewMrOption,
|
||||
'commit/shouldDisableNewMrOption': shouldDisableNewMrOption,
|
||||
'commit/shouldCreateMR': shouldCreateMR,
|
||||
},
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: createMockDirective(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('for default branch', () => {
|
||||
describe('is rendered when pushing to a new branch', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
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('when the `shouldHideNewMrOption` getter returns false', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
jest.spyOn(store, 'dispatch').mockImplementation();
|
||||
});
|
||||
|
||||
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(() => {
|
||||
createComponent({
|
||||
currentBranchId: 'main',
|
||||
createNewBranch: false,
|
||||
shouldDisableNewMrOption: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('has NO new MR', () => {
|
||||
expect(vm.$el.textContent).toBe('');
|
||||
it('disables the new MR checkbox', () => {
|
||||
expect(findCheckbox().attributes('disabled')).toBe('true');
|
||||
});
|
||||
|
||||
it('has new MR', async () => {
|
||||
setMR();
|
||||
it('adds `is-disabled` class to the fieldset', () => {
|
||||
expect(findFieldset().classes()).toContain('is-disabled');
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
expect(vm.$el.textContent).toBe('');
|
||||
it('shows a tooltip', () => {
|
||||
expect(findTooltip().value).toBe(wrapper.vm.$options.i18n.tooltipText);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('for protected branch', () => {
|
||||
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', () => {
|
||||
describe('when the `shouldHideNewMrOption` getter returns true', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
currentBranchId: 'regular',
|
||||
shouldHideNewMrOption: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('is rendered if no MR exists', () => {
|
||||
expect(vm.$el.textContent).not.toBe('');
|
||||
it("doesn't render the new MR checkbox", () => {
|
||||
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 RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.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 RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue';
|
||||
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
|
||||
|
@ -50,6 +51,8 @@ import {
|
|||
runnersDataPaginated,
|
||||
onlineContactTimeoutSecs,
|
||||
staleTimeoutSecs,
|
||||
emptyStateSvgPath,
|
||||
emptyStateFilteredSvgPath,
|
||||
} from '../mock_data';
|
||||
|
||||
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
|
||||
|
@ -78,6 +81,7 @@ describe('AdminRunnersApp', () => {
|
|||
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
|
||||
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
|
||||
const findRunnerList = () => wrapper.findComponent(RunnerList);
|
||||
const findRunnerListEmptyState = () => wrapper.findComponent(RunnerListEmptyState);
|
||||
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
|
||||
const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page');
|
||||
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
|
||||
|
@ -106,6 +110,8 @@ describe('AdminRunnersApp', () => {
|
|||
localMutations,
|
||||
onlineContactTimeoutSecs,
|
||||
staleTimeoutSecs,
|
||||
emptyStateSvgPath,
|
||||
emptyStateFilteredSvgPath,
|
||||
...provide,
|
||||
},
|
||||
...options,
|
||||
|
@ -457,12 +463,28 @@ describe('AdminRunnersApp', () => {
|
|||
runners: { nodes: [] },
|
||||
},
|
||||
});
|
||||
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('shows a message for no results', async () => {
|
||||
expect(wrapper.text()).toContain('No runners found');
|
||||
it('shows an empty state', () => {
|
||||
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 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';
|
||||
|
||||
describe('RunnerTypeCell', () => {
|
||||
describe('RunnerStatusCell', () => {
|
||||
let wrapper;
|
||||
|
||||
const findBadgeAt = (i) => wrapper.findAllComponents(GlBadge).at(i);
|
||||
const findStatusBadge = () => wrapper.findComponent(RunnerStatusBadge);
|
||||
const findPausedBadge = () => wrapper.findComponent(RunnerPausedBadge);
|
||||
|
||||
const createComponent = ({ runner = {} } = {}) => {
|
||||
wrapper = mount(RunnerStatusCell, {
|
||||
|
@ -29,7 +32,7 @@ describe('RunnerTypeCell', () => {
|
|||
createComponent();
|
||||
|
||||
expect(wrapper.text()).toMatchInterpolatedText('online');
|
||||
expect(findBadgeAt(0).text()).toBe('online');
|
||||
expect(findStatusBadge().text()).toBe('online');
|
||||
});
|
||||
|
||||
it('Displays offline status', () => {
|
||||
|
@ -40,7 +43,7 @@ describe('RunnerTypeCell', () => {
|
|||
});
|
||||
|
||||
expect(wrapper.text()).toMatchInterpolatedText('offline');
|
||||
expect(findBadgeAt(0).text()).toBe('offline');
|
||||
expect(findStatusBadge().text()).toBe('offline');
|
||||
});
|
||||
|
||||
it('Displays paused status', () => {
|
||||
|
@ -52,9 +55,7 @@ describe('RunnerTypeCell', () => {
|
|||
});
|
||||
|
||||
expect(wrapper.text()).toMatchInterpolatedText('online paused');
|
||||
|
||||
expect(findBadgeAt(0).text()).toBe('online');
|
||||
expect(findBadgeAt(1).text()).toBe('paused');
|
||||
expect(findPausedBadge().text()).toBe('paused');
|
||||
});
|
||||
|
||||
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 RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.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 RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue';
|
||||
import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
|
||||
|
@ -48,6 +49,8 @@ import {
|
|||
groupRunnersCountData,
|
||||
onlineContactTimeoutSecs,
|
||||
staleTimeoutSecs,
|
||||
emptyStateSvgPath,
|
||||
emptyStateFilteredSvgPath,
|
||||
} from '../mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
@ -75,6 +78,7 @@ describe('GroupRunnersApp', () => {
|
|||
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
|
||||
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
|
||||
const findRunnerList = () => wrapper.findComponent(RunnerList);
|
||||
const findRunnerListEmptyState = () => wrapper.findComponent(RunnerListEmptyState);
|
||||
const findRunnerRow = (id) => extendedWrapper(wrapper.findByTestId(`runner-row-${id}`));
|
||||
const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination));
|
||||
const findRunnerPaginationNext = () => findRunnerPagination().findByLabelText('Go to next page');
|
||||
|
@ -103,6 +107,8 @@ describe('GroupRunnersApp', () => {
|
|||
provide: {
|
||||
onlineContactTimeoutSecs,
|
||||
staleTimeoutSecs,
|
||||
emptyStateSvgPath,
|
||||
emptyStateFilteredSvgPath,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -388,8 +394,8 @@ describe('GroupRunnersApp', () => {
|
|||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('shows a message for no results', async () => {
|
||||
expect(wrapper.text()).toContain('No runners found');
|
||||
it('shows an empty state', async () => {
|
||||
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 staleTimeoutSecs = 7889238; // Ruby's `3.months`
|
||||
|
||||
export const emptyStateSvgPath = 'emptyStateSvgPath.svg';
|
||||
export const emptyStateFilteredSvgPath = 'emptyStateFilteredSvgPath.svg';
|
||||
|
||||
export {
|
||||
runnersData,
|
||||
runnersDataPaginated,
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
fromUrlQueryToSearch,
|
||||
fromSearchToUrl,
|
||||
fromSearchToVariables,
|
||||
isSearchFiltered,
|
||||
} from '~/runner/runner_search_utils';
|
||||
|
||||
describe('search_params.js', () => {
|
||||
|
@ -14,6 +15,7 @@ describe('search_params.js', () => {
|
|||
urlQuery: '',
|
||||
search: { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' },
|
||||
graphqlVariables: { sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: 'a single status',
|
||||
|
@ -268,7 +270,7 @@ describe('search_params.js', () => {
|
|||
describe('fromSearchToUrl', () => {
|
||||
examples.forEach(({ name, urlQuery, search }) => {
|
||||
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 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', () => {
|
||||
|
@ -288,7 +290,7 @@ describe('search_params.js', () => {
|
|||
const search = { filters: [], sort: 'CREATED_DESC' };
|
||||
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) {
|
||||
const option = await screen.findByLabelText(/Commit to .+ branch/);
|
||||
option.click();
|
||||
await option.click();
|
||||
} else {
|
||||
const option = await screen.findByLabelText('Create a new branch');
|
||||
option.click();
|
||||
await option.click();
|
||||
|
||||
const branchNameInput = await screen.findByTestId('ide-new-branch-name');
|
||||
fireEvent.input(branchNameInput, { target: { value: newBranchName } });
|
||||
|
|
|
@ -27,11 +27,9 @@ RSpec.describe ResolvesGroups do
|
|||
|
||||
let_it_be(:lookahead_fields) do
|
||||
<<~FIELDS
|
||||
contacts { nodes { id } }
|
||||
containerRepositoriesCount
|
||||
customEmoji { nodes { id } }
|
||||
fullPath
|
||||
organizations { nodes { id } }
|
||||
path
|
||||
dependencyProxyBlobCount
|
||||
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
|
||||
|
||||
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/',
|
||||
registration_token: Gitlab::CurrentSettings.runners_registration_token,
|
||||
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
|
||||
|
||||
|
@ -130,14 +132,16 @@ RSpec.describe Ci::RunnersHelper do
|
|||
let(:group) { create(:group) }
|
||||
|
||||
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,
|
||||
group_id: group.id,
|
||||
group_full_path: group.full_path,
|
||||
runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
before do
|
||||
stub_application_setting(pipeline_limit_per_project_user_sha: 1)
|
||||
stub_feature_flags(ci_enforce_throttle_pipelines_creation_override: false)
|
||||
end
|
||||
|
||||
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,
|
||||
project_id: project.id,
|
||||
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,
|
||||
project_id: project.id,
|
||||
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
|
||||
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
|
||||
|
||||
context 'without project hooks' do
|
||||
|
|
|
@ -3056,7 +3056,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
|||
end
|
||||
|
||||
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[
|
||||
enqueue
|
||||
|
@ -3076,7 +3076,19 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
|||
it 'schedules a new PipelineHooksWorker job' do
|
||||
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
|
||||
|
@ -3636,6 +3648,18 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
|||
pipeline.succeed!
|
||||
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
|
||||
|
||||
context 'with failed pipeline' do
|
||||
|
@ -3656,6 +3680,18 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
|||
|
||||
pipeline.drop
|
||||
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
|
||||
|
||||
context 'with skipped pipeline' do
|
||||
|
|
|
@ -11,6 +11,7 @@ RSpec.describe Ci::CreatePipelineService, :freeze_time, :clean_gitlab_redis_rate
|
|||
before do
|
||||
stub_ci_pipeline_yaml_file(gitlab_ci_yaml)
|
||||
stub_application_setting(pipeline_limit_per_project_user_sha: 1)
|
||||
stub_feature_flags(ci_enforce_throttle_pipelines_creation_override: false)
|
||||
end
|
||||
|
||||
context 'when user is under the limit' do
|
||||
|
|
|
@ -205,6 +205,25 @@ RSpec.describe Projects::UpdatePagesService do
|
|||
include_examples 'fails with outdated reference message'
|
||||
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
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ RSpec.shared_examples 'shows and resets runner registration token' do
|
|||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'shows no runners' do
|
||||
RSpec.shared_examples 'shows no runners registered' do
|
||||
it 'shows counts with 0' do
|
||||
expect(page).to have_text "Online runners 0"
|
||||
expect(page).to have_text "Offline runners 0"
|
||||
|
@ -70,13 +70,19 @@ RSpec.shared_examples 'shows no runners' do
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
RSpec.shared_examples 'shows runner in list' 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
|
||||
|
||||
it 'shows runner row' do
|
||||
|
|
|
@ -25,6 +25,16 @@ RSpec.describe PipelineHooksWorker do
|
|||
.not_to raise_error
|
||||
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
|
||||
|
||||
it_behaves_like 'worker with data consistency',
|
||||
|
|
|
@ -21,6 +21,20 @@ RSpec.describe PipelineNotificationWorker, :mailer do
|
|||
subject.perform(non_existing_record_id)
|
||||
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',
|
||||
described_class,
|
||||
data_consistency: :delayed
|
||||
|
|
|
@ -963,10 +963,10 @@
|
|||
stylelint-declaration-strict-value "1.8.0"
|
||||
stylelint-scss "4.1.0"
|
||||
|
||||
"@gitlab/svgs@2.17.0":
|
||||
version "2.17.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.17.0.tgz#56d0d11744859b3e1da80dedab2396a95cd01a02"
|
||||
integrity sha512-+cmn4ptdOFjSC8ByqD41kj1xSQ9/YFYLq/Es+jy5t12HmUtvYL8YRfNTlvApReSJ8SM7scwleVy4S19M15Siqw==
|
||||
"@gitlab/svgs@2.18.0":
|
||||
version "2.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.18.0.tgz#aafff929bc5365f7cad736b6d061895b3f9aa381"
|
||||
integrity sha512-Okbm4dAAf/aiaRojUT57yfqY/TVka/zAXN4T+hOx/Yho6wUT2eAJ8CcFpctPdt3kUNM4bHU2CZYoGqklbtXkmg==
|
||||
|
||||
"@gitlab/ui@40.7.1":
|
||||
version "40.7.1"
|
||||
|
|
Loading…
Reference in New Issue