Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f0707f413c
commit
1308dc5eb4
|
|
@ -1,6 +1,4 @@
|
|||
import api from '~/api';
|
||||
import { __ } from '~/locale';
|
||||
import createFlash from '~/flash';
|
||||
import * as types from './mutation_types';
|
||||
import { getCurrentHoverElement, setCurrentHoverElement, addInteractionClass } from '../utils';
|
||||
|
||||
|
|
@ -10,7 +8,6 @@ export default {
|
|||
},
|
||||
requestDataError({ commit }) {
|
||||
commit(types.REQUEST_DATA_ERROR);
|
||||
createFlash(__('An error occurred loading code navigation'));
|
||||
},
|
||||
fetchData({ commit, dispatch, state }) {
|
||||
commit(types.REQUEST_DATA);
|
||||
|
|
|
|||
|
|
@ -354,7 +354,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<div v-show="shouldShow">
|
||||
<div v-if="isLoading" class="loading"><gl-loading-icon /></div>
|
||||
<div v-if="isLoading" class="loading"><gl-loading-icon size="lg" /></div>
|
||||
<div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane">
|
||||
<compare-versions
|
||||
:merge-request-diffs="mergeRequestDiffs"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const FLASH_TYPES = {
|
|||
const hideFlash = (flashEl, fadeTransition = true) => {
|
||||
if (fadeTransition) {
|
||||
Object.assign(flashEl.style, {
|
||||
transition: 'opacity .3s',
|
||||
transition: 'opacity 0.15s',
|
||||
opacity: '0',
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import CommitMessageField from './message_field.vue';
|
|||
import Actions from './actions.vue';
|
||||
import SuccessMessage from './success_message.vue';
|
||||
import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -15,7 +14,6 @@ export default {
|
|||
CommitMessageField,
|
||||
SuccessMessage,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
data() {
|
||||
return {
|
||||
isCompact: true,
|
||||
|
|
@ -29,13 +27,9 @@ export default {
|
|||
...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']),
|
||||
overviewText() {
|
||||
return sprintf(
|
||||
this.glFeatures.stageAllByDefault
|
||||
? __(
|
||||
'<strong>%{stagedFilesLength} staged</strong> and <strong>%{changedFilesLength} unstaged</strong> changes',
|
||||
)
|
||||
: __(
|
||||
'<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes',
|
||||
),
|
||||
__(
|
||||
'<strong>%{stagedFilesLength} staged</strong> and <strong>%{changedFilesLength} unstaged</strong> changes',
|
||||
),
|
||||
{
|
||||
stagedFilesLength: this.stagedFiles.length,
|
||||
changedFilesLength: this.changedFiles.length,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import Icon from '~/vue_shared/components/icon.vue';
|
|||
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
|
||||
import NewDropdown from './new_dropdown/index.vue';
|
||||
import MrFileIcon from './mr_file_icon.vue';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
||||
export default {
|
||||
name: 'FileRowExtra',
|
||||
|
|
@ -19,7 +18,6 @@ export default {
|
|||
ChangedFileIcon,
|
||||
MrFileIcon,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
|
|
@ -57,15 +55,10 @@ export default {
|
|||
return n__('%d staged change', '%d staged changes', this.folderStagedCount);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
this.glFeatures.stageAllByDefault
|
||||
? __('%{staged} staged and %{unstaged} unstaged changes')
|
||||
: __('%{unstaged} unstaged and %{staged} staged changes'),
|
||||
{
|
||||
unstaged: this.folderUnstagedCount,
|
||||
staged: this.folderStagedCount,
|
||||
},
|
||||
);
|
||||
return sprintf(__('%{staged} staged and %{unstaged} unstaged changes'), {
|
||||
unstaged: this.folderUnstagedCount,
|
||||
staged: this.folderStagedCount,
|
||||
});
|
||||
},
|
||||
showTreeChangesCount() {
|
||||
return this.isTree && this.changesCount > 0 && !this.file.opened;
|
||||
|
|
|
|||
|
|
@ -79,10 +79,7 @@ export const createTempEntry = (
|
|||
|
||||
if (type === 'blob') {
|
||||
commit(types.TOGGLE_FILE_OPEN, file.path);
|
||||
|
||||
if (gon.features?.stageAllByDefault)
|
||||
commit(types.STAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) });
|
||||
else commit(types.ADD_FILE_TO_CHANGED, file.path);
|
||||
commit(types.STAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) });
|
||||
|
||||
dispatch('setFileActive', file.path);
|
||||
dispatch('triggerFilesChange');
|
||||
|
|
@ -250,9 +247,7 @@ export const renameEntry = ({ dispatch, commit, state, getters }, { path, name,
|
|||
if (isReset) {
|
||||
commit(types.REMOVE_FILE_FROM_STAGED_AND_CHANGED, newEntry);
|
||||
} else if (!isInChanges) {
|
||||
if (gon.features?.stageAllByDefault)
|
||||
commit(types.STAGE_CHANGE, { path: newPath, diffInfo: getters.getDiffInfo(newPath) });
|
||||
else commit(types.ADD_FILE_TO_CHANGED, newPath);
|
||||
commit(types.STAGE_CHANGE, { path: newPath, diffInfo: getters.getDiffInfo(newPath) });
|
||||
}
|
||||
|
||||
if (!newEntry.tempFile) {
|
||||
|
|
|
|||
|
|
@ -158,9 +158,7 @@ export const changeFileContent = ({ commit, state, getters }, { path, content })
|
|||
const indexOfChangedFile = state.changedFiles.findIndex(f => f.path === path);
|
||||
|
||||
if (file.changed && indexOfChangedFile === -1) {
|
||||
if (gon.features?.stageAllByDefault)
|
||||
commit(types.STAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) });
|
||||
else commit(types.ADD_FILE_TO_CHANGED, path);
|
||||
commit(types.STAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) });
|
||||
} else if (!file.changed && !file.tempFile && indexOfChangedFile !== -1) {
|
||||
commit(types.REMOVE_FILE_FROM_CHANGED, path);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
<script>
|
||||
import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'GroupEmptyState',
|
||||
components: {
|
||||
GlEmptyState,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['config']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-empty-state
|
||||
:title="s__('ContainerRegistry|There are no container images available in this group')"
|
||||
:svg-path="config.noContainersImage"
|
||||
class="container-message"
|
||||
>
|
||||
<template #description>
|
||||
<p class="js-no-container-images-text">
|
||||
<gl-sprintf
|
||||
:message="
|
||||
s__(
|
||||
`ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here. %{docLinkStart}More Information%{docLinkEnd}`,
|
||||
)
|
||||
"
|
||||
>
|
||||
<template #docLink="{content}">
|
||||
<gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</template>
|
||||
</gl-empty-state>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<script>
|
||||
import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'ProjectEmptyState',
|
||||
components: {
|
||||
ClipboardButton,
|
||||
GlEmptyState,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['config']),
|
||||
dockerBuildCommand() {
|
||||
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
|
||||
return `docker build -t ${this.config.repositoryUrl} .`;
|
||||
},
|
||||
dockerPushCommand() {
|
||||
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
|
||||
return `docker push ${this.config.repositoryUrl}`;
|
||||
},
|
||||
dockerLoginCommand() {
|
||||
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
|
||||
return `docker login ${this.config.registryHostUrlWithPort}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-empty-state
|
||||
:title="s__('ContainerRegistry|There are no container images stored for this project')"
|
||||
:svg-path="config.noContainersImage"
|
||||
class="container-message"
|
||||
>
|
||||
<template #description>
|
||||
<p class="js-no-container-images-text">
|
||||
<gl-sprintf
|
||||
:message="
|
||||
s__(`ContainerRegistry|With the Container Registry, every project can have its own space to
|
||||
store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`)
|
||||
"
|
||||
>
|
||||
<template #docLink="{content}">
|
||||
<gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
|
||||
<p class="js-not-logged-in-to-registry-text">
|
||||
<gl-sprintf
|
||||
:message="
|
||||
s__(`ContainerRegistry|If you are not already logged in, you need to authenticate to
|
||||
the Container Registry by using your GitLab username and password. If you have
|
||||
%{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a
|
||||
%{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd}
|
||||
instead of a password.`)
|
||||
"
|
||||
>
|
||||
<template #twofaDocLink="{content}">
|
||||
<gl-link :href="config.twoFactorAuthHelpLink" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
<template #personalAccessTokensDocLink="{content}">
|
||||
<gl-link :href="config.personalAccessTokensHelpLink" target="_blank">{{
|
||||
content
|
||||
}}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<div class="input-group append-bottom-10">
|
||||
<input :value="dockerLoginCommand" type="text" class="form-control monospace" readonly />
|
||||
<span class="input-group-append">
|
||||
<clipboard-button
|
||||
:text="dockerLoginCommand"
|
||||
:title="s__('ContainerRegistry|Copy login command')"
|
||||
class="input-group-text"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<p></p>
|
||||
<p>
|
||||
{{
|
||||
s__(
|
||||
'ContainerRegistry|You can add an image to this registry with the following commands:',
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
|
||||
<div class="input-group append-bottom-10">
|
||||
<input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly />
|
||||
<span class="input-group-append">
|
||||
<clipboard-button
|
||||
:text="dockerBuildCommand"
|
||||
:title="s__('ContainerRegistry|Copy build command')"
|
||||
class="input-group-text"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<input :value="dockerPushCommand" type="text" class="form-control monospace" readonly />
|
||||
<span class="input-group-append">
|
||||
<clipboard-button
|
||||
:text="dockerPushCommand"
|
||||
:title="s__('ContainerRegistry|Copy push command')"
|
||||
class="input-group-text"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</gl-empty-state>
|
||||
</template>
|
||||
|
|
@ -1,7 +1,332 @@
|
|||
<script>
|
||||
export default {};
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import {
|
||||
GlTable,
|
||||
GlFormCheckbox,
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlTooltipDirective,
|
||||
GlPagination,
|
||||
GlModal,
|
||||
GlLoadingIcon,
|
||||
GlSprintf,
|
||||
GlEmptyState,
|
||||
GlResizeObserverDirective,
|
||||
} from '@gitlab/ui';
|
||||
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
|
||||
import { n__, s__ } from '~/locale';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import { numberToHumanSize } from '~/lib/utils/number_utils';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
import Tracking from '~/tracking';
|
||||
import {
|
||||
LIST_KEY_TAG,
|
||||
LIST_KEY_IMAGE_ID,
|
||||
LIST_KEY_SIZE,
|
||||
LIST_KEY_LAST_UPDATED,
|
||||
LIST_KEY_ACTIONS,
|
||||
LIST_KEY_CHECKBOX,
|
||||
LIST_LABEL_TAG,
|
||||
LIST_LABEL_IMAGE_ID,
|
||||
LIST_LABEL_SIZE,
|
||||
LIST_LABEL_LAST_UPDATED,
|
||||
} from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlTable,
|
||||
GlFormCheckbox,
|
||||
GlButton,
|
||||
GlIcon,
|
||||
ClipboardButton,
|
||||
GlPagination,
|
||||
GlModal,
|
||||
GlLoadingIcon,
|
||||
GlSprintf,
|
||||
GlEmptyState,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
GlResizeObserver: GlResizeObserverDirective,
|
||||
},
|
||||
mixins: [timeagoMixin, Tracking.mixin()],
|
||||
data() {
|
||||
return {
|
||||
selectedItems: [],
|
||||
itemsToBeDeleted: [],
|
||||
selectAllChecked: false,
|
||||
modalDescription: null,
|
||||
isDesktop: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['tags', 'tagsPagination', 'isLoading', 'config']),
|
||||
imageName() {
|
||||
const { name } = JSON.parse(window.atob(this.$route.params.id));
|
||||
return name;
|
||||
},
|
||||
fields() {
|
||||
return [
|
||||
{ key: LIST_KEY_CHECKBOX, label: '' },
|
||||
{ key: LIST_KEY_TAG, label: LIST_LABEL_TAG },
|
||||
{ key: LIST_KEY_IMAGE_ID, label: LIST_LABEL_IMAGE_ID },
|
||||
{ key: LIST_KEY_SIZE, label: LIST_LABEL_SIZE },
|
||||
{ key: LIST_KEY_LAST_UPDATED, label: LIST_LABEL_LAST_UPDATED },
|
||||
{ key: LIST_KEY_ACTIONS, label: '' },
|
||||
].filter(f => f.key !== LIST_KEY_CHECKBOX || this.isDesktop);
|
||||
},
|
||||
isMultiDelete() {
|
||||
return this.itemsToBeDeleted.length > 1;
|
||||
},
|
||||
tracking() {
|
||||
return {
|
||||
label: this.isMultiDelete ? 'bulk_registry_tag_delete' : 'registry_tag_delete',
|
||||
};
|
||||
},
|
||||
modalAction() {
|
||||
return n__(
|
||||
'ContainerRegistry|Remove tag',
|
||||
'ContainerRegistry|Remove tags',
|
||||
this.isMultiDelete ? this.itemsToBeDeleted.length : 1,
|
||||
);
|
||||
},
|
||||
currentPage: {
|
||||
get() {
|
||||
return this.tagsPagination.page;
|
||||
},
|
||||
set(page) {
|
||||
this.requestTagsList({ pagination: { page }, id: this.$route.params.id });
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['requestTagsList', 'requestDeleteTag', 'requestDeleteTags']),
|
||||
setModalDescription(itemIndex = -1) {
|
||||
if (itemIndex === -1) {
|
||||
this.modalDescription = {
|
||||
message: s__(`ContainerRegistry|You are about to remove %{item} tags. Are you sure?`),
|
||||
item: this.itemsToBeDeleted.length,
|
||||
};
|
||||
} else {
|
||||
const { path } = this.tags[itemIndex];
|
||||
|
||||
this.modalDescription = {
|
||||
message: s__(`ContainerRegistry|You are about to remove %{item}. Are you sure?`),
|
||||
item: path,
|
||||
};
|
||||
}
|
||||
},
|
||||
formatSize(size) {
|
||||
return numberToHumanSize(size);
|
||||
},
|
||||
layers(layers) {
|
||||
return layers ? n__('%d layer', '%d layers', layers) : '';
|
||||
},
|
||||
onSelectAllChange() {
|
||||
if (this.selectAllChecked) {
|
||||
this.deselectAll();
|
||||
} else {
|
||||
this.selectAll();
|
||||
}
|
||||
},
|
||||
selectAll() {
|
||||
this.selectedItems = this.tags.map((x, index) => index);
|
||||
this.selectAllChecked = true;
|
||||
},
|
||||
deselectAll() {
|
||||
this.selectedItems = [];
|
||||
this.selectAllChecked = false;
|
||||
},
|
||||
updateSelectedItems(index) {
|
||||
const delIndex = this.selectedItems.findIndex(x => x === index);
|
||||
|
||||
if (delIndex > -1) {
|
||||
this.selectedItems.splice(delIndex, 1);
|
||||
this.selectAllChecked = false;
|
||||
} else {
|
||||
this.selectedItems.push(index);
|
||||
|
||||
if (this.selectedItems.length === this.tags.length) {
|
||||
this.selectAllChecked = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteSingleItem(index) {
|
||||
this.setModalDescription(index);
|
||||
this.itemsToBeDeleted = [index];
|
||||
this.track('click_button');
|
||||
this.$refs.deleteModal.show();
|
||||
},
|
||||
deleteMultipleItems() {
|
||||
this.itemsToBeDeleted = [...this.selectedItems];
|
||||
if (this.selectedItems.length === 1) {
|
||||
this.setModalDescription(this.itemsToBeDeleted[0]);
|
||||
} else if (this.selectedItems.length > 1) {
|
||||
this.setModalDescription();
|
||||
}
|
||||
this.track('click_button');
|
||||
this.$refs.deleteModal.show();
|
||||
},
|
||||
handleSingleDelete(itemToDelete) {
|
||||
this.itemsToBeDeleted = [];
|
||||
this.requestDeleteTag({ tag: itemToDelete, imageId: this.$route.params.id });
|
||||
},
|
||||
handleMultipleDelete() {
|
||||
const { itemsToBeDeleted } = this;
|
||||
this.itemsToBeDeleted = [];
|
||||
this.selectedItems = [];
|
||||
|
||||
this.requestDeleteTags({
|
||||
ids: itemsToBeDeleted.map(x => this.tags[x].name),
|
||||
imageId: this.$route.params.id,
|
||||
});
|
||||
},
|
||||
onDeletionConfirmed() {
|
||||
this.track('confirm_delete');
|
||||
if (this.isMultiDelete) {
|
||||
this.handleMultipleDelete();
|
||||
} else {
|
||||
const index = this.itemsToBeDeleted[0];
|
||||
this.handleSingleDelete(this.tags[index]);
|
||||
}
|
||||
},
|
||||
handleResize() {
|
||||
this.isDesktop = GlBreakpointInstance.isDesktop();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
<div
|
||||
v-gl-resize-observer="handleResize"
|
||||
class="my-3 position-absolute w-100 slide-enter-to-element"
|
||||
>
|
||||
<div class="d-flex my-3 align-items-center">
|
||||
<h4>
|
||||
<gl-sprintf :message="s__('ContainerRegistry|%{imageName} tags')">
|
||||
<template #imageName>
|
||||
{{ imageName }}
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</h4>
|
||||
</div>
|
||||
<gl-loading-icon v-if="isLoading" />
|
||||
<template v-else-if="tags.length > 0">
|
||||
<gl-table :items="tags" :fields="fields" :stacked="!isDesktop">
|
||||
<template v-if="isDesktop" #head(checkbox)>
|
||||
<gl-form-checkbox
|
||||
ref="mainCheckbox"
|
||||
:checked="selectAllChecked"
|
||||
@change="onSelectAllChange"
|
||||
/>
|
||||
</template>
|
||||
<template #head(actions)>
|
||||
<gl-button
|
||||
ref="bulkDeleteButton"
|
||||
v-gl-tooltip
|
||||
:disabled="!selectedItems || selectedItems.length === 0"
|
||||
class="float-right"
|
||||
variant="danger"
|
||||
:title="s__('ContainerRegistry|Remove selected tags')"
|
||||
:aria-label="s__('ContainerRegistry|Remove selected tags')"
|
||||
@click="deleteMultipleItems()"
|
||||
>
|
||||
<gl-icon name="remove" />
|
||||
</gl-button>
|
||||
</template>
|
||||
|
||||
<template #cell(checkbox)="{index}">
|
||||
<gl-form-checkbox
|
||||
ref="rowCheckbox"
|
||||
class="js-row-checkbox"
|
||||
:checked="selectedItems.includes(index)"
|
||||
@change="updateSelectedItems(index)"
|
||||
/>
|
||||
</template>
|
||||
<template #cell(name)="{item}">
|
||||
<span ref="rowName">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
<clipboard-button
|
||||
v-if="item.location"
|
||||
ref="rowClipboardButton"
|
||||
:title="item.location"
|
||||
:text="item.location"
|
||||
css-class="btn-default btn-transparent btn-clipboard"
|
||||
/>
|
||||
</template>
|
||||
<template #cell(short_revision)="{value}">
|
||||
<span ref="rowShortRevision">
|
||||
{{ value }}
|
||||
</span>
|
||||
</template>
|
||||
<template #cell(total_size)="{item}">
|
||||
<span ref="rowSize">
|
||||
{{ formatSize(item.total_size) }}
|
||||
<template v-if="item.total_size && item.layers">
|
||||
·
|
||||
</template>
|
||||
{{ layers(item.layers) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #cell(created_at)="{value}">
|
||||
<span ref="rowTime">
|
||||
{{ timeFormatted(value) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #cell(actions)="{index, item}">
|
||||
<gl-button
|
||||
ref="singleDeleteButton"
|
||||
:title="s__('ContainerRegistry|Remove tag')"
|
||||
:aria-label="s__('ContainerRegistry|Remove tag')"
|
||||
:disabled="!item.destroy_path"
|
||||
variant="danger"
|
||||
:class="['js-delete-registry float-right btn-inverted btn-border-color btn-icon']"
|
||||
@click="deleteSingleItem(index)"
|
||||
>
|
||||
<gl-icon name="remove" />
|
||||
</gl-button>
|
||||
</template>
|
||||
</gl-table>
|
||||
<gl-pagination
|
||||
ref="pagination"
|
||||
v-model="currentPage"
|
||||
:per-page="tagsPagination.perPage"
|
||||
:total-items="tagsPagination.total"
|
||||
align="center"
|
||||
class="w-100"
|
||||
/>
|
||||
<gl-modal
|
||||
ref="deleteModal"
|
||||
modal-id="delete-tag-modal"
|
||||
ok-variant="danger"
|
||||
@ok="onDeletionConfirmed"
|
||||
@cancel="track('cancel_delete')"
|
||||
>
|
||||
<template #modal-title>{{ modalAction }}</template>
|
||||
<template #modal-ok>{{ modalAction }}</template>
|
||||
<p v-if="modalDescription">
|
||||
<gl-sprintf :message="modalDescription.message">
|
||||
<template #item>
|
||||
<b>{{ modalDescription.item }}</b>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</gl-modal>
|
||||
</template>
|
||||
<gl-empty-state
|
||||
v-else
|
||||
:title="s__('ContainerRegistry|This image has no active tags')"
|
||||
:svg-path="config.noContainersImage"
|
||||
:description="
|
||||
s__(
|
||||
`ContainerRegistry|The last tag related to this image was recently removed.
|
||||
This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process.
|
||||
If you have any questions, contact your administrator.`,
|
||||
)
|
||||
"
|
||||
class="mx-auto my-0"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,214 @@
|
|||
<script>
|
||||
export default {};
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import {
|
||||
GlLoadingIcon,
|
||||
GlEmptyState,
|
||||
GlPagination,
|
||||
GlTooltipDirective,
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
} from '@gitlab/ui';
|
||||
import Tracking from '~/tracking';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import ProjectEmptyState from '../components/project_empty_state.vue';
|
||||
import GroupEmptyState from '../components/group_empty_state.vue';
|
||||
|
||||
export default {
|
||||
name: 'RegistryListApp',
|
||||
components: {
|
||||
GlEmptyState,
|
||||
GlLoadingIcon,
|
||||
GlPagination,
|
||||
ProjectEmptyState,
|
||||
GroupEmptyState,
|
||||
ClipboardButton,
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
data() {
|
||||
return {
|
||||
itemToDelete: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['config', 'isLoading', 'images', 'pagination']),
|
||||
tracking() {
|
||||
return {
|
||||
label: 'registry_repository_delete',
|
||||
};
|
||||
},
|
||||
currentPage: {
|
||||
get() {
|
||||
return this.pagination.page;
|
||||
},
|
||||
set(page) {
|
||||
this.requestImagesList({ page });
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['requestImagesList', 'requestDeleteImage']),
|
||||
deleteImage(item) {
|
||||
// This event is already tracked in the system and so the name must be kept to aggregate the data
|
||||
this.track('click_button');
|
||||
this.itemToDelete = item;
|
||||
this.$refs.deleteModal.show();
|
||||
},
|
||||
handleDeleteRepository() {
|
||||
this.track('confirm_delete');
|
||||
this.requestDeleteImage(this.itemToDelete.destroy_path);
|
||||
this.itemToDelete = {};
|
||||
},
|
||||
encodeListItem(item) {
|
||||
const params = JSON.stringify({ name: item.path, tags_path: item.tags_path });
|
||||
return window.btoa(params);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
<div class="position-absolute w-100 slide-enter-from-element">
|
||||
<gl-empty-state
|
||||
v-if="config.characterError"
|
||||
:title="s__('ContainerRegistry|Docker connection error')"
|
||||
:svg-path="config.containersErrorImage"
|
||||
>
|
||||
<template #description>
|
||||
<p>
|
||||
<gl-sprintf
|
||||
:message="
|
||||
s__(`ContainerRegistry|We are having trouble connecting to Docker, which could be due to an
|
||||
issue with your project name or path.
|
||||
%{docLinkStart}More Information%{docLinkEnd}`)
|
||||
"
|
||||
>
|
||||
<template #docLink="{content}">
|
||||
<gl-link :href="`${config.helpPagePath}#docker-connection-error`" target="_blank">
|
||||
{{ content }}
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</template>
|
||||
</gl-empty-state>
|
||||
|
||||
<template v-else>
|
||||
<gl-loading-icon v-if="isLoading" size="md" class="prepend-top-16" />
|
||||
|
||||
<template v-else>
|
||||
<div v-if="images.length" ref="imagesList">
|
||||
<h4>{{ s__('ContainerRegistry|Container Registry') }}</h4>
|
||||
<p>
|
||||
<gl-sprintf
|
||||
:message="
|
||||
s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
|
||||
project can have its own space to store its Docker images.
|
||||
%{docLinkStart}More Information%{docLinkEnd}`)
|
||||
"
|
||||
>
|
||||
<template #docLink="{content}">
|
||||
<gl-link :href="config.helpPagePath" target="_blank">
|
||||
{{ content }}
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<div
|
||||
v-for="(listItem, index) in images"
|
||||
:key="index"
|
||||
ref="rowItem"
|
||||
:class="[
|
||||
'd-flex justify-content-between align-items-center py-2 border-bottom',
|
||||
{ 'border-top': index === 0 },
|
||||
]"
|
||||
>
|
||||
<div>
|
||||
<router-link
|
||||
ref="detailsLink"
|
||||
:to="{ name: 'details', params: { id: encodeListItem(listItem) } }"
|
||||
>
|
||||
{{ listItem.path }}
|
||||
</router-link>
|
||||
<clipboard-button
|
||||
v-if="listItem.location"
|
||||
ref="clipboardButton"
|
||||
:text="listItem.location"
|
||||
:title="listItem.location"
|
||||
css-class="btn-default btn-transparent btn-clipboard"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-gl-tooltip="{ disabled: listItem.destroy_path }"
|
||||
class="d-none d-sm-block"
|
||||
:title="
|
||||
s__(
|
||||
'ContainerRegistry|Missing or insufficient permission, delete button disabled',
|
||||
)
|
||||
"
|
||||
>
|
||||
<gl-button
|
||||
ref="deleteImageButton"
|
||||
v-gl-tooltip
|
||||
:disabled="!listItem.destroy_path"
|
||||
:title="s__('ContainerRegistry|Remove repository')"
|
||||
:aria-label="s__('ContainerRegistry|Remove repository')"
|
||||
class="btn-inverted"
|
||||
variant="danger"
|
||||
@click="deleteImage(listItem)"
|
||||
>
|
||||
<gl-icon name="remove" />
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<gl-pagination
|
||||
v-model="currentPage"
|
||||
:per-page="pagination.perPage"
|
||||
:total-items="pagination.total"
|
||||
align="center"
|
||||
class="w-100 mt-2"
|
||||
/>
|
||||
</div>
|
||||
<template v-else>
|
||||
<project-empty-state v-if="!config.isGroupPage" />
|
||||
<group-empty-state v-else />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<gl-modal
|
||||
ref="deleteModal"
|
||||
modal-id="delete-image-modal"
|
||||
ok-variant="danger"
|
||||
@ok="handleDeleteRepository"
|
||||
@cancel="track('cancel_delete')"
|
||||
>
|
||||
<template #modal-title>{{ s__('ContainerRegistry|Remove repository') }}</template>
|
||||
<p>
|
||||
<gl-sprintf
|
||||
:message=" s__(
|
||||
'ContainerRegistry|You are about to remove repository %{title}. Once you confirm, this repository will be permanently deleted.',
|
||||
),"
|
||||
>
|
||||
<template #title>
|
||||
<b>{{ itemToDelete.path }}</b>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<template #modal-ok>{{ __('Remove') }}</template>
|
||||
</gl-modal>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -45,11 +45,11 @@ export const requestImagesList = ({ commit, dispatch, state }, pagination = {})
|
|||
|
||||
export const requestTagsList = ({ commit, dispatch }, { pagination = {}, id }) => {
|
||||
commit(types.SET_MAIN_LOADING, true);
|
||||
const url = window.atob(id);
|
||||
const { tags_path } = JSON.parse(window.atob(id));
|
||||
|
||||
const { page = DEFAULT_PAGE, perPage = DEFAULT_PAGE_SIZE } = pagination;
|
||||
return axios
|
||||
.get(url, { params: { page, per_page: perPage } })
|
||||
.get(tags_path, { params: { page, per_page: perPage } })
|
||||
.then(({ data, headers }) => {
|
||||
dispatch('receiveTagsListSuccess', { data, headers });
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export default {
|
|||
[types.SET_INITIAL_STATE](state, config) {
|
||||
state.config = {
|
||||
...config,
|
||||
isGroupPage: config.isGroupPage !== undefined,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,6 @@
|
|||
class IdeController < ApplicationController
|
||||
layout 'fullscreen'
|
||||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:stage_all_by_default, default_enabled: true)
|
||||
end
|
||||
|
||||
def index
|
||||
Gitlab::UsageDataCounters::WebIdeCounter.increment_views_count
|
||||
end
|
||||
|
|
|
|||
|
|
@ -186,7 +186,8 @@ module Ci
|
|||
},
|
||||
execute_params: {
|
||||
ignore_skip_ci: true,
|
||||
bridge: self
|
||||
bridge: self,
|
||||
merge_request: parent_pipeline.merge_request
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -77,9 +77,7 @@ module Ci
|
|||
|
||||
validates :sha, presence: { unless: :importing? }
|
||||
validates :ref, presence: { unless: :importing? }
|
||||
validates :merge_request, presence: { if: :merge_request_event? }
|
||||
validates :merge_request, absence: { unless: :merge_request_event? }
|
||||
validates :tag, inclusion: { in: [false], if: :merge_request_event? }
|
||||
validates :tag, inclusion: { in: [false], if: :merge_request? }
|
||||
|
||||
validates :external_pull_request, presence: { if: :external_pull_request_event? }
|
||||
validates :external_pull_request, absence: { unless: :external_pull_request_event? }
|
||||
|
|
@ -662,7 +660,7 @@ module Ci
|
|||
|
||||
variables.concat(predefined_commit_variables)
|
||||
|
||||
if merge_request_event? && merge_request
|
||||
if merge_request?
|
||||
variables.append(key: 'CI_MERGE_REQUEST_EVENT_TYPE', value: merge_request_event_type.to_s)
|
||||
variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', value: source_sha.to_s)
|
||||
variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA', value: target_sha.to_s)
|
||||
|
|
@ -720,7 +718,7 @@ module Ci
|
|||
# All the merge requests for which the current pipeline runs/ran against
|
||||
def all_merge_requests
|
||||
@all_merge_requests ||=
|
||||
if merge_request_event?
|
||||
if merge_request?
|
||||
MergeRequest.where(id: merge_request_id)
|
||||
else
|
||||
MergeRequest.where(source_project_id: project_id, source_branch: ref)
|
||||
|
|
@ -812,7 +810,7 @@ module Ci
|
|||
# * nil: Modified path can not be evaluated
|
||||
def modified_paths
|
||||
strong_memoize(:modified_paths) do
|
||||
if merge_request_event?
|
||||
if merge_request?
|
||||
merge_request.modified_paths
|
||||
elsif branch_updated?
|
||||
push_details.modified_paths
|
||||
|
|
@ -836,12 +834,12 @@ module Ci
|
|||
ref == project.default_branch
|
||||
end
|
||||
|
||||
def triggered_by_merge_request?
|
||||
merge_request_event? && merge_request_id.present?
|
||||
def merge_request?
|
||||
merge_request_id.present?
|
||||
end
|
||||
|
||||
def detached_merge_request_pipeline?
|
||||
triggered_by_merge_request? && target_sha.nil?
|
||||
merge_request? && target_sha.nil?
|
||||
end
|
||||
|
||||
def legacy_detached_merge_request_pipeline?
|
||||
|
|
@ -849,7 +847,7 @@ module Ci
|
|||
end
|
||||
|
||||
def merge_request_pipeline?
|
||||
triggered_by_merge_request? && target_sha.present?
|
||||
merge_request? && target_sha.present?
|
||||
end
|
||||
|
||||
def merge_request_ref?
|
||||
|
|
@ -865,7 +863,7 @@ module Ci
|
|||
end
|
||||
|
||||
def source_ref
|
||||
if triggered_by_merge_request?
|
||||
if merge_request?
|
||||
merge_request.source_branch
|
||||
else
|
||||
ref
|
||||
|
|
@ -885,7 +883,7 @@ module Ci
|
|||
end
|
||||
|
||||
def merge_request_event_type
|
||||
return unless merge_request_event?
|
||||
return unless merge_request?
|
||||
|
||||
strong_memoize(:merge_request_event_type) do
|
||||
if merge_request_pipeline?
|
||||
|
|
@ -918,7 +916,7 @@ module Ci
|
|||
|
||||
def git_ref
|
||||
strong_memoize(:git_ref) do
|
||||
if merge_request_event?
|
||||
if merge_request?
|
||||
##
|
||||
# In the future, we're going to change this ref to
|
||||
# merge request's merged reference, such as "refs/merge-requests/:iid/merge".
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ module Ci
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
delegate :merge_request_event?,
|
||||
delegate :merge_request?,
|
||||
:merge_request_ref?,
|
||||
:legacy_detached_merge_request_pipeline?,
|
||||
:merge_train_pipeline?, to: :pipeline
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ module HasRef
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
def branch?
|
||||
!tag? && !merge_request_event?
|
||||
!tag? && !merge_request?
|
||||
end
|
||||
|
||||
def git_ref
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ module ReactiveCaching
|
|||
ExceededReactiveCacheLimit = Class.new(StandardError)
|
||||
|
||||
included do
|
||||
extend ActiveModel::Naming
|
||||
|
||||
class_attribute :reactive_cache_key
|
||||
class_attribute :reactive_cache_lease_timeout
|
||||
class_attribute :reactive_cache_refresh_interval
|
||||
|
|
|
|||
|
|
@ -1163,7 +1163,7 @@ class MergeRequest < ApplicationRecord
|
|||
# Since deployments run on a merge request ref (e.g. `refs/merge-requests/:iid/head`),
|
||||
# we cannot look up environments with source branch name.
|
||||
def environments
|
||||
return Environment.none unless actual_head_pipeline&.triggered_by_merge_request?
|
||||
return Environment.none unless actual_head_pipeline&.merge_request?
|
||||
|
||||
actual_head_pipeline.environments
|
||||
end
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ class MergeRequest::Pipelines
|
|||
pipelines.joins(shas_table)
|
||||
end
|
||||
|
||||
# NOTE: this method returns only parent merge request pipelines.
|
||||
# Child merge request pipelines have a different source.
|
||||
def triggered_by_merge_request
|
||||
source_project.ci_pipelines
|
||||
.where(source: :merge_request_event, merge_request: merge_request)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ class WikiPage
|
|||
PageChangedError = Class.new(StandardError)
|
||||
PageRenameError = Class.new(StandardError)
|
||||
|
||||
MAX_TITLE_BYTES = 245
|
||||
MAX_DIRECTORY_BYTES = 255
|
||||
|
||||
include ActiveModel::Validations
|
||||
include ActiveModel::Conversion
|
||||
include StaticModel
|
||||
|
|
@ -51,6 +54,7 @@ class WikiPage
|
|||
|
||||
validates :title, presence: true
|
||||
validates :content, presence: true
|
||||
validate :validate_path_limits, if: :title_changed?
|
||||
|
||||
# The GitLab ProjectWiki instance.
|
||||
attr_reader :wiki
|
||||
|
|
@ -262,7 +266,7 @@ class WikiPage
|
|||
end
|
||||
|
||||
def title_changed?
|
||||
title.present? && self.class.unhyphenize(@page.url_path) != title
|
||||
title.present? && (@page.nil? || self.class.unhyphenize(@page.url_path) != title)
|
||||
end
|
||||
|
||||
# Updates the current @attributes hash by merging a hash of params
|
||||
|
|
@ -324,4 +328,16 @@ class WikiPage
|
|||
set_attributes
|
||||
@persisted = errors.blank?
|
||||
end
|
||||
|
||||
def validate_path_limits
|
||||
*dirnames, title = @attributes[:title].split('/')
|
||||
|
||||
if title.bytesize > MAX_TITLE_BYTES
|
||||
errors.add(:title, _("exceeds the limit of %{bytes} bytes for page titles") % { bytes: MAX_TITLE_BYTES })
|
||||
end
|
||||
|
||||
if dirnames.any? { |d| d.bytesize > MAX_DIRECTORY_BYTES }
|
||||
errors.add(:title, _("exceeds the limit of %{bytes} bytes for directory names") % { bytes: MAX_DIRECTORY_BYTES })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ module Ci
|
|||
|
||||
def merge_request_presenter
|
||||
strong_memoize(:merge_request_presenter) do
|
||||
if pipeline.triggered_by_merge_request?
|
||||
if pipeline.merge_request?
|
||||
pipeline.merge_request.present(current_user: current_user)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class PipelineEntity < Grape::Entity
|
|||
expose :flags do
|
||||
expose :stuck?, as: :stuck
|
||||
expose :auto_devops_source?, as: :auto_devops
|
||||
expose :merge_request_event?, as: :merge_request
|
||||
expose :merge_request?, as: :merge_request
|
||||
expose :has_yaml_errors?, as: :yaml_errors
|
||||
expose :can_retry?, as: :retryable
|
||||
expose :can_cancel?, as: :cancelable
|
||||
|
|
@ -59,11 +59,11 @@ class PipelineEntity < Grape::Entity
|
|||
|
||||
expose :tag?, as: :tag
|
||||
expose :branch?, as: :branch
|
||||
expose :merge_request_event?, as: :merge_request
|
||||
expose :merge_request?, as: :merge_request
|
||||
end
|
||||
|
||||
expose :commit, using: CommitEntity
|
||||
expose :merge_request_event_type, if: -> (pipeline, _) { pipeline.merge_request_event? }
|
||||
expose :merge_request_event_type, if: -> (pipeline, _) { pipeline.merge_request? }
|
||||
expose :source_sha, if: -> (pipeline, _) { pipeline.merge_request_pipeline? }
|
||||
expose :target_sha, if: -> (pipeline, _) { pipeline.merge_request_pipeline? }
|
||||
expose :yaml_errors, if: -> (pipeline, _) { pipeline.has_yaml_errors? }
|
||||
|
|
@ -104,7 +104,7 @@ class PipelineEntity < Grape::Entity
|
|||
end
|
||||
|
||||
def has_presentable_merge_request?
|
||||
pipeline.triggered_by_merge_request? &&
|
||||
pipeline.merge_request? &&
|
||||
can?(request.current_user, :read_merge_request, pipeline.merge_request)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ module MergeRequests
|
|||
##
|
||||
# UpdateMergeRequestsWorker could be retried by an exception.
|
||||
# pipelines for merge request should not be recreated in such case.
|
||||
return false if !allow_duplicate && merge_request.find_actual_head_pipeline&.triggered_by_merge_request?
|
||||
return false if !allow_duplicate && merge_request.find_actual_head_pipeline&.merge_request?
|
||||
return false if merge_request.has_no_commits?
|
||||
|
||||
true
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
|
||||
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
|
||||
"registry_host_url_with_port" => escape_once(registry_config.host_port),
|
||||
is_group_page: true,
|
||||
character_error: @character_error.to_s } }
|
||||
- else
|
||||
#js-vue-registry-images{ data: { endpoint: group_container_registries_path(@group, format: :json),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@
|
|||
%button.btn.btn-sm{ "@click" => "cancelDiscardConfirmation(file)" } Cancel
|
||||
.editor-wrap{ ":class" => "classObject" }
|
||||
.loading
|
||||
%i.fa.fa-spinner.fa-spin
|
||||
.spinner.spinner-md
|
||||
.editor
|
||||
%pre{ "style" => "height: 350px" }
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
#conflicts{ "v-cloak" => "true", data: { conflicts_path: conflicts_project_merge_request_path(@merge_request.project, @merge_request, format: :json),
|
||||
resolve_conflicts_path: resolve_conflicts_project_merge_request_path(@merge_request.project, @merge_request) } }
|
||||
.loading{ "v-if" => "isLoading" }
|
||||
%i.fa.fa-spinner.fa-spin
|
||||
.spinner.spinner-md
|
||||
|
||||
.nothing-here-block{ "v-if" => "hasError" }
|
||||
{{conflictsData.errorMessage}}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@
|
|||
= dropdown_content
|
||||
= dropdown_loading
|
||||
.card-footer
|
||||
.text-center= icon('spinner spin', class: 'js-source-loading')
|
||||
.text-center
|
||||
.js-source-loading.mt-1.spinner.spinner-sm
|
||||
%ul.list-unstyled.mr_source_commit
|
||||
|
||||
.col-lg-6
|
||||
|
|
@ -58,7 +59,8 @@
|
|||
= dropdown_content
|
||||
= dropdown_loading
|
||||
.card-footer
|
||||
.text-center= icon('spinner spin', class: "js-target-loading")
|
||||
.text-center
|
||||
.js-target-loading.mt-1.spinner.spinner-sm
|
||||
%ul.list-unstyled.mr_target_commit
|
||||
|
||||
- if @merge_request.errors.any?
|
||||
|
|
|
|||
|
|
@ -47,4 +47,5 @@
|
|||
= render 'projects/merge_requests/pipelines', endpoint: url_for(safe_params.merge(action: 'pipelines', format: :json)), disable_initialization: true
|
||||
|
||||
.mr-loading-status
|
||||
= spinner
|
||||
.loading.hide
|
||||
.spinner.spinner-md
|
||||
|
|
|
|||
|
|
@ -88,7 +88,8 @@
|
|||
show_whitespace_default: @show_whitespace_default.to_s }
|
||||
|
||||
.mr-loading-status
|
||||
= spinner
|
||||
.loading.hide
|
||||
.spinner.spinner-md
|
||||
|
||||
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees
|
||||
|
||||
|
|
|
|||
|
|
@ -17,9 +17,13 @@
|
|||
= icon('lightbulb-o')
|
||||
- if @page.persisted?
|
||||
= s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.")
|
||||
= link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'), target: '_blank'
|
||||
= link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'),
|
||||
target: '_blank', rel: 'noopener noreferrer'
|
||||
- else
|
||||
= s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.")
|
||||
= succeed '.' do
|
||||
= link_to _('Learn more'), help_page_path('user/project/wiki/index', anchor: 'creating-a-new-wiki-page'),
|
||||
target: '_blank', rel: 'noopener noreferrer'
|
||||
.form-group.row
|
||||
.col-sm-12= f.label :format, class: 'control-label-full-width'
|
||||
.col-sm-12
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replaced underscore with lodash for spec/javascripts/vue_shared/components
|
||||
merge_request: 25018
|
||||
author: Shubham Pandey
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Limit length of wiki file/directory names
|
||||
merge_request: 24364
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow running child pipelines as merge request pipelines
|
||||
merge_request: 23884
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix false matches of substitution-based quick actions in text
|
||||
merge_request: 24699
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -119,3 +119,7 @@ This limit can be configured for self hosted installations when [enabling
|
|||
Elasticsearch](../integration/elasticsearch.md#enabling-elasticsearch).
|
||||
|
||||
NOTE: **Note:** Set the limit to `0` to disable it.
|
||||
|
||||
## Wiki limits
|
||||
|
||||
- [Length restrictions for file and directory names](../user/project/wiki/index.md#length-restrictions-for-file-and-directory-names).
|
||||
|
|
|
|||
|
|
@ -65,10 +65,13 @@ maximum memory threshold (in bytes) for the Unicorn worker killer by
|
|||
setting the following values `/etc/gitlab/gitlab.rb`:
|
||||
|
||||
```ruby
|
||||
unicorn['worker_memory_limit_min'] = "400 * 1 << 20"
|
||||
unicorn['worker_memory_limit_max'] = "650 * 1 << 20"
|
||||
unicorn['worker_memory_limit_min'] = "1024 * 1 << 20"
|
||||
unicorn['worker_memory_limit_max'] = "1280 * 1 << 20"
|
||||
```
|
||||
|
||||
NOTE: **Note:**
|
||||
These values apply to GitLab 12.7.0 or newer versions. For older GitLab versions please consult [previous worker memory limits](https://gitlab.com/gitlab-org/omnibus-gitlab/-/blob/12.6.0+ee.0/files/gitlab-cookbooks/gitlab/attributes/default.rb#L422-423).
|
||||
|
||||
Otherwise, you can set the `GITLAB_UNICORN_MEMORY_MIN` and `GITLAB_UNICORN_MEMORY_MAX`
|
||||
[environment variables](../environment_variables.md).
|
||||
|
||||
|
|
|
|||
|
|
@ -425,10 +425,12 @@ It is also possible to use GraphQL outside of Vue by directly importing
|
|||
and using the default client with queries.
|
||||
|
||||
```javascript
|
||||
import defaultClient from '~/lib/graphql';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import query from './query.graphql';
|
||||
|
||||
defaultClient.query(query)
|
||||
const defaultClient = createDefaultClient();
|
||||
|
||||
defaultClient.query({ query })
|
||||
.then(result => console.log(result));
|
||||
```
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 491 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
|
|
@ -38,7 +38,7 @@ In addition to having a basic familiarity with [AWS](https://docs.aws.amazon.com
|
|||
|
||||
Below is a diagram of the recommended architecture.
|
||||
|
||||

|
||||

|
||||
|
||||
## AWS costs
|
||||
|
||||
|
|
@ -519,11 +519,34 @@ read the [repository storage paths docs](../../administration/repository_storage
|
|||
|
||||
### Setting up Gitaly
|
||||
|
||||
Gitaly is a service that provides high-level RPC access to Git repositories.
|
||||
It should be enabled and configured in a separate EC2 instance on the
|
||||
[private VPC](#subnets) we configured previously.
|
||||
CAUTION: **Caution:** In this architecture, having a single Gitaly server creates a single point of failure. This limitation will be removed once [Gitaly HA](https://gitlab.com/groups/gitlab-org/-/epics/842) is released.
|
||||
|
||||
Follow the [documentation to set up Gitaly](../../administration/gitaly/index.md).
|
||||
Gitaly is a service that provides high-level RPC access to Git repositories.
|
||||
It should be enabled and configured on a separate EC2 instance in one of the
|
||||
[private subnets](#subnets) we configured previously.
|
||||
|
||||
Let's create an EC2 instance where we'll install Gitaly:
|
||||
|
||||
1. From the EC2 dashboard, click **Launch instance**.
|
||||
1. Choose an AMI. In this example, we'll select the **Ubuntu Server 18.04 LTS (HVM), SSD Volume Type**.
|
||||
1. Choose an instance type. We'll pick a **c5.xlarge**.
|
||||
1. Click **Configure Instance Details**.
|
||||
1. In the **Network** dropdown, select `gitlab-vpc`, the VPC we created earlier.
|
||||
1. In the **Subnet** dropdown, select `gitlab-private-10.0.1.0` from the list of subnets we created earlier.
|
||||
1. Double check that **Auto-assign Public IP** is set to `Use subnet setting (Disable)`.
|
||||
1. Click **Add Storage**.
|
||||
1. Increase the Root volume size to `20 GiB` and change the **Volume Type** to `Provisoned IOPS SSD (io1)`. (This is an arbitrary size. Create a volume big enough for your repository storage requirements.)
|
||||
1. For **IOPS** set `1000` (20 GiB x 50 IOPS). You can provision up to 50 IOPS per GiB. If you select a larger volume, increase the IOPS accordingly. Workloads where many small files are written in a serialized manner, like `git`, requires performant storage, hence the choice of `Provisoned IOPS SSD (io1)`.
|
||||
1. Click on **Add Tags** and add your tags. In our case, we'll only set `Key: Name` and `Value: Gitaly`.
|
||||
1. Click on **Configure Security Group** and let's **Create a new security group**.
|
||||
1. Give your security group a name and description. We'll use `gitlab-gitaly-sec-group` for both.
|
||||
1. Create a **Custom TCP** rule and add port `8075` to the **Port Range**. For the **Source**, select the `gitlab-loadbalancer-sec-group`.
|
||||
1. Click **Review and launch** followed by **Launch** if you're happy with your settings.
|
||||
1. Finally, acknowledge that you have access to the selected private key file or create a new one. Click **Launch Instances**.
|
||||
|
||||
> **Optional:** Instead of storing configuration _and_ repository data on the root volume, you can also choose to add an additional EBS volume for repository storage. Follow the same guidance as above.
|
||||
|
||||
Now that we have our EC2 instance ready, follow the [documentation to install GitLab and set up Gitaly on its own server](../../administration/gitaly/index.md#running-gitaly-on-its-own-server).
|
||||
|
||||
### Using Amazon S3 object storage
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ When you're ready, click the **Create page** and the new page will be created.
|
|||
|
||||

|
||||
|
||||
### Attachment storage
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/33475) in GitLab 11.3.
|
||||
|
||||
Starting with GitLab 11.3, any file that is uploaded to the wiki via GitLab's
|
||||
|
|
@ -58,6 +60,22 @@ if you clone the wiki repository locally. All uploaded files prior to GitLab
|
|||
11.3 are stored in GitLab itself. If you want them to be part of the wiki's Git
|
||||
repository, you will have to upload them again.
|
||||
|
||||
### Length restrictions for file and directory names
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24364) in GitLab 12.8.
|
||||
|
||||
Many common file systems have a [limit of 255 bytes for file and directory names](https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits), and while Git and GitLab both support paths exceeding those limits, the presence of them makes it impossible for users on those file systems to checkout a wiki repository locally.
|
||||
|
||||
To avoid this situation, these limits are enforced when editing pages through the GitLab web interface and API:
|
||||
|
||||
- 245 bytes for page titles (reserving 10 bytes for the file extension).
|
||||
- 255 bytes for directory names.
|
||||
|
||||
Please note that:
|
||||
|
||||
- Non-ASCII characters take up more than one byte.
|
||||
- It's still possible to create files and directories exceeding those limits locally through Git, but this might break on other people's machines.
|
||||
|
||||
## Editing a wiki page
|
||||
|
||||
NOTE: **Note:**
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ module API
|
|||
requires :token, type: String, desc: 'Token to authenticate against Kubernetes'
|
||||
optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)'
|
||||
optional :namespace, type: String, desc: 'Unique namespace related to Group'
|
||||
optional :authorization_type, type: String, values: Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
|
||||
optional :authorization_type, type: String, values: ::Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
|
||||
end
|
||||
use :create_params_ee
|
||||
end
|
||||
|
|
@ -96,7 +96,7 @@ module API
|
|||
put ':id/clusters/:cluster_id' do
|
||||
authorize! :update_cluster, cluster
|
||||
|
||||
update_service = Clusters::UpdateService.new(current_user, update_cluster_params)
|
||||
update_service = ::Clusters::UpdateService.new(current_user, update_cluster_params)
|
||||
|
||||
if update_service.execute(cluster)
|
||||
present cluster, with: Entities::ClusterGroup
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ module API
|
|||
requires :token, type: String, desc: 'Token to authenticate against Kubernetes'
|
||||
optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)'
|
||||
optional :namespace, type: String, desc: 'Unique namespace related to Project'
|
||||
optional :authorization_type, type: String, values: Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
|
||||
optional :authorization_type, type: String, values: ::Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
|
||||
end
|
||||
use :create_params_ee
|
||||
end
|
||||
|
|
@ -100,7 +100,7 @@ module API
|
|||
put ':id/clusters/:cluster_id' do
|
||||
authorize! :update_cluster, cluster
|
||||
|
||||
update_service = Clusters::UpdateService.new(current_user, update_cluster_params)
|
||||
update_service = ::Clusters::UpdateService.new(current_user, update_cluster_params)
|
||||
|
||||
if update_service.execute(cluster)
|
||||
present cluster, with: Entities::ClusterProject
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@ module Gitlab
|
|||
end
|
||||
|
||||
def match(content)
|
||||
content.match %r{^/#{all_names.join('|')} ?(.*)$}
|
||||
content.match %r{^/#{all_names.join('|')}(?![\S]) ?(.*)$}
|
||||
end
|
||||
|
||||
def perform_substitution(context, content)
|
||||
return unless content
|
||||
|
||||
all_names.each do |a_name|
|
||||
content = content.gsub(%r{/#{a_name} ?(.*)$}i, execute_block(action_block, context, '\1'))
|
||||
content = content.gsub(%r{/#{a_name}(?![\S]) ?(.*)$}i, execute_block(action_block, context, '\1'))
|
||||
end
|
||||
|
||||
content
|
||||
|
|
|
|||
|
|
@ -472,9 +472,6 @@ msgstr ""
|
|||
msgid "%{total} open issues"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{unstaged} unstaged and %{staged} staged changes"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -733,9 +730,6 @@ msgstr ""
|
|||
msgid "<no scopes selected>"
|
||||
msgstr ""
|
||||
|
||||
msgid "<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes"
|
||||
msgstr ""
|
||||
|
||||
msgid "<strong>%{group_name}</strong> group members"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -1676,9 +1670,6 @@ msgstr ""
|
|||
msgid "An error occurred fetching the dropdown data."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred loading code navigation"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred previewing the blob"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -5071,6 +5062,9 @@ msgstr ""
|
|||
msgid "Container Scanning"
|
||||
msgstr ""
|
||||
|
||||
msgid "Container does not exist"
|
||||
msgstr ""
|
||||
|
||||
msgid "Container registry images"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -5080,6 +5074,9 @@ msgstr ""
|
|||
msgid "Container repositories sync capacity"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|%{imageName} tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -5131,6 +5128,9 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Last Updated"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Missing or insufficient permission, delete button disabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Number of tags to retain:"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -5196,12 +5196,21 @@ msgstr ""
|
|||
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|You are about to remove %{item} tags. Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|You are about to remove %{item}. Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|You are about to remove <b>%{count}</b> tags. Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|You are about to remove <b>%{title}</b>. Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|You are about to remove repository %{title}. Once you confirm, this repository will be permanently deleted."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|You are about to remove repository <b>%{title}</b>. Once you confirm, this repository will be permanently deleted."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -12674,6 +12683,9 @@ msgstr ""
|
|||
msgid "No connection could be made to a Gitaly Server, please check your logs!"
|
||||
msgstr ""
|
||||
|
||||
msgid "No containers available"
|
||||
msgstr ""
|
||||
|
||||
msgid "No contributions"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -14020,6 +14032,9 @@ msgstr ""
|
|||
msgid "Please wait while we import the repository for you. Refresh at will."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pod does not exist"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pod logs"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -20425,6 +20440,9 @@ msgstr ""
|
|||
msgid "Unable to collect memory info"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unable to connect to Elasticsearch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unable to connect to Prometheus server"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -20494,6 +20512,9 @@ msgstr ""
|
|||
msgid "Unknown Error"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unknown cache key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unknown encryption strategy: %{encrypted_strategy}!"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -22766,6 +22787,12 @@ msgstr ""
|
|||
msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
|
||||
msgstr ""
|
||||
|
||||
msgid "exceeds the limit of %{bytes} bytes for directory names"
|
||||
msgstr ""
|
||||
|
||||
msgid "exceeds the limit of %{bytes} bytes for page titles"
|
||||
msgstr ""
|
||||
|
||||
msgid "expired on %{milestone_due_date}"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -2,35 +2,34 @@
|
|||
|
||||
module QA
|
||||
context 'Plan' do
|
||||
describe 'Close issue' do
|
||||
include Support::Api
|
||||
|
||||
describe 'Issue' do
|
||||
let(:issue) do
|
||||
Resource::Issue.fabricate_via_api!
|
||||
end
|
||||
|
||||
let(:issue_id) { issue.api_response[:iid] }
|
||||
|
||||
before do
|
||||
Flow::Login.sign_in
|
||||
let(:api_client) { Runtime::API::Client.new(:gitlab) }
|
||||
|
||||
before do
|
||||
# Initial commit should be pushed because
|
||||
# the very first commit to the project doesn't close the issue
|
||||
# https://gitlab.com/gitlab-org/gitlab-foss/issues/38965
|
||||
push_commit('Initial commit')
|
||||
end
|
||||
|
||||
it 'closes an issue by pushing a commit' do
|
||||
it 'closes via pushing a commit' do
|
||||
push_commit("Closes ##{issue_id}", false)
|
||||
|
||||
issue.visit!
|
||||
|
||||
Page::Project::Issue::Show.perform do |show|
|
||||
reopen_issue_button_visible = show.wait_until(reload: true) do
|
||||
show.has_element?(:reopen_issue_button, wait: 1.0)
|
||||
end
|
||||
expect(reopen_issue_button_visible).to be_truthy
|
||||
Support::Retrier.retry_until(max_duration: 10, sleep_interval: 1) do
|
||||
issue_closed?
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def push_commit(commit_message, new_branch = true)
|
||||
Resource::Repository::ProjectPush.fabricate! do |push|
|
||||
push.commit_message = commit_message
|
||||
|
|
@ -39,6 +38,11 @@ module QA
|
|||
push.project = issue.project
|
||||
end
|
||||
end
|
||||
|
||||
def issue_closed?
|
||||
response = get Runtime::API::Request.new(api_client, "/projects/#{issue.project.id}/issues/#{issue_id}").url
|
||||
parse_body(response)[:state] == 'closed'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
context 'Plan', :smoke, :reliable do
|
||||
context 'Plan', :smoke do
|
||||
describe 'Issue creation' do
|
||||
before do
|
||||
Flow::Login.sign_in
|
||||
end
|
||||
|
||||
it 'creates an issue' do
|
||||
it 'creates an issue', :reliable do
|
||||
issue = Resource::Issue.fabricate_via_browser_ui!
|
||||
|
||||
Page::Project::Menu.perform(&:click_issues)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import testAction from 'helpers/vuex_action_helper';
|
||||
import actions from '~/code_navigation/store/actions';
|
||||
import createFlash from '~/flash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { setCurrentHoverElement, addInteractionClass } from '~/code_navigation/utils';
|
||||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/code_navigation/utils');
|
||||
|
||||
describe('Code navigation actions', () => {
|
||||
|
|
@ -25,13 +23,6 @@ describe('Code navigation actions', () => {
|
|||
describe('requestDataError', () => {
|
||||
it('commits REQUEST_DATA_ERROR', () =>
|
||||
testAction(actions.requestDataError, null, {}, [{ type: 'REQUEST_DATA_ERROR' }], []));
|
||||
|
||||
it('creates a flash message', () =>
|
||||
testAction(actions.requestDataError, null, {}, [{ type: 'REQUEST_DATA_ERROR' }], []).then(
|
||||
() => {
|
||||
expect(createFlash).toHaveBeenCalled();
|
||||
},
|
||||
));
|
||||
});
|
||||
|
||||
describe('fetchData', () => {
|
||||
|
|
|
|||
|
|
@ -534,27 +534,21 @@ describe('IDE store file actions', () => {
|
|||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('adds a newline to the end of the file if it doesnt already exist', done => {
|
||||
callAction('content')
|
||||
it('adds file into stagedFiles array', done => {
|
||||
store
|
||||
.dispatch('changeFileContent', {
|
||||
path: tmpFile.path,
|
||||
content: 'content',
|
||||
})
|
||||
.then(() => {
|
||||
expect(tmpFile.content).toBe('content\n');
|
||||
expect(store.state.stagedFiles.length).toBe(1);
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('adds file into changedFiles array', done => {
|
||||
callAction()
|
||||
.then(() => {
|
||||
expect(store.state.changedFiles.length).toBe(1);
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('adds file not more than once into changedFiles array', done => {
|
||||
it('adds file not more than once into stagedFiles array', done => {
|
||||
store
|
||||
.dispatch('changeFileContent', {
|
||||
path: tmpFile.path,
|
||||
|
|
@ -567,7 +561,7 @@ describe('IDE store file actions', () => {
|
|||
}),
|
||||
)
|
||||
.then(() => {
|
||||
expect(store.state.changedFiles.length).toBe(1);
|
||||
expect(store.state.stagedFiles.length).toBe(1);
|
||||
|
||||
done();
|
||||
})
|
||||
|
|
@ -594,52 +588,6 @@ describe('IDE store file actions', () => {
|
|||
.catch(done.fail);
|
||||
});
|
||||
|
||||
describe('when `gon.feature.stageAllByDefault` is true', () => {
|
||||
const originalGonFeatures = Object.assign({}, gon.features);
|
||||
|
||||
beforeAll(() => {
|
||||
gon.features = { stageAllByDefault: true };
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
gon.features = originalGonFeatures;
|
||||
});
|
||||
|
||||
it('adds file into stagedFiles array', done => {
|
||||
store
|
||||
.dispatch('changeFileContent', {
|
||||
path: tmpFile.path,
|
||||
content: 'content',
|
||||
})
|
||||
.then(() => {
|
||||
expect(store.state.stagedFiles.length).toBe(1);
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('adds file not more than once into stagedFiles array', done => {
|
||||
store
|
||||
.dispatch('changeFileContent', {
|
||||
path: tmpFile.path,
|
||||
content: 'content',
|
||||
})
|
||||
.then(() =>
|
||||
store.dispatch('changeFileContent', {
|
||||
path: tmpFile.path,
|
||||
content: 'content 123',
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
expect(store.state.stagedFiles.length).toBe(1);
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
it('bursts unused seal', done => {
|
||||
store
|
||||
.dispatch('changeFileContent', {
|
||||
|
|
|
|||
|
|
@ -61,19 +61,14 @@ describe('IDE store integration', () => {
|
|||
store.dispatch('createTempEntry', { name: TEST_PATH, type: 'blob' });
|
||||
});
|
||||
|
||||
it('has changed and staged', () => {
|
||||
expect(store.state.changedFiles).toEqual([
|
||||
expect.objectContaining({
|
||||
path: TEST_PATH,
|
||||
tempFile: true,
|
||||
deleted: false,
|
||||
}),
|
||||
]);
|
||||
|
||||
it('is added to staged as modified', () => {
|
||||
expect(store.state.stagedFiles).toEqual([
|
||||
expect.objectContaining({
|
||||
path: TEST_PATH,
|
||||
deleted: true,
|
||||
deleted: false,
|
||||
staged: true,
|
||||
changed: true,
|
||||
tempFile: false,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Registry Group Empty state to match the default snapshot 1`] = `
|
||||
<div
|
||||
class="container-message"
|
||||
svg-path="foo"
|
||||
title="There are no container images available in this group"
|
||||
>
|
||||
<p
|
||||
class="js-no-container-images-text"
|
||||
>
|
||||
With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here.
|
||||
<gl-link-stub
|
||||
href="baz"
|
||||
target="_blank"
|
||||
>
|
||||
More Information
|
||||
</gl-link-stub>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Registry Project Empty state to match the default snapshot 1`] = `
|
||||
<div
|
||||
class="container-message"
|
||||
svg-path="bazFoo"
|
||||
title="There are no container images stored for this project"
|
||||
>
|
||||
<p
|
||||
class="js-no-container-images-text"
|
||||
>
|
||||
With the Container Registry, every project can have its own space to store its Docker images.
|
||||
<gl-link-stub
|
||||
href="baz"
|
||||
target="_blank"
|
||||
>
|
||||
More Information
|
||||
</gl-link-stub>
|
||||
</p>
|
||||
|
||||
<h5>
|
||||
Quick Start
|
||||
</h5>
|
||||
|
||||
<p
|
||||
class="js-not-logged-in-to-registry-text"
|
||||
>
|
||||
If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have
|
||||
<gl-link-stub
|
||||
href="barBaz"
|
||||
target="_blank"
|
||||
>
|
||||
Two-Factor Authentication
|
||||
</gl-link-stub>
|
||||
enabled, use a
|
||||
<gl-link-stub
|
||||
href="fooBaz"
|
||||
target="_blank"
|
||||
>
|
||||
Personal Access Token
|
||||
</gl-link-stub>
|
||||
instead of a password.
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="input-group append-bottom-10"
|
||||
>
|
||||
<input
|
||||
class="form-control monospace"
|
||||
readonly="readonly"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="input-group-append"
|
||||
>
|
||||
<clipboard-button-stub
|
||||
class="input-group-text"
|
||||
cssclass="btn-default"
|
||||
text="docker login bar"
|
||||
title="Copy login command"
|
||||
tooltipplacement="top"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p />
|
||||
|
||||
<p>
|
||||
|
||||
You can add an image to this registry with the following commands:
|
||||
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="input-group append-bottom-10"
|
||||
>
|
||||
<input
|
||||
class="form-control monospace"
|
||||
readonly="readonly"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="input-group-append"
|
||||
>
|
||||
<clipboard-button-stub
|
||||
class="input-group-text"
|
||||
cssclass="btn-default"
|
||||
text="docker build -t foo ."
|
||||
title="Copy build command"
|
||||
tooltipplacement="top"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="input-group"
|
||||
>
|
||||
<input
|
||||
class="form-control monospace"
|
||||
readonly="readonly"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="input-group-append"
|
||||
>
|
||||
<clipboard-button-stub
|
||||
class="input-group-text"
|
||||
cssclass="btn-default"
|
||||
text="docker push foo"
|
||||
title="Copy push command"
|
||||
tooltipplacement="top"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import { GlSprintf } from '@gitlab/ui';
|
||||
import { GlEmptyState } from '../stubs';
|
||||
import groupEmptyState from '~/registry/explorer/components/group_empty_state.vue';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('Registry Group Empty state', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new Vuex.Store({
|
||||
state: {
|
||||
config: {
|
||||
noContainersImage: 'foo',
|
||||
helpPagePath: 'baz',
|
||||
},
|
||||
},
|
||||
});
|
||||
wrapper = shallowMount(groupEmptyState, {
|
||||
localVue,
|
||||
store,
|
||||
stubs: {
|
||||
GlEmptyState,
|
||||
GlSprintf,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('to match the default snapshot', () => {
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import { GlSprintf } from '@gitlab/ui';
|
||||
import { GlEmptyState } from '../stubs';
|
||||
import projectEmptyState from '~/registry/explorer/components/project_empty_state.vue';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('Registry Project Empty state', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new Vuex.Store({
|
||||
state: {
|
||||
config: {
|
||||
repositoryUrl: 'foo',
|
||||
registryHostUrlWithPort: 'bar',
|
||||
helpPagePath: 'baz',
|
||||
twoFactorAuthHelpLink: 'barBaz',
|
||||
personalAccessTokensHelpLink: 'fooBaz',
|
||||
noContainersImage: 'bazFoo',
|
||||
},
|
||||
},
|
||||
});
|
||||
wrapper = shallowMount(projectEmptyState, {
|
||||
localVue,
|
||||
store,
|
||||
stubs: {
|
||||
GlEmptyState,
|
||||
GlSprintf,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('to match the default snapshot', () => {
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,3 +1,11 @@
|
|||
export const headers = {
|
||||
'X-PER-PAGE': 5,
|
||||
'X-PAGE': 1,
|
||||
'X-TOTAL': 13,
|
||||
'X-TOTAL_PAGES': 1,
|
||||
'X-NEXT-PAGE': null,
|
||||
'X-PREVIOUS-PAGE': null,
|
||||
};
|
||||
export const reposServerResponse = [
|
||||
{
|
||||
destroy_path: 'path',
|
||||
|
|
@ -36,3 +44,46 @@ export const registryServerResponse = [
|
|||
created_at: 1505828744434,
|
||||
},
|
||||
];
|
||||
|
||||
export const imagesListResponse = {
|
||||
data: [
|
||||
{
|
||||
path: 'foo',
|
||||
location: 'location',
|
||||
destroy_path: 'path',
|
||||
},
|
||||
{
|
||||
path: 'bar',
|
||||
location: 'location-2',
|
||||
destroy_path: 'path-2',
|
||||
},
|
||||
],
|
||||
headers,
|
||||
};
|
||||
|
||||
export const tagsListResponse = {
|
||||
data: [
|
||||
{
|
||||
tag: 'centos6',
|
||||
revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
|
||||
short_revision: 'b118ab5b0',
|
||||
size: 19,
|
||||
layers: 10,
|
||||
location: 'location',
|
||||
path: 'bar',
|
||||
created_at: 1505828744434,
|
||||
destroy_path: 'path',
|
||||
},
|
||||
{
|
||||
tag: 'test-image',
|
||||
revision: 'b969de599faea2b3d9b6605a8b0897261c571acaa36db1bdc7349b5775b4e0b4',
|
||||
short_revision: 'b969de599',
|
||||
size: 19,
|
||||
layers: 10,
|
||||
path: 'foo',
|
||||
location: 'location-2',
|
||||
created_at: 1505828744434,
|
||||
},
|
||||
],
|
||||
headers,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,293 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { GlTable, GlPagination, GlLoadingIcon } from '@gitlab/ui';
|
||||
import Tracking from '~/tracking';
|
||||
import stubChildren from 'helpers/stub_children';
|
||||
import component from '~/registry/explorer/pages/details.vue';
|
||||
import store from '~/registry/explorer/stores/';
|
||||
import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/';
|
||||
import { tagsListResponse } from '../mock_data';
|
||||
import { GlModal } from '../stubs';
|
||||
|
||||
describe('Details Page', () => {
|
||||
let wrapper;
|
||||
let dispatchSpy;
|
||||
|
||||
const findDeleteModal = () => wrapper.find(GlModal);
|
||||
const findPagination = () => wrapper.find(GlPagination);
|
||||
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||
const findTagsTable = () => wrapper.find(GlTable);
|
||||
const findMainCheckbox = () => wrapper.find({ ref: 'mainCheckbox' });
|
||||
const findFirstRowItem = ref => wrapper.find({ ref });
|
||||
const findBulkDeleteButton = () => wrapper.find({ ref: 'bulkDeleteButton' });
|
||||
// findAll and refs seems to no work falling back to class
|
||||
const findAllDeleteButtons = () => wrapper.findAll('.js-delete-registry');
|
||||
const findAllCheckboxes = () => wrapper.findAll('.js-row-checkbox');
|
||||
const findCheckedCheckboxes = () => findAllCheckboxes().filter(c => c.attributes('checked'));
|
||||
|
||||
const routeId = window.btoa(JSON.stringify({ name: 'foo', tags_path: 'bar' }));
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mount(component, {
|
||||
store,
|
||||
stubs: {
|
||||
...stubChildren(component),
|
||||
GlModal,
|
||||
GlSprintf: false,
|
||||
GlTable: false,
|
||||
},
|
||||
mocks: {
|
||||
$route: {
|
||||
params: {
|
||||
id: routeId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
dispatchSpy = jest.spyOn(store, 'dispatch');
|
||||
store.dispatch('receiveTagsListSuccess', tagsListResponse);
|
||||
jest.spyOn(Tracking, 'event');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('when isLoading is true', () => {
|
||||
beforeAll(() => store.commit(SET_MAIN_LOADING, true));
|
||||
|
||||
afterAll(() => store.commit(SET_MAIN_LOADING, false));
|
||||
|
||||
it('has a loading icon', () => {
|
||||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not have a main content', () => {
|
||||
expect(findTagsTable().exists()).toBe(false);
|
||||
expect(findPagination().exists()).toBe(false);
|
||||
expect(findDeleteModal().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('table', () => {
|
||||
it.each([
|
||||
'rowCheckbox',
|
||||
'rowName',
|
||||
'rowShortRevision',
|
||||
'rowSize',
|
||||
'rowTime',
|
||||
'singleDeleteButton',
|
||||
])('%s exist in the table', element => {
|
||||
expect(findFirstRowItem(element).exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('header checkbox', () => {
|
||||
it('exists', () => {
|
||||
expect(findMainCheckbox().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('if selected set selectedItem and allSelected', () => {
|
||||
findMainCheckbox().vm.$emit('change');
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findMainCheckbox().attributes('checked')).toBeTruthy();
|
||||
expect(findCheckedCheckboxes()).toHaveLength(store.state.tags.length);
|
||||
});
|
||||
});
|
||||
|
||||
it('if deselect unset selectedItem and allSelected', () => {
|
||||
wrapper.setData({ selectedItems: [1, 2], selectAllChecked: true });
|
||||
findMainCheckbox().vm.$emit('change');
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findMainCheckbox().attributes('checked')).toBe(undefined);
|
||||
expect(findCheckedCheckboxes()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('row checkbox', () => {
|
||||
it('if selected adds item to selectedItems', () => {
|
||||
findFirstRowItem('rowCheckbox').vm.$emit('change');
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.selectedItems).toEqual([1]);
|
||||
expect(findFirstRowItem('rowCheckbox').attributes('checked')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('if deselect remove index from selectedItems', () => {
|
||||
wrapper.setData({ selectedItems: [1] });
|
||||
findFirstRowItem('rowCheckbox').vm.$emit('change');
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.selectedItems.length).toBe(0);
|
||||
expect(findFirstRowItem('rowCheckbox').attributes('checked')).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('header delete button', () => {
|
||||
it('exists', () => {
|
||||
expect(findBulkDeleteButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('is disabled if no item is selected', () => {
|
||||
expect(findBulkDeleteButton().attributes('disabled')).toBe('true');
|
||||
});
|
||||
|
||||
it('is enabled if at least one item is selected', () => {
|
||||
wrapper.setData({ selectedItems: [1] });
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findBulkDeleteButton().attributes('disabled')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('on click', () => {
|
||||
it('when one item is selected', () => {
|
||||
wrapper.setData({ selectedItems: [1] });
|
||||
findBulkDeleteButton().vm.$emit('click');
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findDeleteModal().html()).toContain(
|
||||
'You are about to remove <b>foo</b>. Are you sure?',
|
||||
);
|
||||
expect(GlModal.methods.show).toHaveBeenCalled();
|
||||
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
|
||||
label: 'registry_tag_delete',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('when multiple items are selected', () => {
|
||||
wrapper.setData({ selectedItems: [0, 1] });
|
||||
findBulkDeleteButton().vm.$emit('click');
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findDeleteModal().html()).toContain(
|
||||
'You are about to remove <b>2</b> tags. Are you sure?',
|
||||
);
|
||||
expect(GlModal.methods.show).toHaveBeenCalled();
|
||||
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
|
||||
label: 'bulk_registry_tag_delete',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('row delete button', () => {
|
||||
it('exists', () => {
|
||||
expect(
|
||||
findAllDeleteButtons()
|
||||
.at(0)
|
||||
.exists(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('is disabled if the item has no destroy_path', () => {
|
||||
expect(
|
||||
findAllDeleteButtons()
|
||||
.at(1)
|
||||
.attributes('disabled'),
|
||||
).toBe('true');
|
||||
});
|
||||
|
||||
it('on click', () => {
|
||||
findAllDeleteButtons()
|
||||
.at(0)
|
||||
.vm.$emit('click');
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findDeleteModal().html()).toContain(
|
||||
'You are about to remove <b>bar</b>. Are you sure?',
|
||||
);
|
||||
expect(GlModal.methods.show).toHaveBeenCalled();
|
||||
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
|
||||
label: 'registry_tag_delete',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
it('exists', () => {
|
||||
expect(findPagination().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('is wired to the correct pagination props', () => {
|
||||
const pagination = findPagination();
|
||||
expect(pagination.props('perPage')).toBe(store.state.tagsPagination.perPage);
|
||||
expect(pagination.props('totalItems')).toBe(store.state.tagsPagination.total);
|
||||
expect(pagination.props('value')).toBe(store.state.tagsPagination.page);
|
||||
});
|
||||
|
||||
it('fetch the data from the API when the v-model changes', () => {
|
||||
dispatchSpy.mockResolvedValue();
|
||||
wrapper.setData({ currentPage: 2 });
|
||||
expect(store.dispatch).toHaveBeenCalledWith('requestTagsList', {
|
||||
id: wrapper.vm.$route.params.id,
|
||||
pagination: { page: 2 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('modal', () => {
|
||||
it('exists', () => {
|
||||
expect(findDeleteModal().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when ok event is emitted', () => {
|
||||
beforeEach(() => {
|
||||
dispatchSpy.mockResolvedValue();
|
||||
});
|
||||
|
||||
it('tracks confirm_delete', () => {
|
||||
const deleteModal = findDeleteModal();
|
||||
deleteModal.vm.$emit('ok');
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'confirm_delete', {
|
||||
label: 'registry_tag_delete',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('when only one element is selected', () => {
|
||||
const deleteModal = findDeleteModal();
|
||||
|
||||
wrapper.setData({ itemsToBeDeleted: [0] });
|
||||
deleteModal.vm.$emit('ok');
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTag', {
|
||||
tag: store.state.tags[0],
|
||||
imageId: wrapper.vm.$route.params.id,
|
||||
});
|
||||
// itemsToBeDeleted is not represented in the DOM, is used as parking variable between selected and deleted items
|
||||
expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
|
||||
expect(findCheckedCheckboxes()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('when multiple elements are selected', () => {
|
||||
const deleteModal = findDeleteModal();
|
||||
|
||||
wrapper.setData({ itemsToBeDeleted: [0, 1] });
|
||||
deleteModal.vm.$emit('ok');
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith('requestDeleteTags', {
|
||||
ids: store.state.tags.map(t => t.name),
|
||||
imageId: wrapper.vm.$route.params.id,
|
||||
});
|
||||
// itemsToBeDeleted is not represented in the DOM, is used as parking variable between selected and deleted items
|
||||
expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
|
||||
expect(findCheckedCheckboxes()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('tracks cancel_delete when cancel event is emitted', () => {
|
||||
const deleteModal = findDeleteModal();
|
||||
deleteModal.vm.$emit('cancel');
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', {
|
||||
label: 'registry_tag_delete',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
import VueRouter from 'vue-router';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import { GlPagination, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
|
||||
import Tracking from '~/tracking';
|
||||
import component from '~/registry/explorer/pages/list.vue';
|
||||
import store from '~/registry/explorer/stores/';
|
||||
import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/';
|
||||
import { imagesListResponse } from '../mock_data';
|
||||
import { GlModal, GlEmptyState } from '../stubs';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueRouter);
|
||||
|
||||
describe('List Page', () => {
|
||||
let wrapper;
|
||||
let dispatchSpy;
|
||||
|
||||
const findDeleteBtn = () => wrapper.find({ ref: 'deleteImageButton' });
|
||||
const findDeleteModal = () => wrapper.find(GlModal);
|
||||
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||
const findImagesList = () => wrapper.find({ ref: 'imagesList' });
|
||||
const findRowItems = () => wrapper.findAll({ ref: 'rowItem' });
|
||||
const findEmptyState = () => wrapper.find(GlEmptyState);
|
||||
const findDetailsLink = () => wrapper.find({ ref: 'detailsLink' });
|
||||
const findClipboardButton = () => wrapper.find({ ref: 'clipboardButton' });
|
||||
const findPagination = () => wrapper.find(GlPagination);
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(component, {
|
||||
localVue,
|
||||
store,
|
||||
stubs: {
|
||||
GlModal,
|
||||
GlEmptyState,
|
||||
GlSprintf,
|
||||
},
|
||||
});
|
||||
dispatchSpy = jest.spyOn(store, 'dispatch');
|
||||
store.dispatch('receiveImagesListSuccess', imagesListResponse);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('connection error', () => {
|
||||
const config = {
|
||||
characterError: true,
|
||||
containersErrorImage: 'foo',
|
||||
helpPagePath: 'bar',
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
store.dispatch('setInitialState', config);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
store.dispatch('setInitialState', {});
|
||||
});
|
||||
|
||||
it('should show an empty state', () => {
|
||||
expect(findEmptyState().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('empty state should have an svg-path', () => {
|
||||
expect(findEmptyState().attributes('svg-path')).toBe(config.containersErrorImage);
|
||||
});
|
||||
|
||||
it('empty state should have a description', () => {
|
||||
expect(findEmptyState().html()).toContain('connection error');
|
||||
});
|
||||
|
||||
it('should not show the loading or default state', () => {
|
||||
expect(findLoadingIcon().exists()).toBe(false);
|
||||
expect(findImagesList().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when isLoading is true', () => {
|
||||
beforeAll(() => store.commit(SET_MAIN_LOADING, true));
|
||||
|
||||
afterAll(() => store.commit(SET_MAIN_LOADING, false));
|
||||
|
||||
it('shows the loading icon', () => {
|
||||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('imagesList is not visible', () => {
|
||||
expect(findImagesList().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('list', () => {
|
||||
describe('listElement', () => {
|
||||
let listElements;
|
||||
let firstElement;
|
||||
|
||||
beforeEach(() => {
|
||||
listElements = findRowItems();
|
||||
[firstElement] = store.state.images;
|
||||
});
|
||||
|
||||
it('contains one list element for each image', () => {
|
||||
expect(listElements.length).toBe(store.state.images.length);
|
||||
});
|
||||
|
||||
it('contains a link to the details page', () => {
|
||||
const link = findDetailsLink();
|
||||
expect(link.html()).toContain(firstElement.path);
|
||||
expect(link.props('to').name).toBe('details');
|
||||
});
|
||||
|
||||
it('contains a clipboard button', () => {
|
||||
const button = findClipboardButton();
|
||||
expect(button.exists()).toBe(true);
|
||||
expect(button.props('text')).toBe(firstElement.location);
|
||||
expect(button.props('title')).toBe(firstElement.location);
|
||||
});
|
||||
|
||||
describe('delete image', () => {
|
||||
it('should be possible to delete a repo', () => {
|
||||
const deleteBtn = findDeleteBtn();
|
||||
expect(deleteBtn.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should call deleteItem when confirming deletion', () => {
|
||||
dispatchSpy.mockResolvedValue();
|
||||
const itemToDelete = wrapper.vm.images[0];
|
||||
wrapper.setData({ itemToDelete });
|
||||
findDeleteModal().vm.$emit('ok');
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
'requestDeleteImage',
|
||||
itemToDelete.destroy_path,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
it('exists', () => {
|
||||
expect(findPagination().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('is wired to the correct pagination props', () => {
|
||||
const pagination = findPagination();
|
||||
expect(pagination.props('perPage')).toBe(store.state.pagination.perPage);
|
||||
expect(pagination.props('totalItems')).toBe(store.state.pagination.total);
|
||||
expect(pagination.props('value')).toBe(store.state.pagination.page);
|
||||
});
|
||||
|
||||
it('fetch the data from the API when the v-model changes', () => {
|
||||
dispatchSpy.mockReturnValue();
|
||||
wrapper.setData({ currentPage: 2 });
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith('requestImagesList', { page: 2 });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('modal', () => {
|
||||
it('exists', () => {
|
||||
expect(findDeleteModal().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('contains a description with the path of the item to delete', () => {
|
||||
wrapper.setData({ itemToDelete: { path: 'foo' } });
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findDeleteModal().html()).toContain('foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tracking', () => {
|
||||
const testTrackingCall = action => {
|
||||
expect(Tracking.event).toHaveBeenCalledWith(undefined, action, {
|
||||
label: 'registry_repository_delete',
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Tracking, 'event');
|
||||
dispatchSpy.mockReturnValue();
|
||||
});
|
||||
|
||||
it('send an event when delete button is clicked', () => {
|
||||
const deleteBtn = findDeleteBtn();
|
||||
deleteBtn.vm.$emit('click');
|
||||
testTrackingCall('click_button');
|
||||
});
|
||||
it('send an event when cancel is pressed on modal', () => {
|
||||
const deleteModal = findDeleteModal();
|
||||
deleteModal.vm.$emit('cancel');
|
||||
testTrackingCall('cancel_delete');
|
||||
});
|
||||
it('send an event when confirm is clicked on modal', () => {
|
||||
dispatchSpy.mockReturnValue();
|
||||
const deleteModal = findDeleteModal();
|
||||
deleteModal.vm.$emit('ok');
|
||||
testTrackingCall('confirm_delete');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -120,14 +120,15 @@ describe('Actions RegistryExplorer Store', () => {
|
|||
});
|
||||
|
||||
describe('fetch tags list', () => {
|
||||
const url = window.btoa(`${endpoint}/1}`);
|
||||
const url = `${endpoint}/1}`;
|
||||
const path = window.btoa(JSON.stringify({ tags_path: `${endpoint}/1}` }));
|
||||
|
||||
it('sets the tagsList', done => {
|
||||
mock.onGet(window.atob(url)).replyOnce(200, registryServerResponse, {});
|
||||
mock.onGet(url).replyOnce(200, registryServerResponse, {});
|
||||
|
||||
testAction(
|
||||
actions.requestTagsList,
|
||||
{ id: url },
|
||||
{ id: path },
|
||||
{},
|
||||
[
|
||||
{ type: types.SET_MAIN_LOADING, payload: true },
|
||||
|
|
@ -146,7 +147,7 @@ describe('Actions RegistryExplorer Store', () => {
|
|||
it('should create flash on error', done => {
|
||||
testAction(
|
||||
actions.requestTagsList,
|
||||
{ id: url },
|
||||
{ id: path },
|
||||
{},
|
||||
[
|
||||
{ type: types.SET_MAIN_LOADING, payload: true },
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@ describe('Mutations Registry Explorer Store', () => {
|
|||
|
||||
describe('SET_INITIAL_STATE', () => {
|
||||
it('should set the initial state', () => {
|
||||
const expectedState = { ...mockState, config: { endpoint: 'foo' } };
|
||||
mutations[types.SET_INITIAL_STATE](mockState, { endpoint: 'foo' });
|
||||
const payload = { endpoint: 'foo', isGroupPage: true };
|
||||
const expectedState = { ...mockState, config: payload };
|
||||
mutations[types.SET_INITIAL_STATE](mockState, payload);
|
||||
|
||||
expect(mockState).toEqual(expectedState);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
export const GlModal = {
|
||||
template: '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-ok"></slot></div>',
|
||||
methods: {
|
||||
show: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export const GlEmptyState = {
|
||||
template: '<div><slot name="description"></slot></div>',
|
||||
name: 'GlEmptyStateSTub',
|
||||
};
|
||||
|
|
@ -40,7 +40,7 @@ describe('Flash', () => {
|
|||
|
||||
expect(el.style['transition-property']).toBe('opacity');
|
||||
|
||||
expect(el.style['transition-duration']).toBe('0.3s');
|
||||
expect(el.style['transition-duration']).toBe('0.15s');
|
||||
});
|
||||
|
||||
it('sets opacity style', () => {
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ describe('IDE commit form', () => {
|
|||
vm.$store.state.stagedFiles.push('test');
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('p').textContent).toContain('1 unstaged and 1 staged changes');
|
||||
expect(vm.$el.querySelector('p').textContent).toContain('1 staged and 1 unstaged changes');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ describe('IDE extra file row component', () => {
|
|||
stagedFilesCount = 1;
|
||||
unstagedFilesCount = 1;
|
||||
|
||||
expect(vm.folderChangesTooltip).toBe('1 unstaged and 1 staged changes');
|
||||
expect(vm.folderChangesTooltip).toBe('1 staged and 1 unstaged changes');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -225,35 +225,6 @@ describe('Multi-file store actions', () => {
|
|||
.catch(done.fail);
|
||||
});
|
||||
|
||||
describe('when `gon.feature.stageAllByDefault` is true', () => {
|
||||
const originalGonFeatures = Object.assign({}, gon.features);
|
||||
|
||||
beforeAll(() => {
|
||||
gon.features = { stageAllByDefault: true };
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
gon.features = originalGonFeatures;
|
||||
});
|
||||
|
||||
it('adds tmp file to staged files', done => {
|
||||
const name = 'test';
|
||||
|
||||
store
|
||||
.dispatch('createTempEntry', {
|
||||
name,
|
||||
branchId: 'mybranch',
|
||||
type: 'blob',
|
||||
})
|
||||
.then(() => {
|
||||
expect(store.state.stagedFiles).toEqual([jasmine.objectContaining({ name })]);
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
it('adds tmp file to open files', done => {
|
||||
const name = 'test';
|
||||
|
||||
|
|
@ -274,7 +245,7 @@ describe('Multi-file store actions', () => {
|
|||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('adds tmp file to changed files', done => {
|
||||
it('adds tmp file to staged files', done => {
|
||||
const name = 'test';
|
||||
|
||||
store
|
||||
|
|
@ -284,9 +255,7 @@ describe('Multi-file store actions', () => {
|
|||
type: 'blob',
|
||||
})
|
||||
.then(() => {
|
||||
expect(store.state.changedFiles).toEqual([
|
||||
jasmine.objectContaining({ name, tempFile: true }),
|
||||
]);
|
||||
expect(store.state.stagedFiles).toEqual([jasmine.objectContaining({ name })]);
|
||||
|
||||
done();
|
||||
})
|
||||
|
|
@ -294,15 +263,9 @@ describe('Multi-file store actions', () => {
|
|||
});
|
||||
|
||||
it('sets tmp file as active', () => {
|
||||
const dispatch = jasmine.createSpy();
|
||||
const commit = jasmine.createSpy();
|
||||
createTempEntry(store, { name: 'test', branchId: 'mybranch', type: 'blob' });
|
||||
|
||||
createTempEntry(
|
||||
{ state: store.state, getters: store.getters, dispatch, commit },
|
||||
{ name: 'test', branchId: 'mybranch', type: 'blob' },
|
||||
);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith('setFileActive', 'test');
|
||||
expect(store.dispatch).toHaveBeenCalledWith('setFileActive', 'test');
|
||||
});
|
||||
|
||||
it('creates flash message if file already exists', done => {
|
||||
|
|
@ -804,55 +767,19 @@ describe('Multi-file store actions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when `gon.feature.stageAllByDefault` is true', () => {
|
||||
const originalGonFeatures = Object.assign({}, gon.features);
|
||||
it('by default renames an entry and stages it', () => {
|
||||
const dispatch = jasmine.createSpy();
|
||||
const commit = jasmine.createSpy();
|
||||
|
||||
beforeAll(() => {
|
||||
gon.features = { stageAllByDefault: true };
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
gon.features = originalGonFeatures;
|
||||
});
|
||||
|
||||
it('by default renames an entry and stages it', () => {
|
||||
const dispatch = jasmine.createSpy();
|
||||
const commit = jasmine.createSpy();
|
||||
|
||||
renameEntry(
|
||||
{ dispatch, commit, state: store.state, getters: store.getters },
|
||||
{ path: 'orig', name: 'renamed' },
|
||||
);
|
||||
|
||||
expect(commit.calls.allArgs()).toEqual([
|
||||
[types.RENAME_ENTRY, { path: 'orig', name: 'renamed', parentPath: undefined }],
|
||||
[types.STAGE_CHANGE, jasmine.objectContaining({ path: 'renamed' })],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('by default renames an entry and adds to changed', done => {
|
||||
testAction(
|
||||
renameEntry,
|
||||
renameEntry(
|
||||
{ dispatch, commit, state: store.state, getters: store.getters },
|
||||
{ path: 'orig', name: 'renamed' },
|
||||
store.state,
|
||||
[
|
||||
{
|
||||
type: types.RENAME_ENTRY,
|
||||
payload: {
|
||||
path: 'orig',
|
||||
name: 'renamed',
|
||||
parentPath: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: types.ADD_FILE_TO_CHANGED,
|
||||
payload: 'renamed',
|
||||
},
|
||||
],
|
||||
jasmine.any(Object),
|
||||
done,
|
||||
);
|
||||
|
||||
expect(commit.calls.allArgs()).toEqual([
|
||||
[types.RENAME_ENTRY, { path: 'orig', name: 'renamed', parentPath: undefined }],
|
||||
[types.STAGE_CHANGE, jasmine.objectContaining({ path: 'renamed' })],
|
||||
]);
|
||||
});
|
||||
|
||||
it('if not changed, completely unstages and discards entry if renamed to original', done => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import _ from 'underscore';
|
||||
import { head } from 'lodash';
|
||||
|
||||
import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
|
||||
import { mount, createLocalVue } from '@vue/test-utils';
|
||||
|
|
@ -99,9 +99,9 @@ describe('ProjectSelector component', () => {
|
|||
|
||||
it(`triggers a "projectClicked" event when a project is clicked`, () => {
|
||||
spyOn(vm, '$emit');
|
||||
wrapper.find(ProjectListItem).vm.$emit('click', _.first(searchResults));
|
||||
wrapper.find(ProjectListItem).vm.$emit('click', head(searchResults));
|
||||
|
||||
expect(vm.$emit).toHaveBeenCalledWith('projectClicked', _.first(searchResults));
|
||||
expect(vm.$emit).toHaveBeenCalledWith('projectClicked', head(searchResults));
|
||||
});
|
||||
|
||||
it(`shows a "no results" message if showNoResultsMessage === true`, () => {
|
||||
|
|
|
|||
|
|
@ -19,11 +19,29 @@ EOF
|
|||
expect(subject.perform_substitution(self, nil)).to be_nil
|
||||
end
|
||||
|
||||
it 'performs the substitution by default' do
|
||||
expect(subject.perform_substitution(self, content)).to eq <<EOF
|
||||
context 'when content contains command name' do
|
||||
it 'performs the substitution by default' do
|
||||
expect(subject.perform_substitution(self, content)).to eq <<EOF
|
||||
Hello! Let's do this!
|
||||
I like this stuff foo
|
||||
EOF
|
||||
end
|
||||
end
|
||||
|
||||
context 'when content contains command name in word' do
|
||||
let(:content) do
|
||||
<<EOF
|
||||
Hello! Let's do this!
|
||||
`/sub_names` I like this stuff
|
||||
EOF
|
||||
end
|
||||
|
||||
it 'does not perform the substitution' do
|
||||
expect(subject.perform_substitution(self, content)).to eq <<EOF
|
||||
Hello! Let's do this!
|
||||
`/sub_names` I like this stuff
|
||||
EOF
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -41,5 +59,9 @@ EOF
|
|||
it 'is nil if content does not have the command' do
|
||||
expect(subject.match('blah')).to be_falsey
|
||||
end
|
||||
|
||||
it 'is nil if content contains the command as prefix' do
|
||||
expect(subject.match('/sub_namex')).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ describe Ci::Build do
|
|||
it { is_expected.to respond_to(:has_trace?) }
|
||||
it { is_expected.to respond_to(:trace) }
|
||||
|
||||
it { is_expected.to delegate_method(:merge_request_event?).to(:pipeline) }
|
||||
it { is_expected.to delegate_method(:merge_request?).to(:pipeline) }
|
||||
it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) }
|
||||
it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) }
|
||||
|
||||
|
|
|
|||
|
|
@ -162,6 +162,23 @@ describe Ci::Pipeline, :mailer do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#merge_request?' do
|
||||
let(:pipeline) { create(:ci_pipeline, merge_request: merge_request) }
|
||||
let(:merge_request) { create(:merge_request) }
|
||||
|
||||
it 'returns true' do
|
||||
expect(pipeline).to be_merge_request
|
||||
end
|
||||
|
||||
context 'when merge request is nil' do
|
||||
let(:merge_request) { nil }
|
||||
|
||||
it 'returns false' do
|
||||
expect(pipeline).not_to be_merge_request
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#detached_merge_request_pipeline?' do
|
||||
subject { pipeline.detached_merge_request_pipeline? }
|
||||
|
||||
|
|
@ -367,48 +384,6 @@ describe Ci::Pipeline, :mailer do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'Validations for merge request pipelines' do
|
||||
let(:pipeline) do
|
||||
build(:ci_pipeline, source: source, merge_request: merge_request)
|
||||
end
|
||||
|
||||
let(:merge_request) do
|
||||
create(:merge_request,
|
||||
source_project: project,
|
||||
source_branch: 'feature',
|
||||
target_project: project,
|
||||
target_branch: 'master')
|
||||
end
|
||||
|
||||
context 'when source is merge request' do
|
||||
let(:source) { :merge_request_event }
|
||||
|
||||
context 'when merge request is specified' do
|
||||
it { expect(pipeline).to be_valid }
|
||||
end
|
||||
|
||||
context 'when merge request is empty' do
|
||||
let(:merge_request) { nil }
|
||||
|
||||
it { expect(pipeline).not_to be_valid }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when source is web' do
|
||||
let(:source) { :web }
|
||||
|
||||
context 'when merge request is specified' do
|
||||
it { expect(pipeline).not_to be_valid }
|
||||
end
|
||||
|
||||
context 'when merge request is empty' do
|
||||
let(:merge_request) { nil }
|
||||
|
||||
it { expect(pipeline).to be_valid }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'modules' do
|
||||
it_behaves_like 'AtomicInternalId', validate_presence: false do
|
||||
let(:internal_id_attribute) { :iid }
|
||||
|
|
@ -612,9 +587,9 @@ describe Ci::Pipeline, :mailer do
|
|||
]
|
||||
end
|
||||
|
||||
context 'when source is merge request' do
|
||||
context 'when pipeline is merge request' do
|
||||
let(:pipeline) do
|
||||
create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request)
|
||||
create(:ci_pipeline, merge_request: merge_request)
|
||||
end
|
||||
|
||||
let(:merge_request) do
|
||||
|
|
@ -651,7 +626,7 @@ describe Ci::Pipeline, :mailer do
|
|||
'CI_MERGE_REQUEST_TITLE' => merge_request.title,
|
||||
'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list,
|
||||
'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
|
||||
'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).join(','),
|
||||
'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','),
|
||||
'CI_MERGE_REQUEST_EVENT_TYPE' => pipeline.merge_request_event_type.to_s)
|
||||
end
|
||||
|
||||
|
|
@ -1263,9 +1238,9 @@ describe Ci::Pipeline, :mailer do
|
|||
is_expected.to be_truthy
|
||||
end
|
||||
|
||||
context 'when source is merge request' do
|
||||
context 'when pipeline is merge request' do
|
||||
let(:pipeline) do
|
||||
create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request)
|
||||
create(:ci_pipeline, merge_request: merge_request)
|
||||
end
|
||||
|
||||
let(:merge_request) do
|
||||
|
|
|
|||
|
|
@ -7,7 +7,18 @@ describe WikiPage do
|
|||
let(:user) { project.owner }
|
||||
let(:wiki) { ProjectWiki.new(project, user) }
|
||||
|
||||
subject { described_class.new(wiki) }
|
||||
let(:new_page) do
|
||||
described_class.new(wiki).tap do |page|
|
||||
page.attributes = { title: 'test page', content: 'test content' }
|
||||
end
|
||||
end
|
||||
|
||||
let(:existing_page) do
|
||||
create_page('test page', 'test content')
|
||||
wiki.find_page('test page')
|
||||
end
|
||||
|
||||
subject { new_page }
|
||||
|
||||
describe '.group_by_directory' do
|
||||
context 'when there are no pages' do
|
||||
|
|
@ -100,56 +111,134 @@ describe WikiPage do
|
|||
|
||||
describe "#initialize" do
|
||||
context "when initialized with an existing page" do
|
||||
before do
|
||||
create_page("test page", "test content")
|
||||
@page = wiki.wiki.page(title: "test page")
|
||||
@wiki_page = described_class.new(wiki, @page, true)
|
||||
end
|
||||
subject { existing_page }
|
||||
|
||||
it "sets the slug attribute" do
|
||||
expect(@wiki_page.slug).to eq("test-page")
|
||||
expect(subject.slug).to eq("test-page")
|
||||
end
|
||||
|
||||
it "sets the title attribute" do
|
||||
expect(@wiki_page.title).to eq("test page")
|
||||
expect(subject.title).to eq("test page")
|
||||
end
|
||||
|
||||
it "sets the formatted content attribute" do
|
||||
expect(@wiki_page.content).to eq("test content")
|
||||
expect(subject.content).to eq("test content")
|
||||
end
|
||||
|
||||
it "sets the format attribute" do
|
||||
expect(@wiki_page.format).to eq(:markdown)
|
||||
expect(subject.format).to eq(:markdown)
|
||||
end
|
||||
|
||||
it "sets the message attribute" do
|
||||
expect(@wiki_page.message).to eq("test commit")
|
||||
expect(subject.message).to eq("test commit")
|
||||
end
|
||||
|
||||
it "sets the version attribute" do
|
||||
expect(@wiki_page.version).to be_a Gitlab::Git::WikiPageVersion
|
||||
expect(subject.version).to be_a Gitlab::Git::WikiPageVersion
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
before do
|
||||
subject.attributes = { title: 'title', content: 'content' }
|
||||
end
|
||||
|
||||
it "validates presence of title" do
|
||||
subject.attributes.delete(:title)
|
||||
expect(subject.valid?).to be_falsey
|
||||
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors.keys).to contain_exactly(:title)
|
||||
end
|
||||
|
||||
it "validates presence of content" do
|
||||
subject.attributes.delete(:content)
|
||||
expect(subject.valid?).to be_falsey
|
||||
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors.keys).to contain_exactly(:content)
|
||||
end
|
||||
|
||||
describe '#validate_path_limits' do
|
||||
let(:max_title) { described_class::MAX_TITLE_BYTES }
|
||||
let(:max_directory) { described_class::MAX_DIRECTORY_BYTES }
|
||||
|
||||
where(:character) do
|
||||
['a', 'ä', '🙈']
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:size) { character.bytesize.to_f }
|
||||
let(:valid_title) { character * (max_title / size).floor }
|
||||
let(:valid_directory) { character * (max_directory / size).floor }
|
||||
let(:invalid_title) { character * ((max_title + 1) / size).ceil }
|
||||
let(:invalid_directory) { character * ((max_directory + 1) / size).ceil }
|
||||
|
||||
it 'accepts page titles below the limit' do
|
||||
subject.title = valid_title
|
||||
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
|
||||
it 'accepts directories below the limit' do
|
||||
subject.title = valid_directory + '/foo'
|
||||
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
|
||||
it 'accepts a path with page title and directory below the limit' do
|
||||
subject.title = "#{valid_directory}/#{valid_title}"
|
||||
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
|
||||
it 'rejects page titles exceeding the limit' do
|
||||
subject.title = invalid_title
|
||||
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:title]).to contain_exactly(
|
||||
"exceeds the limit of #{max_title} bytes for page titles"
|
||||
)
|
||||
end
|
||||
|
||||
it 'rejects directories exceeding the limit' do
|
||||
subject.title = invalid_directory + '/foo'
|
||||
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:title]).to contain_exactly(
|
||||
"exceeds the limit of #{max_directory} bytes for directory names"
|
||||
)
|
||||
end
|
||||
|
||||
it 'rejects a page with both title and directory exceeding the limit' do
|
||||
subject.title = "#{invalid_directory}/#{invalid_title}"
|
||||
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:title]).to contain_exactly(
|
||||
"exceeds the limit of #{max_title} bytes for page titles",
|
||||
"exceeds the limit of #{max_directory} bytes for directory names"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an existing page title exceeding the limit' do
|
||||
subject do
|
||||
title = 'a' * (max_title + 1)
|
||||
create_page(title, 'content')
|
||||
wiki.find_page(title)
|
||||
end
|
||||
|
||||
it 'accepts the exceeding title length when unchanged' do
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
|
||||
it 'rejects the exceeding title length when changed' do
|
||||
subject.title = 'b' * (max_title + 1)
|
||||
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors).to include(:title)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#create" do
|
||||
let(:wiki_attr) do
|
||||
let(:attributes) do
|
||||
{
|
||||
title: "Index",
|
||||
content: "Home Page",
|
||||
|
|
@ -158,22 +247,19 @@ describe WikiPage do
|
|||
}
|
||||
end
|
||||
|
||||
after do
|
||||
destroy_page("Index")
|
||||
end
|
||||
|
||||
context "with valid attributes" do
|
||||
it "saves the wiki page" do
|
||||
subject.create(wiki_attr)
|
||||
subject.create(attributes)
|
||||
|
||||
expect(wiki.find_page("Index")).not_to be_nil
|
||||
end
|
||||
|
||||
it "returns true" do
|
||||
expect(subject.create(wiki_attr)).to eq(true)
|
||||
expect(subject.create(attributes)).to eq(true)
|
||||
end
|
||||
|
||||
it 'saves the wiki page with message' do
|
||||
subject.create(wiki_attr)
|
||||
subject.create(attributes)
|
||||
|
||||
expect(wiki.find_page("Index").message).to eq 'Custom Commit Message'
|
||||
end
|
||||
|
|
@ -183,40 +269,37 @@ describe WikiPage do
|
|||
describe "dot in the title" do
|
||||
let(:title) { 'Index v1.2.3' }
|
||||
|
||||
before do
|
||||
@wiki_attr = { title: title, content: "Home Page", format: "markdown" }
|
||||
end
|
||||
|
||||
describe "#create" do
|
||||
after do
|
||||
destroy_page(title)
|
||||
end
|
||||
let(:attributes) { { title: title, content: "Home Page", format: "markdown" } }
|
||||
|
||||
context "with valid attributes" do
|
||||
it "saves the wiki page" do
|
||||
subject.create(@wiki_attr)
|
||||
subject.create(attributes)
|
||||
|
||||
expect(wiki.find_page(title)).not_to be_nil
|
||||
end
|
||||
|
||||
it "returns true" do
|
||||
expect(subject.create(@wiki_attr)).to eq(true)
|
||||
expect(subject.create(attributes)).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#update" do
|
||||
before do
|
||||
subject do
|
||||
create_page(title, "content")
|
||||
@page = wiki.find_page(title)
|
||||
wiki.find_page(title)
|
||||
end
|
||||
|
||||
it "updates the content of the page" do
|
||||
@page.update(content: "new content")
|
||||
@page = wiki.find_page(title)
|
||||
subject.update(content: "new content")
|
||||
page = wiki.find_page(title)
|
||||
|
||||
expect(page.content).to eq('new content')
|
||||
end
|
||||
|
||||
it "returns true" do
|
||||
expect(@page.update(content: "more content")).to be_truthy
|
||||
expect(subject.update(content: "more content")).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -226,66 +309,55 @@ describe WikiPage do
|
|||
it 'raises an error if a page with the same path already exists' do
|
||||
create_page('New Page', 'content')
|
||||
create_page('foo/bar', 'content')
|
||||
|
||||
expect { create_page('New Page', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError
|
||||
expect { create_page('foo/bar', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError
|
||||
|
||||
destroy_page('New Page')
|
||||
destroy_page('bar', 'foo')
|
||||
end
|
||||
|
||||
it 'if the title is preceded by a / it is removed' do
|
||||
create_page('/New Page', 'content')
|
||||
|
||||
expect(wiki.find_page('New Page')).not_to be_nil
|
||||
|
||||
destroy_page('New Page')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#update" do
|
||||
before do
|
||||
create_page("Update", "content")
|
||||
@page = wiki.find_page("Update")
|
||||
end
|
||||
|
||||
after do
|
||||
destroy_page(@page.title, @page.directory)
|
||||
end
|
||||
subject { existing_page }
|
||||
|
||||
context "with valid attributes" do
|
||||
it "updates the content of the page" do
|
||||
new_content = "new content"
|
||||
|
||||
@page.update(content: new_content)
|
||||
@page = wiki.find_page("Update")
|
||||
subject.update(content: new_content)
|
||||
page = wiki.find_page('test page')
|
||||
|
||||
expect(@page.content).to eq("new content")
|
||||
expect(page.content).to eq("new content")
|
||||
end
|
||||
|
||||
it "updates the title of the page" do
|
||||
new_title = "Index v.1.2.4"
|
||||
|
||||
@page.update(title: new_title)
|
||||
@page = wiki.find_page(new_title)
|
||||
subject.update(title: new_title)
|
||||
page = wiki.find_page(new_title)
|
||||
|
||||
expect(@page.title).to eq(new_title)
|
||||
expect(page.title).to eq(new_title)
|
||||
end
|
||||
|
||||
it "returns true" do
|
||||
expect(@page.update(content: "more content")).to be_truthy
|
||||
expect(subject.update(content: "more content")).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'with same last commit sha' do
|
||||
it 'returns true' do
|
||||
expect(@page.update(content: 'more content', last_commit_sha: @page.last_commit_sha)).to be_truthy
|
||||
expect(subject.update(content: 'more content', last_commit_sha: subject.last_commit_sha)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'with different last commit sha' do
|
||||
it 'raises exception' do
|
||||
expect { @page.update(content: 'more content', last_commit_sha: 'xxx') }.to raise_error(WikiPage::PageChangedError)
|
||||
expect { subject.update(content: 'more content', last_commit_sha: 'xxx') }.to raise_error(WikiPage::PageChangedError)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -293,23 +365,21 @@ describe WikiPage do
|
|||
it 'raises an error if the page already exists' do
|
||||
create_page('Existing Page', 'content')
|
||||
|
||||
expect { @page.update(title: 'Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError)
|
||||
expect(@page.title).to eq 'Update'
|
||||
expect(@page.content).to eq 'new_content'
|
||||
|
||||
destroy_page('Existing Page')
|
||||
expect { subject.update(title: 'Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError)
|
||||
expect(subject.title).to eq 'test page'
|
||||
expect(subject.content).to eq 'new_content'
|
||||
end
|
||||
|
||||
it 'updates the content and rename the file' do
|
||||
new_title = 'Renamed Page'
|
||||
new_content = 'updated content'
|
||||
|
||||
expect(@page.update(title: new_title, content: new_content)).to be_truthy
|
||||
expect(subject.update(title: new_title, content: new_content)).to be_truthy
|
||||
|
||||
@page = wiki.find_page(new_title)
|
||||
page = wiki.find_page(new_title)
|
||||
|
||||
expect(@page).not_to be_nil
|
||||
expect(@page.content).to eq new_content
|
||||
expect(page).not_to be_nil
|
||||
expect(page.content).to eq new_content
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -317,18 +387,16 @@ describe WikiPage do
|
|||
it 'raises an error if the page already exists' do
|
||||
create_page('foo/Existing Page', 'content')
|
||||
|
||||
expect { @page.update(title: 'foo/Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError)
|
||||
expect(@page.title).to eq 'Update'
|
||||
expect(@page.content).to eq 'new_content'
|
||||
|
||||
destroy_page('Existing Page', 'foo')
|
||||
expect { subject.update(title: 'foo/Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError)
|
||||
expect(subject.title).to eq 'test page'
|
||||
expect(subject.content).to eq 'new_content'
|
||||
end
|
||||
|
||||
it 'updates the content and moves the file' do
|
||||
new_title = 'foo/Other Page'
|
||||
new_content = 'new_content'
|
||||
|
||||
expect(@page.update(title: new_title, content: new_content)).to be_truthy
|
||||
expect(subject.update(title: new_title, content: new_content)).to be_truthy
|
||||
|
||||
page = wiki.find_page(new_title)
|
||||
|
||||
|
|
@ -337,120 +405,101 @@ describe WikiPage do
|
|||
end
|
||||
|
||||
context 'in subdir' do
|
||||
before do
|
||||
subject do
|
||||
create_page('foo/Existing Page', 'content')
|
||||
@page = wiki.find_page('foo/Existing Page')
|
||||
wiki.find_page('foo/Existing Page')
|
||||
end
|
||||
|
||||
it 'moves the page to the root folder if the title is preceded by /' do
|
||||
expect(@page.slug).to eq 'foo/Existing-Page'
|
||||
expect(@page.update(title: '/Existing Page', content: 'new_content')).to be_truthy
|
||||
expect(@page.slug).to eq 'Existing-Page'
|
||||
expect(subject.slug).to eq 'foo/Existing-Page'
|
||||
expect(subject.update(title: '/Existing Page', content: 'new_content')).to be_truthy
|
||||
expect(subject.slug).to eq 'Existing-Page'
|
||||
end
|
||||
|
||||
it 'does nothing if it has the same title' do
|
||||
original_path = @page.slug
|
||||
original_path = subject.slug
|
||||
|
||||
expect(@page.update(title: 'Existing Page', content: 'new_content')).to be_truthy
|
||||
expect(@page.slug).to eq original_path
|
||||
expect(subject.update(title: 'Existing Page', content: 'new_content')).to be_truthy
|
||||
expect(subject.slug).to eq original_path
|
||||
end
|
||||
end
|
||||
|
||||
context 'in root dir' do
|
||||
it 'does nothing if the title is preceded by /' do
|
||||
original_path = @page.slug
|
||||
original_path = subject.slug
|
||||
|
||||
expect(@page.update(title: '/Update', content: 'new_content')).to be_truthy
|
||||
expect(@page.slug).to eq original_path
|
||||
expect(subject.update(title: '/test page', content: 'new_content')).to be_truthy
|
||||
expect(subject.slug).to eq original_path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with invalid attributes" do
|
||||
it 'aborts update if title blank' do
|
||||
expect(@page.update(title: '', content: 'new_content')).to be_falsey
|
||||
expect(@page.content).to eq 'new_content'
|
||||
expect(subject.update(title: '', content: 'new_content')).to be_falsey
|
||||
expect(subject.content).to eq 'new_content'
|
||||
|
||||
page = wiki.find_page('Update')
|
||||
expect(page.content).to eq 'content'
|
||||
page = wiki.find_page('test page')
|
||||
|
||||
@page.title = 'Update'
|
||||
expect(page.content).to eq 'test content'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#destroy" do
|
||||
before do
|
||||
create_page("Delete Page", "content")
|
||||
@page = wiki.find_page("Delete Page")
|
||||
end
|
||||
subject { existing_page }
|
||||
|
||||
it "deletes the page" do
|
||||
@page.delete
|
||||
subject.delete
|
||||
|
||||
expect(wiki.list_pages).to be_empty
|
||||
end
|
||||
|
||||
it "returns true" do
|
||||
expect(@page.delete).to eq(true)
|
||||
expect(subject.delete).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#versions" do
|
||||
let(:page) { wiki.find_page("Update") }
|
||||
|
||||
before do
|
||||
create_page("Update", "content")
|
||||
end
|
||||
|
||||
after do
|
||||
destroy_page("Update")
|
||||
end
|
||||
subject { existing_page }
|
||||
|
||||
it "returns an array of all commits for the page" do
|
||||
3.times { |i| page.update(content: "content #{i}") }
|
||||
3.times { |i| subject.update(content: "content #{i}") }
|
||||
|
||||
expect(page.versions.count).to eq(4)
|
||||
expect(subject.versions.count).to eq(4)
|
||||
end
|
||||
|
||||
it 'returns instances of WikiPageVersion' do
|
||||
expect(page.versions).to all( be_a(Gitlab::Git::WikiPageVersion) )
|
||||
expect(subject.versions).to all( be_a(Gitlab::Git::WikiPageVersion) )
|
||||
end
|
||||
end
|
||||
|
||||
describe "#title" do
|
||||
before do
|
||||
create_page("Title", "content")
|
||||
@page = wiki.find_page("Title")
|
||||
end
|
||||
|
||||
after do
|
||||
destroy_page("Title")
|
||||
end
|
||||
|
||||
it "replaces a hyphen to a space" do
|
||||
@page.title = "Import-existing-repositories-into-GitLab"
|
||||
expect(@page.title).to eq("Import existing repositories into GitLab")
|
||||
subject.title = "Import-existing-repositories-into-GitLab"
|
||||
|
||||
expect(subject.title).to eq("Import existing repositories into GitLab")
|
||||
end
|
||||
|
||||
it 'unescapes html' do
|
||||
@page.title = 'foo & bar'
|
||||
subject.title = 'foo & bar'
|
||||
|
||||
expect(@page.title).to eq('foo & bar')
|
||||
expect(subject.title).to eq('foo & bar')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#path' do
|
||||
let(:path) { 'mypath.md' }
|
||||
let(:wiki_page) { instance_double('Gitlab::Git::WikiPage', path: path).as_null_object }
|
||||
let(:git_page) { instance_double('Gitlab::Git::WikiPage', path: path).as_null_object }
|
||||
|
||||
it 'returns the path when persisted' do
|
||||
page = described_class.new(wiki, wiki_page, true)
|
||||
page = described_class.new(wiki, git_page, true)
|
||||
|
||||
expect(page.path).to eq(path)
|
||||
end
|
||||
|
||||
it 'returns nil when not persisted' do
|
||||
page = described_class.new(wiki, wiki_page, false)
|
||||
page = described_class.new(wiki, git_page, false)
|
||||
|
||||
expect(page.path).to be_nil
|
||||
end
|
||||
|
|
@ -458,39 +507,38 @@ describe WikiPage do
|
|||
|
||||
describe '#directory' do
|
||||
context 'when the page is at the root directory' do
|
||||
it 'returns an empty string' do
|
||||
subject do
|
||||
create_page('file', 'content')
|
||||
page = wiki.find_page('file')
|
||||
wiki.find_page('file')
|
||||
end
|
||||
|
||||
expect(page.directory).to eq('')
|
||||
it 'returns an empty string' do
|
||||
expect(subject.directory).to eq('')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the page is inside an actual directory' do
|
||||
it 'returns the full directory hierarchy' do
|
||||
subject do
|
||||
create_page('dir_1/dir_1_1/file', 'content')
|
||||
page = wiki.find_page('dir_1/dir_1_1/file')
|
||||
wiki.find_page('dir_1/dir_1_1/file')
|
||||
end
|
||||
|
||||
expect(page.directory).to eq('dir_1/dir_1_1')
|
||||
it 'returns the full directory hierarchy' do
|
||||
expect(subject.directory).to eq('dir_1/dir_1_1')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#historical?' do
|
||||
let(:page) { wiki.find_page('Update') }
|
||||
let(:old_version) { page.versions.last.id }
|
||||
let(:old_page) { wiki.find_page('Update', old_version) }
|
||||
let(:latest_version) { page.versions.first.id }
|
||||
let(:latest_page) { wiki.find_page('Update', latest_version) }
|
||||
subject { existing_page }
|
||||
|
||||
let(:old_version) { subject.versions.last.id }
|
||||
let(:old_page) { wiki.find_page(subject.title, old_version) }
|
||||
let(:latest_version) { subject.versions.first.id }
|
||||
let(:latest_page) { wiki.find_page(subject.title, latest_version) }
|
||||
|
||||
before do
|
||||
create_page('Update', 'content')
|
||||
@page = wiki.find_page('Update')
|
||||
3.times { |i| @page.update(content: "content #{i}") }
|
||||
end
|
||||
|
||||
after do
|
||||
destroy_page('Update')
|
||||
3.times { |i| subject.update(content: "content #{i}") }
|
||||
end
|
||||
|
||||
it 'returns true when requesting an old version' do
|
||||
|
|
@ -520,56 +568,48 @@ describe WikiPage do
|
|||
|
||||
describe '#to_partial_path' do
|
||||
it 'returns the relative path to the partial to be used' do
|
||||
page = build(:wiki_page)
|
||||
|
||||
expect(page.to_partial_path).to eq('projects/wikis/wiki_page')
|
||||
expect(subject.to_partial_path).to eq('projects/wikis/wiki_page')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#==' do
|
||||
let(:original_wiki_page) { create(:wiki_page) }
|
||||
subject { existing_page }
|
||||
|
||||
it 'returns true for identical wiki page' do
|
||||
expect(original_wiki_page).to eq(original_wiki_page)
|
||||
expect(subject).to eq(subject)
|
||||
end
|
||||
|
||||
it 'returns false for updated wiki page' do
|
||||
updated_wiki_page = original_wiki_page.update(content: "Updated content")
|
||||
expect(original_wiki_page).not_to eq(updated_wiki_page)
|
||||
subject.update(content: "Updated content")
|
||||
updated_page = wiki.find_page('test page')
|
||||
|
||||
expect(updated_page).not_to be_nil
|
||||
expect(updated_page).not_to eq(subject)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#last_commit_sha' do
|
||||
before do
|
||||
create_page("Update", "content")
|
||||
@page = wiki.find_page("Update")
|
||||
end
|
||||
|
||||
after do
|
||||
destroy_page("Update")
|
||||
end
|
||||
subject { existing_page }
|
||||
|
||||
it 'returns commit sha' do
|
||||
expect(@page.last_commit_sha).to eq @page.last_version.sha
|
||||
expect(subject.last_commit_sha).to eq subject.last_version.sha
|
||||
end
|
||||
|
||||
it 'is changed after page updated' do
|
||||
last_commit_sha_before_update = @page.last_commit_sha
|
||||
last_commit_sha_before_update = subject.last_commit_sha
|
||||
|
||||
@page.update(content: "new content")
|
||||
@page = wiki.find_page("Update")
|
||||
subject.update(content: "new content")
|
||||
page = wiki.find_page('test page')
|
||||
|
||||
expect(@page.last_commit_sha).not_to eq last_commit_sha_before_update
|
||||
expect(page.last_commit_sha).not_to eq last_commit_sha_before_update
|
||||
end
|
||||
end
|
||||
|
||||
describe '#hook_attrs' do
|
||||
it 'adds absolute urls for images in the content' do
|
||||
create_page("test page", "test")
|
||||
page = wiki.wiki.page(title: "test page")
|
||||
wiki_page = described_class.new(wiki, page, true)
|
||||
subject.attributes[:content] = 'test'
|
||||
|
||||
expect(wiki_page.hook_attrs['content']).to eq("test")
|
||||
expect(subject.hook_attrs['content']).to eq("test")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -587,11 +627,6 @@ describe WikiPage do
|
|||
wiki.wiki.write_page(name, :markdown, content, commit_details)
|
||||
end
|
||||
|
||||
def destroy_page(title, dir = '')
|
||||
page = wiki.wiki.page(title: title, dir: dir)
|
||||
wiki.delete_page(page, "test commit")
|
||||
end
|
||||
|
||||
def get_slugs(page_or_dir)
|
||||
if page_or_dir.is_a? WikiPage
|
||||
[page_or_dir.slug]
|
||||
|
|
|
|||
|
|
@ -148,6 +148,12 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
|
|||
end
|
||||
|
||||
context 'when "include" is provided' do
|
||||
let(:file_content) do
|
||||
YAML.dump(
|
||||
rspec: { script: 'rspec' },
|
||||
echo: { script: 'echo' })
|
||||
end
|
||||
|
||||
shared_examples 'creates a child pipeline' do
|
||||
it 'creates only one new pipeline' do
|
||||
expect { service.execute(bridge) }
|
||||
|
|
@ -189,9 +195,6 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
|
|||
end
|
||||
|
||||
before do
|
||||
file_content = YAML.dump(
|
||||
rspec: { script: 'rspec' },
|
||||
echo: { script: 'echo' })
|
||||
upstream_project.repository.create_file(
|
||||
user, 'child-pipeline.yml', file_content, message: 'message', branch_name: 'master')
|
||||
|
||||
|
|
@ -218,6 +221,29 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
|
|||
it_behaves_like 'creates a child pipeline'
|
||||
end
|
||||
|
||||
context 'when the parent is a merge request pipeline' do
|
||||
let(:merge_request) { create(:merge_request, source_project: bridge.project, target_project: bridge.project) }
|
||||
let(:file_content) do
|
||||
YAML.dump(
|
||||
workflow: { rules: [{ if: '$CI_MERGE_REQUEST_ID' }] },
|
||||
rspec: { script: 'rspec' },
|
||||
echo: { script: 'echo' })
|
||||
end
|
||||
|
||||
before do
|
||||
bridge.pipeline.update!(source: :merge_request_event, merge_request: merge_request)
|
||||
end
|
||||
|
||||
it_behaves_like 'creates a child pipeline'
|
||||
|
||||
it 'propagates the merge request to the child pipeline' do
|
||||
pipeline = service.execute(bridge)
|
||||
|
||||
expect(pipeline.merge_request).to eq(merge_request)
|
||||
expect(pipeline).to be_merge_request
|
||||
end
|
||||
end
|
||||
|
||||
context 'when upstream pipeline is a child pipeline' do
|
||||
let!(:pipeline_source) do
|
||||
create(:ci_sources_pipeline,
|
||||
|
|
|
|||
|
|
@ -1473,15 +1473,6 @@ describe Ci::CreatePipelineService do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when merge request is not specified' do
|
||||
let(:merge_request) { nil }
|
||||
|
||||
it 'does not create a detached merge request pipeline' do
|
||||
expect(pipeline).not_to be_persisted
|
||||
expect(pipeline.errors[:merge_request]).to eq(["can't be blank"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when config does not have merge_requests keywords" do
|
||||
|
|
@ -1518,17 +1509,6 @@ describe Ci::CreatePipelineService do
|
|||
.to eq(['No stages / jobs for this pipeline.'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when merge request is not specified' do
|
||||
let(:merge_request) { nil }
|
||||
|
||||
it 'does not create a detached merge request pipeline' do
|
||||
expect(pipeline).not_to be_persisted
|
||||
|
||||
expect(pipeline.errors[:base])
|
||||
.to eq(['No stages / jobs for this pipeline.'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when config uses regular expression for only keyword" do
|
||||
|
|
@ -1623,6 +1603,7 @@ describe Ci::CreatePipelineService do
|
|||
|
||||
context 'when source is web' do
|
||||
let(:source) { :web }
|
||||
let(:merge_request) { nil }
|
||||
|
||||
context "when config has merge_requests keywords" do
|
||||
let(:config) do
|
||||
|
|
@ -1644,30 +1625,11 @@ describe Ci::CreatePipelineService do
|
|||
}
|
||||
end
|
||||
|
||||
context 'when merge request is specified' do
|
||||
let(:merge_request) do
|
||||
create(:merge_request,
|
||||
source_project: project,
|
||||
source_branch: Gitlab::Git.ref_name(ref_name),
|
||||
target_project: project,
|
||||
target_branch: 'master')
|
||||
end
|
||||
|
||||
it 'does not create a merge request pipeline' do
|
||||
expect(pipeline).not_to be_persisted
|
||||
expect(pipeline.errors[:merge_request]).to eq(["must be blank"])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when merge request is not specified' do
|
||||
let(:merge_request) { nil }
|
||||
|
||||
it 'creates a branch pipeline' do
|
||||
expect(pipeline).to be_persisted
|
||||
expect(pipeline).to be_web
|
||||
expect(pipeline.merge_request).to be_nil
|
||||
expect(pipeline.builds.order(:stage_id).pluck(:name)).to eq(%w[build pages])
|
||||
end
|
||||
it 'creates a branch pipeline' do
|
||||
expect(pipeline).to be_persisted
|
||||
expect(pipeline).to be_web
|
||||
expect(pipeline.merge_request).to be_nil
|
||||
expect(pipeline.builds.order(:stage_id).pluck(:name)).to eq(%w[build pages])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ module KubernetesHelpers
|
|||
end
|
||||
|
||||
logs_url = service.api_url + "/api/v1/namespaces/#{namespace}/pods/#{pod_name}" \
|
||||
"/log?#{container_query_param}tailLines=#{Clusters::Platforms::Kubernetes::LOGS_LIMIT}×tamps=true"
|
||||
"/log?#{container_query_param}tailLines=#{::PodLogs::KubernetesService::LOGS_LIMIT}×tamps=true"
|
||||
|
||||
if status
|
||||
response = { status: status }
|
||||
|
|
|
|||
Loading…
Reference in New Issue