Commit c3f24fcb authored by Mike Greiling's avatar Mike Greiling

Merge branch 'master' into 'ps-ide-defer-loading-terminal-modules'

# Conflicts:
#   doc/development/testing_guide/frontend_testing.md
parents 14d8c620 5891d8c0
...@@ -20,8 +20,7 @@ export const init = ({ dispatch }, { endpoint, logState, pagePath }) => { ...@@ -20,8 +20,7 @@ export const init = ({ dispatch }, { endpoint, logState, pagePath }) => {
logState, logState,
pagePath, pagePath,
}); });
dispatch('fetchJob');
return Promise.all([dispatch('fetchJob'), dispatch('fetchTrace')]);
}; };
export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint); export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint);
...@@ -39,6 +38,7 @@ export const toggleSidebar = ({ dispatch, state }) => { ...@@ -39,6 +38,7 @@ export const toggleSidebar = ({ dispatch, state }) => {
}; };
let eTagPoll; let eTagPoll;
let isTraceReadyForRender;
export const clearEtagPoll = () => { export const clearEtagPoll = () => {
eTagPoll = null; eTagPoll = null;
...@@ -70,7 +70,14 @@ export const fetchJob = ({ state, dispatch }) => { ...@@ -70,7 +70,14 @@ export const fetchJob = ({ state, dispatch }) => {
}); });
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
eTagPoll.makeRequest(); // eslint-disable-next-line promise/catch-or-return
eTagPoll.makeRequest().then(() => {
// if a job is canceled we still need to dispatch
// fetchTrace to get the trace so we check for has_trace
if (state.job.started || state.job.has_trace) {
dispatch('fetchTrace');
}
});
} else { } else {
axios axios
.get(state.jobEndpoint) .get(state.jobEndpoint)
...@@ -80,9 +87,15 @@ export const fetchJob = ({ state, dispatch }) => { ...@@ -80,9 +87,15 @@ export const fetchJob = ({ state, dispatch }) => {
Visibility.change(() => { Visibility.change(() => {
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
// This check is needed to ensure the loading icon
// is not shown for a finished job during a visibility change
if (!isTraceReadyForRender) {
dispatch('startPollingTrace');
}
dispatch('restartPolling'); dispatch('restartPolling');
} else { } else {
dispatch('stopPolling'); dispatch('stopPolling');
dispatch('stopPollingTrace');
} }
}); });
}; };
...@@ -163,6 +176,8 @@ export const fetchTrace = ({ dispatch, state }) => ...@@ -163,6 +176,8 @@ export const fetchTrace = ({ dispatch, state }) =>
params: { state: state.traceState }, params: { state: state.traceState },
}) })
.then(({ data }) => { .then(({ data }) => {
isTraceReadyForRender = data.complete;
dispatch('toggleScrollisInBottom', isScrolledToBottom()); dispatch('toggleScrollisInBottom', isScrolledToBottom());
dispatch('receiveTraceSuccess', data); dispatch('receiveTraceSuccess', data);
......
...@@ -49,6 +49,7 @@ export default { ...@@ -49,6 +49,7 @@ export default {
[types.SET_TRACE_TIMEOUT](state, id) { [types.SET_TRACE_TIMEOUT](state, id) {
state.traceTimeout = id; state.traceTimeout = id;
state.isTraceComplete = false;
}, },
/** /**
......
<script> <script>
import { GlPagination } from '@gitlab/ui'; import { GlKeysetPagination } from '@gitlab/ui';
import ImageListRow from './image_list_row.vue'; import ImageListRow from './image_list_row.vue';
export default { export default {
name: 'ImageList', name: 'ImageList',
components: { components: {
GlPagination, GlKeysetPagination,
ImageListRow, ImageListRow,
}, },
props: { props: {
...@@ -13,19 +13,14 @@ export default { ...@@ -13,19 +13,14 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
pagination: { pageInfo: {
type: Object, type: Object,
required: true, required: true,
}, },
}, },
computed: { computed: {
currentPage: { showPagination() {
get() { return this.pageInfo.hasPreviousPage || this.pageInfo.hasNextPage;
return this.pagination.page;
},
set(page) {
this.$emit('pageChange', page);
},
}, },
}, },
}; };
...@@ -40,13 +35,15 @@ export default { ...@@ -40,13 +35,15 @@ export default {
:first="index === 0" :first="index === 0"
@delete="$emit('delete', $event)" @delete="$emit('delete', $event)"
/> />
<div class="gl-display-flex gl-justify-content-center">
<gl-pagination <gl-keyset-pagination
v-model="currentPage" v-if="showPagination"
:per-page="pagination.perPage" :has-next-page="pageInfo.hasNextPage"
:total-items="pagination.total" :has-previous-page="pageInfo.hasPreviousPage"
align="center" class="gl-mt-3"
class="w-100 gl-mt-3" @prev="$emit('prev-page')"
@next="$emit('next-page')"
/> />
</div> </div>
</div>
</template> </template>
<script> <script>
import { GlTooltipDirective, GlIcon, GlSprintf } from '@gitlab/ui'; import { GlTooltipDirective, GlIcon, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale'; import { n__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue'; import ListItem from '~/vue_shared/components/registry/list_item.vue';
import DeleteButton from '../delete_button.vue'; import DeleteButton from '../delete_button.vue';
...@@ -11,6 +13,8 @@ import { ...@@ -11,6 +13,8 @@ import {
REMOVE_REPOSITORY_LABEL, REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION, ROW_SCHEDULED_FOR_DELETION,
CLEANUP_TIMED_OUT_ERROR_MESSAGE, CLEANUP_TIMED_OUT_ERROR_MESSAGE,
IMAGE_DELETE_SCHEDULED_STATUS,
IMAGE_FAILED_DELETED_STATUS,
} from '../../constants/index'; } from '../../constants/index';
export default { export default {
...@@ -38,19 +42,29 @@ export default { ...@@ -38,19 +42,29 @@ export default {
}, },
computed: { computed: {
disabledDelete() { disabledDelete() {
return !this.item.destroy_path || this.item.deleting; return !this.item.canDelete || this.deleting;
},
id() {
return getIdFromGraphQLId(this.item.id);
},
deleting() {
return this.item.status === IMAGE_DELETE_SCHEDULED_STATUS;
},
failedDelete() {
return this.item.status === IMAGE_FAILED_DELETED_STATUS;
}, },
tagsCountText() { tagsCountText() {
return n__( return n__(
'ContainerRegistry|%{count} Tag', 'ContainerRegistry|%{count} Tag',
'ContainerRegistry|%{count} Tags', 'ContainerRegistry|%{count} Tags',
this.item.tags_count, this.item.tagsCount,
); );
}, },
warningIconText() { warningIconText() {
if (this.item.failedDelete) { if (this.failedDelete) {
return ASYNC_DELETE_IMAGE_ERROR_MESSAGE; return ASYNC_DELETE_IMAGE_ERROR_MESSAGE;
} else if (this.item.cleanup_policy_started_at) { }
if (this.item.expirationPolicyStartedAt) {
return CLEANUP_TIMED_OUT_ERROR_MESSAGE; return CLEANUP_TIMED_OUT_ERROR_MESSAGE;
} }
return null; return null;
...@@ -63,23 +77,23 @@ export default { ...@@ -63,23 +77,23 @@ export default {
<list-item <list-item
v-gl-tooltip="{ v-gl-tooltip="{
placement: 'left', placement: 'left',
disabled: !item.deleting, disabled: !deleting,
title: $options.i18n.ROW_SCHEDULED_FOR_DELETION, title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
}" }"
v-bind="$attrs" v-bind="$attrs"
:disabled="item.deleting" :disabled="deleting"
> >
<template #left-primary> <template #left-primary>
<router-link <router-link
class="gl-text-body gl-font-weight-bold" class="gl-text-body gl-font-weight-bold"
data-testid="details-link" data-testid="details-link"
:to="{ name: 'details', params: { id: item.id } }" :to="{ name: 'details', params: { id } }"
> >
{{ item.path }} {{ item.path }}
</router-link> </router-link>
<clipboard-button <clipboard-button
v-if="item.location" v-if="item.location"
:disabled="item.deleting" :disabled="deleting"
:text="item.location" :text="item.location"
:title="item.location" :title="item.location"
category="tertiary" category="tertiary"
...@@ -97,7 +111,7 @@ export default { ...@@ -97,7 +111,7 @@ export default {
<gl-icon name="tag" class="gl-mr-2" /> <gl-icon name="tag" class="gl-mr-2" />
<gl-sprintf :message="tagsCountText"> <gl-sprintf :message="tagsCountText">
<template #count> <template #count>
{{ item.tags_count }} {{ item.tagsCount }}
</template> </template>
</gl-sprintf> </gl-sprintf>
</span> </span>
...@@ -106,7 +120,7 @@ export default { ...@@ -106,7 +120,7 @@ export default {
<delete-button <delete-button
:title="$options.i18n.REMOVE_REPOSITORY_LABEL" :title="$options.i18n.REMOVE_REPOSITORY_LABEL"
:disabled="disabledDelete" :disabled="disabledDelete"
:tooltip-disabled="Boolean(item.destroy_path)" :tooltip-disabled="item.canDelete"
:tooltip-title="$options.i18n.LIST_DELETE_BUTTON_DISABLED" :tooltip-title="$options.i18n.LIST_DELETE_BUTTON_DISABLED"
@delete="$emit('delete', item)" @delete="$emit('delete', item)"
/> />
......
...@@ -44,5 +44,6 @@ export const EMPTY_RESULT_MESSAGE = s__( ...@@ -44,5 +44,6 @@ export const EMPTY_RESULT_MESSAGE = s__(
// Parameters // Parameters
export const IMAGE_DELETE_SCHEDULED_STATUS = 'delete_scheduled'; export const IMAGE_DELETE_SCHEDULED_STATUS = 'DELETE_SCHEDULED';
export const IMAGE_FAILED_DELETED_STATUS = 'delete_failed'; export const IMAGE_FAILED_DELETED_STATUS = 'DELETE_FAILED';
export const GRAPHQL_PAGE_SIZE = 10;
fragment ContainerRepositoryFields on ContainerRepository {
id
name
path
status
location
canDelete
createdAt
tagsCount
expirationPolicyStartedAt
}
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
assumeImmutableResults: true,
},
),
});
mutation destroyContainerRepository($id: ContainerRepositoryID!) {
destroyContainerRepository(input: { id: $id }) {
containerRepository {
id
status
}
errors
}
}
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "../fragments/container_repository.fragment.graphql"
query getProjectContainerRepositories(
$fullPath: ID!
$name: String
$first: Int
$last: Int
$after: String
$before: String
) {
group(fullPath: $fullPath) {
containerRepositoriesCount
containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
nodes {
...ContainerRepositoryFields
}
pageInfo {
...PageInfo
}
}
}
}
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "../fragments/container_repository.fragment.graphql"
query getProjectContainerRepositories(
$fullPath: ID!
$name: String
$first: Int
$last: Int
$after: String
$before: String
) {
project(fullPath: $fullPath) {
containerRepositoriesCount
containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
nodes {
...ContainerRepositoryFields
}
pageInfo {
...PageInfo
}
}
}
}
...@@ -5,6 +5,7 @@ import RegistryExplorer from './pages/index.vue'; ...@@ -5,6 +5,7 @@ import RegistryExplorer from './pages/index.vue';
import RegistryBreadcrumb from './components/registry_breadcrumb.vue'; import RegistryBreadcrumb from './components/registry_breadcrumb.vue';
import { createStore } from './stores'; import { createStore } from './stores';
import createRouter from './router'; import createRouter from './router';
import { apolloProvider } from './graphql/index';
Vue.use(Translate); Vue.use(Translate);
Vue.use(GlToast); Vue.use(GlToast);
...@@ -27,6 +28,7 @@ export default () => { ...@@ -27,6 +28,7 @@ export default () => {
el, el,
store, store,
router, router,
apolloProvider,
components: { components: {
RegistryExplorer, RegistryExplorer,
}, },
......
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState } from 'vuex';
import { import {
GlEmptyState, GlEmptyState,
GlTooltipDirective, GlTooltipDirective,
...@@ -11,6 +11,7 @@ import { ...@@ -11,6 +11,7 @@ import {
GlSearchBoxByClick, GlSearchBoxByClick,
} from '@gitlab/ui'; } from '@gitlab/ui';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import createFlash from '~/flash';
import ProjectEmptyState from '../components/list_page/project_empty_state.vue'; import ProjectEmptyState from '../components/list_page/project_empty_state.vue';
import GroupEmptyState from '../components/list_page/group_empty_state.vue'; import GroupEmptyState from '../components/list_page/group_empty_state.vue';
...@@ -18,6 +19,10 @@ import RegistryHeader from '../components/list_page/registry_header.vue'; ...@@ -18,6 +19,10 @@ import RegistryHeader from '../components/list_page/registry_header.vue';
import ImageList from '../components/list_page/image_list.vue'; import ImageList from '../components/list_page/image_list.vue';
import CliCommands from '../components/list_page/cli_commands.vue'; import CliCommands from '../components/list_page/cli_commands.vue';
import getProjectContainerRepositories from '../graphql/queries/get_project_container_repositories.graphql';
import getGroupContainerRepositories from '../graphql/queries/get_group_container_repositories.graphql';
import deleteContainerRepository from '../graphql/mutations/delete_container_repository.graphql';
import { import {
DELETE_IMAGE_SUCCESS_MESSAGE, DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE, DELETE_IMAGE_ERROR_MESSAGE,
...@@ -29,6 +34,8 @@ import { ...@@ -29,6 +34,8 @@ import {
IMAGE_REPOSITORY_LIST_LABEL, IMAGE_REPOSITORY_LIST_LABEL,
EMPTY_RESULT_TITLE, EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE, EMPTY_RESULT_MESSAGE,
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
} from '../constants/index'; } from '../constants/index';
export default { export default {
...@@ -66,21 +73,63 @@ export default { ...@@ -66,21 +73,63 @@ export default {
EMPTY_RESULT_TITLE, EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE, EMPTY_RESULT_MESSAGE,
}, },
apollo: {
images: {
query() {
return this.graphQlQuery;
},
variables() {
return this.queryVariables;
},
update(data) {
return data[this.graphqlResource]?.containerRepositories.nodes;
},
result({ data }) {
this.pageInfo = data[this.graphqlResource]?.containerRepositories?.pageInfo;
this.containerRepositoriesCount = data[this.graphqlResource]?.containerRepositoriesCount;
},
error() {
createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
},
},
},
data() { data() {
return { return {
images: [],
pageInfo: {},
containerRepositoriesCount: 0,
itemToDelete: {}, itemToDelete: {},
deleteAlertType: null, deleteAlertType: null,
search: null, searchValue: null,
isEmpty: false, name: null,
mutationLoading: false,
}; };
}, },
computed: { computed: {
...mapState(['config', 'isLoading', 'images', 'pagination']), ...mapState(['config']),
graphqlResource() {
return this.config.isGroupPage ? 'group' : 'project';
},
graphQlQuery() {
return this.config.isGroupPage
? getGroupContainerRepositories
: getProjectContainerRepositories;
},
queryVariables() {
return {
name: this.name,
fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath,
first: GRAPHQL_PAGE_SIZE,
};
},
tracking() { tracking() {
return { return {
label: 'registry_repository_delete', label: 'registry_repository_delete',
}; };
}, },
isLoading() {
return this.$apollo.queries.images.loading || this.mutationLoading;
},
showCommands() { showCommands() {
return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length); return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length);
}, },
...@@ -93,19 +142,7 @@ export default { ...@@ -93,19 +142,7 @@ export default {
: DELETE_IMAGE_ERROR_MESSAGE; : DELETE_IMAGE_ERROR_MESSAGE;
}, },
}, },
mounted() {
this.loadImageList(this.$route.name);
},
methods: { methods: {
...mapActions(['requestImagesList', 'requestDeleteImage']),
loadImageList(fromName) {
if (!fromName || !this.images?.length) {
return this.requestImagesList().then(() => {
this.isEmpty = this.images.length === 0;
});
}
return Promise.resolve();
},
deleteImage(item) { deleteImage(item) {
this.track('click_button'); this.track('click_button');
this.itemToDelete = item; this.itemToDelete = item;
...@@ -113,18 +150,59 @@ export default { ...@@ -113,18 +150,59 @@ export default {
}, },
handleDeleteImage() { handleDeleteImage() {
this.track('confirm_delete'); this.track('confirm_delete');
return this.requestDeleteImage(this.itemToDelete) this.mutationLoading = true;
.then(() => { return this.$apollo
.mutate({
mutation: deleteContainerRepository,
variables: {
id: this.itemToDelete.id,
},
})
.then(({ data }) => {
if (data?.destroyContainerRepository?.errors[0]) {
this.deleteAlertType = 'danger';
} else {
this.deleteAlertType = 'success'; this.deleteAlertType = 'success';
}
}) })
.catch(() => { .catch(() => {
this.deleteAlertType = 'danger'; this.deleteAlertType = 'danger';
})
.finally(() => {
this.mutationLoading = false;
}); });
}, },
dismissDeleteAlert() { dismissDeleteAlert() {
this.deleteAlertType = null; this.deleteAlertType = null;
this.itemToDelete = {}; this.itemToDelete = {};
}, },
fetchNextPage() {
if (this.pageInfo?.hasNextPage) {
this.$apollo.queries.images.fetchMore({
variables: {
after: this.pageInfo?.endCursor,
first: GRAPHQL_PAGE_SIZE,
},
updateQuery(previousResult, { fetchMoreResult }) {
return fetchMoreResult;
},
});
}
},
fetchPreviousPage() {
if (this.pageInfo?.hasPreviousPage) {
this.$apollo.queries.images.fetchMore({
variables: {
first: null,
before: this.pageInfo?.startCursor,
last: GRAPHQL_PAGE_SIZE,
},
updateQuery(previousResult, { fetchMoreResult }) {
return fetchMoreResult;
},
});
}
},
}, },
}; };
</script> </script>
...@@ -134,7 +212,7 @@ export default { ...@@ -134,7 +212,7 @@ export default {
<gl-alert <gl-alert
v-if="showDeleteAlert" v-if="showDeleteAlert"
:variant="deleteAlertType" :variant="deleteAlertType"
class="mt-2" class="gl-mt-5"
dismissible dismissible
@dismiss="dismissDeleteAlert" @dismiss="dismissDeleteAlert"
> >
...@@ -165,7 +243,7 @@ export default { ...@@ -165,7 +243,7 @@ export default {
<template v-else> <template v-else>
<registry-header <registry-header
:images-count="pagination.total" :images-count="containerRepositoriesCount"
:expiration-policy="config.expirationPolicy" :expiration-policy="config.expirationPolicy"
:help-page-path="config.helpPagePath" :help-page-path="config.helpPagePath"
:expiration-policy-help-page-path="config.expirationPolicyHelpPagePath" :expiration-policy-help-page-path="config.expirationPolicyHelpPagePath"
...@@ -176,7 +254,7 @@ export default { ...@@ -176,7 +254,7 @@ export default {
</template> </template>
</registry-header> </registry-header>
<div v-if="isLoading" class="mt-2"> <div v-if="isLoading" class="gl-mt-5">
<gl-skeleton-loader <gl-skeleton-loader
v-for="index in $options.loader.repeat" v-for="index in $options.loader.repeat"
:key="index" :key="index"
...@@ -190,16 +268,17 @@ export default { ...@@ -190,16 +268,17 @@ export default {
</gl-skeleton-loader> </gl-skeleton-loader>
</div> </div>
<template v-else> <template v-else>
<template v-if="!isEmpty"> <template v-if="images.length > 0 || name">
<div class="gl-display-flex gl-p-1 gl-mt-3" data-testid="listHeader"> <div class="gl-display-flex gl-p-1 gl-mt-3" data-testid="listHeader">
<div class="gl-flex-fill-1"> <div class="gl-flex-fill-1">
<h5>{{ $options.i18n.IMAGE_REPOSITORY_LIST_LABEL }}</h5> <h5>{{ $options.i18n.IMAGE_REPOSITORY_LIST_LABEL }}</h5>
</div> </div>
<div> <div>
<gl-search-box-by-click <gl-search-box-by-click
v-model="search" v-model="searchValue"
:placeholder="$options.i18n.SEARCH_PLACEHOLDER_TEXT" :placeholder="$options.i18n.SEARCH_PLACEHOLDER_TEXT"
@submit="requestImagesList({ name: $event })" @clear="name = null"
@submit="name = $event"
/> />
</div> </div>
</div> </div>
...@@ -207,9 +286,10 @@ export default { ...@@ -207,9 +286,10 @@ export default {
<image-list <image-list
v-if="images.length" v-if="images.length"
:images="images" :images="images"
:pagination="pagination" :page-info="pageInfo"
@pageChange="requestImagesList({ pagination: { page: $event }, name: search })"
@delete="deleteImage" @delete="deleteImage"
@prev-page="fetchPreviousPage"
@next-page="fetchNextPage"
/> />
<gl-empty-state <gl-empty-state
......
...@@ -11,7 +11,7 @@ module Mutations ...@@ -11,7 +11,7 @@ module Mutations
argument :iid, GraphQL::STRING_TYPE, argument :iid, GraphQL::STRING_TYPE,
required: true, required: true,
description: "The iid of the alert to mutate" description: "The IID of the alert to mutate"
field :alert, field :alert,
Types::AlertManagement::AlertType, Types::AlertManagement::AlertType,
......
...@@ -8,7 +8,7 @@ module Mutations ...@@ -8,7 +8,7 @@ module Mutations
argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration], argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration],
required: true, required: true,
description: "The id of the integration to remove" description: "The ID of the integration to remove"
def resolve(id:) def resolve(id:)
integration = authorized_find!(id: id) integration = authorized_find!(id: id)
......
...@@ -8,7 +8,7 @@ module Mutations ...@@ -8,7 +8,7 @@ module Mutations
argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration], argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration],
required: true, required: true,
description: "The id of the integration to mutate" description: "The ID of the integration to mutate"
def resolve(id:) def resolve(id:)
integration = authorized_find!(id: id) integration = authorized_find!(id: id)
......
...@@ -8,7 +8,7 @@ module Mutations ...@@ -8,7 +8,7 @@ module Mutations
argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration], argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration],
required: true, required: true,
description: "The id of the integration to mutate" description: "The ID of the integration to mutate"
argument :name, GraphQL::STRING_TYPE, argument :name, GraphQL::STRING_TYPE,
required: false, required: false,
......
...@@ -8,7 +8,7 @@ module Mutations ...@@ -8,7 +8,7 @@ module Mutations
argument :id, Types::GlobalIDType[::PrometheusService], argument :id, Types::GlobalIDType[::PrometheusService],
required: true, required: true,
description: "The id of the integration to mutate" description: "The ID of the integration to mutate"
def resolve(id:) def resolve(id:)
integration = authorized_find!(id: id) integration = authorized_find!(id: id)
......
...@@ -8,7 +8,7 @@ module Mutations ...@@ -8,7 +8,7 @@ module Mutations
argument :id, Types::GlobalIDType[::PrometheusService], argument :id, Types::GlobalIDType[::PrometheusService],
required: true, required: true,
description: "The id of the integration to mutate" description: "The ID of the integration to mutate"
argument :active, GraphQL::BOOLEAN_TYPE, argument :active, GraphQL::BOOLEAN_TYPE,
required: false, required: false,
......
...@@ -12,7 +12,7 @@ module Mutations ...@@ -12,7 +12,7 @@ module Mutations
argument :awardable_id, argument :awardable_id,
::Types::GlobalIDType[::Awardable], ::Types::GlobalIDType[::Awardable],
required: true, required: true,
description: 'The global id of the awardable resource' description: 'The global ID of the awardable resource'
argument :name, argument :name,
GraphQL::STRING_TYPE, GraphQL::STRING_TYPE,
......
...@@ -7,7 +7,7 @@ module Mutations ...@@ -7,7 +7,7 @@ module Mutations
argument :id, PipelineID, argument :id, PipelineID,
required: true, required: true,
description: 'The id of the pipeline to mutate' description: 'The ID of the pipeline to mutate'
private private
......
...@@ -11,7 +11,7 @@ module Mutations ...@@ -11,7 +11,7 @@ module Mutations
argument :iid, GraphQL::ID_TYPE, argument :iid, GraphQL::ID_TYPE,
required: true, required: true,
description: "The iid of the issue to modify designs for" description: "The IID of the issue to modify designs for"
private private
......
...@@ -10,7 +10,7 @@ module Mutations ...@@ -10,7 +10,7 @@ module Mutations
argument :id, argument :id,
Types::GlobalIDType[Discussion], Types::GlobalIDType[Discussion],
required: true, required: true,
description: 'The global id of the discussion' description: 'The global ID of the discussion'
argument :resolve, argument :resolve,
GraphQL::BOOLEAN_TYPE, GraphQL::BOOLEAN_TYPE,
......
...@@ -11,7 +11,7 @@ module Mutations ...@@ -11,7 +11,7 @@ module Mutations
argument :iid, GraphQL::STRING_TYPE, argument :iid, GraphQL::STRING_TYPE,
required: true, required: true,
description: "The iid of the merge request to mutate" description: "The IID of the merge request to mutate"
field :merge_request, field :merge_request,
Types::MergeRequestType, Types::MergeRequestType,
......
...@@ -20,12 +20,12 @@ module Mutations ...@@ -20,12 +20,12 @@ module Mutations
argument :environment_id, argument :environment_id,
::Types::GlobalIDType[::Environment], ::Types::GlobalIDType[::Environment],
required: false, required: false,
description: 'The global id of the environment to add an annotation to' description: 'The global ID of the environment to add an annotation to'
argument :cluster_id, argument :cluster_id,
::Types::GlobalIDType[::Clusters::Cluster], ::Types::GlobalIDType[::Clusters::Cluster],
required: false, required: false,
description: 'The global id of the cluster to add an annotation to' description: 'The global ID of the cluster to add an annotation to'
argument :starting_at, Types::TimeType, argument :starting_at, Types::TimeType,
required: true, required: true,
......
...@@ -11,7 +11,7 @@ module Mutations ...@@ -11,7 +11,7 @@ module Mutations
argument :noteable_id, argument :noteable_id,
::Types::GlobalIDType[::Noteable], ::Types::GlobalIDType[::Noteable],
required: true, required: true,
description: 'The global id of the resource to add a note to' description: 'The global ID of the resource to add a note to'
argument :body, argument :body,
GraphQL::STRING_TYPE, GraphQL::STRING_TYPE,
......
...@@ -9,7 +9,7 @@ module Mutations ...@@ -9,7 +9,7 @@ module Mutations
argument :discussion_id, argument :discussion_id,
::Types::GlobalIDType[::Discussion], ::Types::GlobalIDType[::Discussion],
required: false, required: false,
description: 'The global id of the discussion this note is in reply to' description: 'The global ID of the discussion this note is in reply to'
private private
......
...@@ -10,7 +10,7 @@ module Mutations ...@@ -10,7 +10,7 @@ module Mutations
argument :id, argument :id,
::Types::GlobalIDType[::Note], ::Types::GlobalIDType[::Note],
required: true, required: true,
description: 'The global id of the note to destroy' description: 'The global ID of the note to destroy'
def resolve(id:) def resolve(id:)
note = authorized_find!(id: id) note = authorized_find!(id: id)
......
...@@ -16,7 +16,7 @@ module Mutations ...@@ -16,7 +16,7 @@ module Mutations
loads: Types::Notes::NoteType, loads: Types::Notes::NoteType,
as: :note, as: :note,
required: true, required: true,
description: 'The global id of the DiffNote to update' description: 'The global ID of the DiffNote to update'
argument :position, argument :position,
Types::Notes::UpdateDiffImagePositionInputType, Types::Notes::UpdateDiffImagePositionInputType,
......
...@@ -11,7 +11,7 @@ module Mutations ...@@ -11,7 +11,7 @@ module Mutations
argument :id, argument :id,
::Types::GlobalIDType[::Note], ::Types::GlobalIDType[::Note],
required: true, required: true,
description: 'The global id of the note to update' description: 'The global ID of the note to update'
def resolve(args) def resolve(args)
note = authorized_find!(id: args[:id]) note = authorized_find!(id: args[:id])
......
...@@ -9,7 +9,7 @@ module Mutations ...@@ -9,7 +9,7 @@ module Mutations
argument :id, ::Types::GlobalIDType[::Snippet], argument :id, ::Types::GlobalIDType[::Snippet],
required: true, required: true,
description: 'The global id of the snippet to destroy' description: 'The global ID of the snippet to destroy'
def resolve(id:) def resolve(id:)
snippet = authorized_find!(id: id) snippet = authorized_find!(id: id)
......
...@@ -7,7 +7,7 @@ module Mutations ...@@ -7,7 +7,7 @@ module Mutations
argument :id, ::Types::GlobalIDType[::Snippet], argument :id, ::Types::GlobalIDType[::Snippet],
required: true, required: true,
description: 'The global id of the snippet to update' description: 'The global ID of the snippet to update'
def resolve(id:) def resolve(id:)
snippet = authorized_find!(id: id) snippet = authorized_find!(id: id)
......
...@@ -9,7 +9,7 @@ module Mutations ...@@ -9,7 +9,7 @@ module Mutations
argument :id, ::Types::GlobalIDType[::Snippet], argument :id, ::Types::GlobalIDType[::Snippet],
required: true, required: true,
description: 'The global id of the snippet to update' description: 'The global ID of the snippet to update'
argument :title, GraphQL::STRING_TYPE, argument :title, GraphQL::STRING_TYPE,
required: false, required: false,
......
...@@ -10,7 +10,7 @@ module Mutations ...@@ -10,7 +10,7 @@ module Mutations
argument :id, argument :id,
::Types::GlobalIDType[::Todo], ::Types::GlobalIDType[::Todo],
required: true, required: true,
description: 'The global id of the todo to mark as done' description: 'The global ID of the todo to mark as done'
field :todo, Types::TodoType, field :todo, Types::TodoType,
null: false, null: false,
......
...@@ -10,7 +10,7 @@ module Mutations ...@@ -10,7 +10,7 @@ module Mutations
argument :id, argument :id,
::Types::GlobalIDType[::Todo], ::Types::GlobalIDType[::Todo],
required: true, required: true,
description: 'The global id of the todo to restore' description: 'The global ID of the todo to restore'
field :todo, Types::TodoType, field :todo, Types::TodoType,
null: false, null: false,
......
...@@ -10,11 +10,11 @@ module Mutations ...@@ -10,11 +10,11 @@ module Mutations
argument :ids, argument :ids,
[::Types::GlobalIDType[::Todo]], [::Types::GlobalIDType[::Todo]],
required: true, required: true,
description: 'The global ids of the todos to restore (a maximum of 50 is supported at once)' description: 'The global IDs of the todos to restore (a maximum of 50 is supported at once)'
field :updated_ids, [::Types::GlobalIDType[Todo]], field :updated_ids, [::Types::GlobalIDType[Todo]],
null: false, null: false,
description: 'The ids of the updated todo items', description: 'The IDs of the updated todo items',
deprecated: { reason: 'Use todos', milestone: '13.2' } deprecated: { reason: 'Use todos', milestone: '13.2' }
field :todos, [::Types::TodoType], field :todos, [::Types::TodoType],
......
...@@ -8,7 +8,7 @@ module Types ...@@ -8,7 +8,7 @@ module Types
argument :jira_account_id, argument :jira_account_id,
GraphQL::STRING_TYPE, GraphQL::STRING_TYPE,
required: true, required: true,
description: 'Jira account id of the user' description: 'Jira account ID of the user'
argument :gitlab_id, argument :gitlab_id,
GraphQL::INT_TYPE, GraphQL::INT_TYPE,
required: false, required: false,
......
...@@ -16,4 +16,5 @@ ...@@ -16,4 +16,5 @@
"cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'), "cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'),
"is_admin": current_user&.admin.to_s, "is_admin": current_user&.admin.to_s,
is_group_page: "true", is_group_page: "true",
"group_path": @group.full_path,
character_error: @character_error.to_s } } character_error: @character_error.to_s } }
...@@ -17,6 +17,6 @@ ...@@ -17,6 +17,6 @@
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'), "garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'), "run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
"cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'), "cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'),
"project_path": @project.full_path,
"is_admin": current_user&.admin.to_s, "is_admin": current_user&.admin.to_s,
character_error: @character_error.to_s } } character_error: @character_error.to_s } }
---
title: Refactor container registry list page to grapqhl
merge_request: 48602
author:
type: changed
---
title: Update template to use codequality 0.85.18-gitlab.1
merge_request: 49034
author:
type: changed
---
title: Ensure job trace endpoint is not called if the current job has not started or the browser is not visible
merge_request: 48516
author:
type: fixed
...@@ -484,6 +484,7 @@ sudo ...@@ -484,6 +484,7 @@ sudo
swimlane swimlane
swimlanes swimlanes
syslog syslog
tanuki
tcpdump tcpdump
Thanos Thanos
Tiller Tiller
......
...@@ -133,7 +133,7 @@ you will need to follow the Consul [outage recovery](#outage-recovery) process. ...@@ -133,7 +133,7 @@ you will need to follow the Consul [outage recovery](#outage-recovery) process.
To be safe, it's recommended that you only restart Consul in one node at a time to To be safe, it's recommended that you only restart Consul in one node at a time to
ensure the cluster remains intact. For larger clusters, it is possible to restart ensure the cluster remains intact. For larger clusters, it is possible to restart
multiple nodes at a time. See the multiple nodes at a time. See the
[Consul consensus document](https://www.consul.io/docs/internals/consensus.html#deployment-table) [Consul consensus document](https://www.consul.io/docs/architecture/consensus#deployment-table)
for how many failures it can tolerate. This will be the number of simultaneous for how many failures it can tolerate. This will be the number of simultaneous
restarts it can sustain. restarts it can sustain.
......
...@@ -167,7 +167,7 @@ When the list of hosts is updated, it might take a while for the old connections ...@@ -167,7 +167,7 @@ When the list of hosts is updated, it might take a while for the old connections
to be terminated. The `disconnect_timeout` setting can be used to enforce an to be terminated. The `disconnect_timeout` setting can be used to enforce an
upper limit on the time it will take to terminate all old database connections. upper limit on the time it will take to terminate all old database connections.
Some nameservers (like [Consul](https://www.consul.io/docs/agent/dns.html#udp-based-dns-queries)) can return a truncated list of hosts when Some nameservers (like [Consul](https://www.consul.io/docs/discovery/dns#udp-based-dns-queries)) can return a truncated list of hosts when
queried over UDP. To overcome this issue, you can use TCP for querying by setting queried over UDP. To overcome this issue, you can use TCP for querying by setting
`use_tcp` to `true`. `use_tcp` to `true`.
......
...@@ -30,7 +30,7 @@ Autogenerated input type of AddAwardEmoji ...@@ -30,7 +30,7 @@ Autogenerated input type of AddAwardEmoji
""" """
input AddAwardEmojiInput { input AddAwardEmojiInput {
""" """
The global id of the awardable resource The global ID of the awardable resource
""" """
awardableId: AwardableID! awardableId: AwardableID!
...@@ -837,7 +837,7 @@ input AlertSetAssigneesInput { ...@@ -837,7 +837,7 @@ input AlertSetAssigneesInput {
clientMutationId: String clientMutationId: String
""" """
The iid of the alert to mutate The IID of the alert to mutate
""" """
iid: String! iid: String!
...@@ -892,7 +892,7 @@ input AlertTodoCreateInput { ...@@ -892,7 +892,7 @@ input AlertTodoCreateInput {
clientMutationId: String clientMutationId: String
""" """
The iid of the alert to mutate The IID of the alert to mutate
""" """
iid: String! iid: String!
...@@ -992,7 +992,7 @@ Autogenerated input type of AwardEmojiAdd ...@@ -992,7 +992,7 @@ Autogenerated input type of AwardEmojiAdd
""" """
input AwardEmojiAddInput { input AwardEmojiAddInput {
""" """
The global id of the awardable resource The global ID of the awardable resource
""" """
awardableId: AwardableID! awardableId: AwardableID!
...@@ -1032,7 +1032,7 @@ Autogenerated input type of AwardEmojiRemove ...@@ -1032,7 +1032,7 @@ Autogenerated input type of AwardEmojiRemove
""" """
input AwardEmojiRemoveInput { input AwardEmojiRemoveInput {
""" """
The global id of the awardable resource The global ID of the awardable resource
""" """
awardableId: AwardableID! awardableId: AwardableID!
...@@ -1072,7 +1072,7 @@ Autogenerated input type of AwardEmojiToggle ...@@ -1072,7 +1072,7 @@ Autogenerated input type of AwardEmojiToggle
""" """
input AwardEmojiToggleInput { input AwardEmojiToggleInput {
""" """
The global id of the awardable resource The global ID of the awardable resource
""" """
awardableId: AwardableID! awardableId: AwardableID!
...@@ -1435,7 +1435,7 @@ type BoardEpic implements CurrentUserTodos & Noteable { ...@@ -1435,7 +1435,7 @@ type BoardEpic implements CurrentUserTodos & Noteable {
iid: ID iid: ID
""" """
Filter epics by iid for autocomplete Filter epics by IID for autocomplete
""" """
iidStartsWith: String iidStartsWith: String
...@@ -2632,7 +2632,7 @@ input ClusterAgentDeleteInput { ...@@ -2632,7 +2632,7 @@ input ClusterAgentDeleteInput {
clientMutationId: String clientMutationId: String
""" """
Global id of the cluster agent that will be deleted Global ID of the cluster agent that will be deleted
""" """
id: ClustersAgentID! id: ClustersAgentID!
} }
...@@ -3691,7 +3691,7 @@ input CreateAlertIssueInput { ...@@ -3691,7 +3691,7 @@ input CreateAlertIssueInput {
clientMutationId: String clientMutationId: String
""" """
The iid of the alert to mutate The IID of the alert to mutate
""" """
iid: String! iid: String!
...@@ -3741,7 +3741,7 @@ input CreateAnnotationInput { ...@@ -3741,7 +3741,7 @@ input CreateAnnotationInput {
clientMutationId: String clientMutationId: String
""" """
The global id of the cluster to add an annotation to The global ID of the cluster to add an annotation to
""" """
clusterId: ClustersClusterID clusterId: ClustersClusterID
...@@ -3761,7 +3761,7 @@ input CreateAnnotationInput { ...@@ -3761,7 +3761,7 @@ input CreateAnnotationInput {
endingAt: Time endingAt: Time
""" """
The global id of the environment to add an annotation to The global ID of the environment to add an annotation to
""" """
environmentId: EnvironmentID environmentId: EnvironmentID
...@@ -4046,7 +4046,7 @@ input CreateDiffNoteInput { ...@@ -4046,7 +4046,7 @@ input CreateDiffNoteInput {
confidential: Boolean confidential: Boolean
""" """
The global id of the resource to add a note to The global ID of the resource to add a note to
""" """
noteableId: NoteableID! noteableId: NoteableID!
...@@ -4176,7 +4176,7 @@ input CreateImageDiffNoteInput { ...@@ -4176,7 +4176,7 @@ input CreateImageDiffNoteInput {
confidential: Boolean confidential: Boolean
""" """
The global id of the resource to add a note to The global ID of the resource to add a note to
""" """
noteableId: NoteableID! noteableId: NoteableID!
...@@ -4401,12 +4401,12 @@ input CreateNoteInput { ...@@ -4401,12 +4401,12 @@ input CreateNoteInput {
confidential: Boolean confidential: Boolean
""" """
The global id of the discussion this note is in reply to The global ID of the discussion this note is in reply to
""" """
discussionId: DiscussionID discussionId: DiscussionID
""" """
The global id of the resource to add a note to The global ID of the resource to add a note to
""" """
noteableId: NoteableID! noteableId: NoteableID!
} }
...@@ -6035,7 +6035,7 @@ input DesignManagementDeleteInput { ...@@ -6035,7 +6035,7 @@ input DesignManagementDeleteInput {
filenames: [String!]! filenames: [String!]!
""" """
The iid of the issue to modify designs for The IID of the issue to modify designs for
""" """
iid: ID! iid: ID!
...@@ -6135,7 +6135,7 @@ input DesignManagementUploadInput { ...@@ -6135,7 +6135,7 @@ input DesignManagementUploadInput {
files: [Upload!]! files: [Upload!]!
""" """
The iid of the issue to modify designs for The IID of the issue to modify designs for
""" """
iid: ID! iid: ID!
...@@ -6445,7 +6445,7 @@ input DestroyNoteInput { ...@@ -6445,7 +6445,7 @@ input DestroyNoteInput {
clientMutationId: String clientMutationId: String
""" """
The global id of the note to destroy The global ID of the note to destroy
""" """
id: NoteID! id: NoteID!
} }
...@@ -6480,7 +6480,7 @@ input DestroySnippetInput { ...@@ -6480,7 +6480,7 @@ input DestroySnippetInput {
clientMutationId: String clientMutationId: String
""" """
The global id of the snippet to destroy The global ID of the snippet to destroy
""" """
id: SnippetID! id: SnippetID!
} }
...@@ -6999,7 +6999,7 @@ input DiscussionToggleResolveInput { ...@@ -6999,7 +6999,7 @@ input DiscussionToggleResolveInput {
clientMutationId: String clientMutationId: String
""" """
The global id of the discussion The global ID of the discussion
""" """
id: DiscussionID! id: DiscussionID!
...@@ -7270,7 +7270,7 @@ type Epic implements CurrentUserTodos & Noteable { ...@@ -7270,7 +7270,7 @@ type Epic implements CurrentUserTodos & Noteable {
iid: ID iid: ID
""" """
Filter epics by iid for autocomplete Filter epics by IID for autocomplete
""" """
iidStartsWith: String iidStartsWith: String
...@@ -7683,12 +7683,12 @@ input EpicAddIssueInput { ...@@ -7683,12 +7683,12 @@ input EpicAddIssueInput {
groupPath: ID! groupPath: ID!
""" """
The iid of the epic to mutate The IID of the epic to mutate
""" """
iid: ID! iid: ID!
""" """
The iid of the issue to be added The IID of the issue to be added
""" """
issueIid: String! issueIid: String!
...@@ -8338,7 +8338,7 @@ input EpicSetSubscriptionInput { ...@@ -8338,7 +8338,7 @@ input EpicSetSubscriptionInput {
groupPath: ID! groupPath: ID!
""" """
The iid of the epic to mutate The IID of the epic to mutate
""" """
iid: ID! iid: ID!
...@@ -8422,12 +8422,12 @@ A node of an epic tree. ...@@ -8422,12 +8422,12 @@ A node of an epic tree.
""" """
input EpicTreeNodeFieldsInputType { input EpicTreeNodeFieldsInputType {
""" """
The id of the epic_issue or issue that the actual epic or issue is switched with The ID of the epic_issue or issue that the actual epic or issue is switched with
""" """
adjacentReferenceId: EpicTreeSortingID adjacentReferenceId: EpicTreeSortingID
""" """
The id of the epic_issue or epic that is being moved The ID of the epic_issue or epic that is being moved
""" """
id: EpicTreeSortingID! id: EpicTreeSortingID!
...@@ -8447,7 +8447,7 @@ Autogenerated input type of EpicTreeReorder ...@@ -8447,7 +8447,7 @@ Autogenerated input type of EpicTreeReorder
""" """
input EpicTreeReorderInput { input EpicTreeReorderInput {
""" """
The id of the base epic of the tree The ID of the base epic of the tree
""" """
baseEpicId: EpicID! baseEpicId: EpicID!
...@@ -8965,7 +8965,7 @@ type Group { ...@@ -8965,7 +8965,7 @@ type Group {
iid: ID iid: ID
""" """
Filter epics by iid for autocomplete Filter epics by IID for autocomplete
""" """
iidStartsWith: String iidStartsWith: String
...@@ -9053,7 +9053,7 @@ type Group { ...@@ -9053,7 +9053,7 @@ type Group {
iid: ID iid: ID
""" """
Filter epics by iid for autocomplete Filter epics by IID for autocomplete
""" """
iidStartsWith: String iidStartsWith: String
...@@ -10176,7 +10176,7 @@ input HttpIntegrationDestroyInput { ...@@ -10176,7 +10176,7 @@ input HttpIntegrationDestroyInput {
clientMutationId: String clientMutationId: String
""" """
The id of the integration to remove The ID of the integration to remove
""" """
id: AlertManagementHttpIntegrationID! id: AlertManagementHttpIntegrationID!
} }
...@@ -10211,7 +10211,7 @@ input HttpIntegrationResetTokenInput { ...@@ -10211,7 +10211,7 @@ input HttpIntegrationResetTokenInput {
clientMutationId: String clientMutationId: String
""" """
The id of the integration to mutate The ID of the integration to mutate
""" """
id: AlertManagementHttpIntegrationID! id: AlertManagementHttpIntegrationID!
} }
...@@ -10251,7 +10251,7 @@ input HttpIntegrationUpdateInput { ...@@ -10251,7 +10251,7 @@ input HttpIntegrationUpdateInput {
clientMutationId: String clientMutationId: String
""" """
The id of the integration to mutate The ID of the integration to mutate
""" """
id: AlertManagementHttpIntegrationID! id: AlertManagementHttpIntegrationID!
...@@ -12186,7 +12186,7 @@ input JiraUsersMappingInputType { ...@@ -12186,7 +12186,7 @@ input JiraUsersMappingInputType {
gitlabId: Int gitlabId: Int
""" """
Jira account id of the user Jira account ID of the user
""" """
jiraAccountId: String! jiraAccountId: String!
} }
...@@ -12379,7 +12379,7 @@ input MarkAsSpamSnippetInput { ...@@ -12379,7 +12379,7 @@ input MarkAsSpamSnippetInput {
clientMutationId: String clientMutationId: String
""" """
The global id of the snippet to update The global ID of the snippet to update
""" """
id: SnippetID! id: SnippetID!
} }
...@@ -13323,7 +13323,7 @@ input MergeRequestSetAssigneesInput { ...@@ -13323,7 +13323,7 @@ input MergeRequestSetAssigneesInput {
clientMutationId: String clientMutationId: String
""" """
The iid of the merge request to mutate The IID of the merge request to mutate
""" """
iid: String! iid: String!
...@@ -13368,7 +13368,7 @@ input MergeRequestSetLabelsInput { ...@@ -13368,7 +13368,7 @@ input MergeRequestSetLabelsInput {
clientMutationId: String clientMutationId: String
""" """
The iid of the merge request to mutate The IID of the merge request to mutate
""" """
iid: String! iid: String!
...@@ -13418,7 +13418,7 @@ input MergeRequestSetLockedInput { ...@@ -13418,7 +13418,7 @@ input MergeRequestSetLockedInput {
clientMutationId: String clientMutationId: String
""" """
The iid of the merge request to mutate The IID of the merge request to mutate
""" """
iid: String! iid: String!
...@@ -13463,7 +13463,7 @@ input MergeRequestSetMilestoneInput { ...@@ -13463,7 +13463,7 @@ input MergeRequestSetMilestoneInput {
clientMutationId: String clientMutationId: String
""" """
The iid of the merge request to mutate The IID of the merge request to mutate
""" """
iid: String! iid: String!
...@@ -13508,7 +13508,7 @@ input MergeRequestSetSubscriptionInput { ...@@ -13508,7 +13508,7 @@ input MergeRequestSetSubscriptionInput {
clientMutationId: String clientMutationId: String
""" """
The iid of the merge request to mutate The IID of the merge request to mutate
""" """
iid: String! iid: String!
...@@ -13553,7 +13553,7 @@ input MergeRequestSetWipInput { ...@@ -13553,7 +13553,7 @@ input MergeRequestSetWipInput {
clientMutationId: String clientMutationId: String
""" """
The iid of the merge request to mutate The IID of the merge request to mutate
""" """
iid: String! iid: String!
...@@ -13699,7 +13699,7 @@ input MergeRequestUpdateInput { ...@@ -13699,7 +13699,7 @@ input MergeRequestUpdateInput {
description: String description: String
""" """
The iid of the merge request to mutate The IID of the merge request to mutate
""" """
iid: String! iid: String!
...@@ -14446,7 +14446,7 @@ input NamespaceIncreaseStorageTemporarilyInput { ...@@ -14446,7 +14446,7 @@ input NamespaceIncreaseStorageTemporarilyInput {
clientMutationId: String clientMutationId: String
""" """
The global id of the namespace to mutate The global ID of the namespace to mutate
""" """
id: NamespaceID! id: NamespaceID!
} }
...@@ -15326,7 +15326,7 @@ input PipelineCancelInput { ...@@ -15326,7 +15326,7 @@ input PipelineCancelInput {
clientMutationId: String clientMutationId: String
""" """
The id of the pipeline to mutate The ID of the pipeline to mutate
""" """
id: CiPipelineID! id: CiPipelineID!
} }
...@@ -15392,7 +15392,7 @@ input PipelineDestroyInput { ...@@ -15392,7 +15392,7 @@ input PipelineDestroyInput {
clientMutationId: String clientMutationId: String
""" """
The id of the pipeline to mutate The ID of the pipeline to mutate
""" """
id: CiPipelineID! id: CiPipelineID!
} }
...@@ -15454,7 +15454,7 @@ input PipelineRetryInput { ...@@ -15454,7 +15454,7 @@ input PipelineRetryInput {
clientMutationId: String clientMutationId: String
""" """
The id of the pipeline to mutate The ID of the pipeline to mutate
""" """
id: CiPipelineID! id: CiPipelineID!
} }
...@@ -17779,7 +17779,7 @@ input PrometheusIntegrationResetTokenInput { ...@@ -17779,7 +17779,7 @@ input PrometheusIntegrationResetTokenInput {
clientMutationId: String clientMutationId: String
""" """
The id of the integration to mutate The ID of the integration to mutate
""" """
id: PrometheusServiceID! id: PrometheusServiceID!
} }
...@@ -17824,7 +17824,7 @@ input PrometheusIntegrationUpdateInput { ...@@ -17824,7 +17824,7 @@ input PrometheusIntegrationUpdateInput {
clientMutationId: String clientMutationId: String
""" """
The id of the integration to mutate The ID of the integration to mutate
""" """
id: PrometheusServiceID! id: PrometheusServiceID!
} }
...@@ -19160,7 +19160,7 @@ Autogenerated input type of RemoveAwardEmoji ...@@ -19160,7 +19160,7 @@ Autogenerated input type of RemoveAwardEmoji
""" """
input RemoveAwardEmojiInput { input RemoveAwardEmojiInput {
""" """
The global id of the awardable resource The global ID of the awardable resource
""" """
awardableId: AwardableID! awardableId: AwardableID!
...@@ -19235,7 +19235,7 @@ input RepositionImageDiffNoteInput { ...@@ -19235,7 +19235,7 @@ input RepositionImageDiffNoteInput {
clientMutationId: String clientMutationId: String
""" """
The global id of the DiffNote to update The global ID of the DiffNote to update
""" """
id: DiffNoteID! id: DiffNoteID!
...@@ -22205,7 +22205,7 @@ input TodoMarkDoneInput { ...@@ -22205,7 +22205,7 @@ input TodoMarkDoneInput {
clientMutationId: String clientMutationId: String
""" """
The global id of the todo to mark as done The global ID of the todo to mark as done
""" """
id: TodoID! id: TodoID!
} }
...@@ -22240,7 +22240,7 @@ input TodoRestoreInput { ...@@ -22240,7 +22240,7 @@ input TodoRestoreInput {
clientMutationId: String clientMutationId: String
""" """
The global id of the todo to restore The global ID of the todo to restore
""" """
id: TodoID! id: TodoID!
} }
...@@ -22255,7 +22255,7 @@ input TodoRestoreManyInput { ...@@ -22255,7 +22255,7 @@ input TodoRestoreManyInput {
clientMutationId: String clientMutationId: String
""" """
The global ids of the todos to restore (a maximum of 50 is supported at once) The global IDs of the todos to restore (a maximum of 50 is supported at once)
""" """
ids: [TodoID!]! ids: [TodoID!]!
} }
...@@ -22280,7 +22280,7 @@ type TodoRestoreManyPayload { ...@@ -22280,7 +22280,7 @@ type TodoRestoreManyPayload {
todos: [Todo!]! todos: [Todo!]!
""" """
The ids of the updated todo items. Deprecated in 13.2: Use todos The IDs of the updated todo items. Deprecated in 13.2: Use todos
""" """
updatedIds: [TodoID!]! @deprecated(reason: "Use todos. Deprecated in 13.2") updatedIds: [TodoID!]! @deprecated(reason: "Use todos. Deprecated in 13.2")
} }
...@@ -22387,7 +22387,7 @@ Autogenerated input type of ToggleAwardEmoji ...@@ -22387,7 +22387,7 @@ Autogenerated input type of ToggleAwardEmoji
""" """
input ToggleAwardEmojiInput { input ToggleAwardEmojiInput {
""" """
The global id of the awardable resource The global ID of the awardable resource
""" """
awardableId: AwardableID! awardableId: AwardableID!
...@@ -22609,7 +22609,7 @@ input UpdateAlertStatusInput { ...@@ -22609,7 +22609,7 @@ input UpdateAlertStatusInput {
clientMutationId: String clientMutationId: String
""" """
The iid of the alert to mutate The IID of the alert to mutate
""" """
iid: String! iid: String!
...@@ -22704,7 +22704,7 @@ Autogenerated input type of UpdateBoard ...@@ -22704,7 +22704,7 @@ Autogenerated input type of UpdateBoard
""" """
input UpdateBoardInput { input UpdateBoardInput {
""" """
The id of user to be assigned to the board The ID of user to be assigned to the board
""" """
assigneeId: UserID assigneeId: UserID
...@@ -22739,7 +22739,7 @@ input UpdateBoardInput { ...@@ -22739,7 +22739,7 @@ input UpdateBoardInput {
labels: [String!] labels: [String!]
""" """
The id of milestone to be assigned to the board The ID of milestone to be assigned to the board
""" """
milestoneId: MilestoneID milestoneId: MilestoneID
...@@ -22991,7 +22991,7 @@ input UpdateEpicInput { ...@@ -22991,7 +22991,7 @@ input UpdateEpicInput {
groupPath: ID! groupPath: ID!
""" """
The iid of the epic to mutate The IID of the epic to mutate
""" """
iid: ID! iid: ID!
...@@ -23056,7 +23056,7 @@ input UpdateImageDiffNoteInput { ...@@ -23056,7 +23056,7 @@ input UpdateImageDiffNoteInput {
clientMutationId: String clientMutationId: String
""" """
The global id of the note to update The global ID of the note to update
""" """
id: NoteID! id: NoteID!
...@@ -23266,7 +23266,7 @@ input UpdateNoteInput { ...@@ -23266,7 +23266,7 @@ input UpdateNoteInput {
confidential: Boolean confidential: Boolean
""" """
The global id of the note to update The global ID of the note to update
""" """
id: NoteID! id: NoteID!
} }
...@@ -23306,7 +23306,7 @@ input UpdateRequirementInput { ...@@ -23306,7 +23306,7 @@ input UpdateRequirementInput {
description: String description: String
""" """
The iid of the requirement to update The IID of the requirement to update
""" """
iid: String! iid: String!
...@@ -23371,7 +23371,7 @@ input UpdateSnippetInput { ...@@ -23371,7 +23371,7 @@ input UpdateSnippetInput {
description: String description: String
""" """
The global id of the snippet to update The global ID of the snippet to update
""" """
id: SnippetID! id: SnippetID!
......
...@@ -105,7 +105,7 @@ ...@@ -105,7 +105,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "awardableId", "name": "awardableId",
"description": "The global id of the awardable resource", "description": "The global ID of the awardable resource",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -2103,7 +2103,7 @@ ...@@ -2103,7 +2103,7 @@
}, },
{ {
"name": "iid", "name": "iid",
"description": "The iid of the alert to mutate", "description": "The IID of the alert to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -2279,7 +2279,7 @@ ...@@ -2279,7 +2279,7 @@
}, },
{ {
"name": "iid", "name": "iid",
"description": "The iid of the alert to mutate", "description": "The IID of the alert to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -2563,7 +2563,7 @@ ...@@ -2563,7 +2563,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "awardableId", "name": "awardableId",
"description": "The global id of the awardable resource", "description": "The global ID of the awardable resource",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -2679,7 +2679,7 @@ ...@@ -2679,7 +2679,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "awardableId", "name": "awardableId",
"description": "The global id of the awardable resource", "description": "The global ID of the awardable resource",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -2795,7 +2795,7 @@ ...@@ -2795,7 +2795,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "awardableId", "name": "awardableId",
"description": "The global id of the awardable resource", "description": "The global ID of the awardable resource",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -3869,7 +3869,7 @@ ...@@ -3869,7 +3869,7 @@
}, },
{ {
"name": "iidStartsWith", "name": "iidStartsWith",
"description": "Filter epics by iid for autocomplete", "description": "Filter epics by IID for autocomplete",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -7113,7 +7113,7 @@ ...@@ -7113,7 +7113,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "Global id of the cluster agent that will be deleted", "description": "Global ID of the cluster agent that will be deleted",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -10073,7 +10073,7 @@ ...@@ -10073,7 +10073,7 @@
}, },
{ {
"name": "iid", "name": "iid",
"description": "The iid of the alert to mutate", "description": "The IID of the alert to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -10203,7 +10203,7 @@ ...@@ -10203,7 +10203,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "environmentId", "name": "environmentId",
"description": "The global id of the environment to add an annotation to", "description": "The global ID of the environment to add an annotation to",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "EnvironmentID", "name": "EnvironmentID",
...@@ -10213,7 +10213,7 @@ ...@@ -10213,7 +10213,7 @@
}, },
{ {
"name": "clusterId", "name": "clusterId",
"description": "The global id of the cluster to add an annotation to", "description": "The global ID of the cluster to add an annotation to",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "ClustersClusterID", "name": "ClustersClusterID",
...@@ -11025,7 +11025,7 @@ ...@@ -11025,7 +11025,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "noteableId", "name": "noteableId",
"description": "The global id of the resource to add a note to", "description": "The global ID of the resource to add a note to",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -11373,7 +11373,7 @@ ...@@ -11373,7 +11373,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "noteableId", "name": "noteableId",
"description": "The global id of the resource to add a note to", "description": "The global ID of the resource to add a note to",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -11951,7 +11951,7 @@ ...@@ -11951,7 +11951,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "noteableId", "name": "noteableId",
"description": "The global id of the resource to add a note to", "description": "The global ID of the resource to add a note to",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -11989,7 +11989,7 @@ ...@@ -11989,7 +11989,7 @@
}, },
{ {
"name": "discussionId", "name": "discussionId",
"description": "The global id of the discussion this note is in reply to", "description": "The global ID of the discussion this note is in reply to",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "DiscussionID", "name": "DiscussionID",
...@@ -16611,7 +16611,7 @@ ...@@ -16611,7 +16611,7 @@
}, },
{ {
"name": "iid", "name": "iid",
"description": "The iid of the issue to modify designs for", "description": "The IID of the issue to modify designs for",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -16891,7 +16891,7 @@ ...@@ -16891,7 +16891,7 @@
}, },
{ {
"name": "iid", "name": "iid",
"description": "The iid of the issue to modify designs for", "description": "The IID of the issue to modify designs for",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -17766,7 +17766,7 @@ ...@@ -17766,7 +17766,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The global id of the note to destroy", "description": "The global ID of the note to destroy",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -17868,7 +17868,7 @@ ...@@ -17868,7 +17868,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The global id of the snippet to destroy", "description": "The global ID of the snippet to destroy",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -19477,7 +19477,7 @@ ...@@ -19477,7 +19477,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The global id of the discussion", "description": "The global ID of the discussion",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -20366,7 +20366,7 @@ ...@@ -20366,7 +20366,7 @@
}, },
{ {
"name": "iidStartsWith", "name": "iidStartsWith",
"description": "Filter epics by iid for autocomplete", "description": "Filter epics by IID for autocomplete",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -21375,7 +21375,7 @@ ...@@ -21375,7 +21375,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "iid", "name": "iid",
"description": "The iid of the epic to mutate", "description": "The IID of the epic to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -21417,7 +21417,7 @@ ...@@ -21417,7 +21417,7 @@
}, },
{ {
"name": "issueIid", "name": "issueIid",
"description": "The iid of the issue to be added", "description": "The IID of the issue to be added",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -23282,7 +23282,7 @@ ...@@ -23282,7 +23282,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "iid", "name": "iid",
"description": "The iid of the epic to mutate", "description": "The IID of the epic to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -23499,7 +23499,7 @@ ...@@ -23499,7 +23499,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The id of the epic_issue or epic that is being moved", "description": "The ID of the epic_issue or epic that is being moved",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -23513,7 +23513,7 @@ ...@@ -23513,7 +23513,7 @@
}, },
{ {
"name": "adjacentReferenceId", "name": "adjacentReferenceId",
"description": "The id of the epic_issue or issue that the actual epic or issue is switched with", "description": "The ID of the epic_issue or issue that the actual epic or issue is switched with",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "EpicTreeSortingID", "name": "EpicTreeSortingID",
...@@ -23554,7 +23554,7 @@ ...@@ -23554,7 +23554,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "baseEpicId", "name": "baseEpicId",
"description": "The id of the base epic of the tree", "description": "The ID of the base epic of the tree",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -24959,7 +24959,7 @@ ...@@ -24959,7 +24959,7 @@
}, },
{ {
"name": "iidStartsWith", "name": "iidStartsWith",
"description": "Filter epics by iid for autocomplete", "description": "Filter epics by IID for autocomplete",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -25118,7 +25118,7 @@ ...@@ -25118,7 +25118,7 @@
}, },
{ {
"name": "iidStartsWith", "name": "iidStartsWith",
"description": "Filter epics by iid for autocomplete", "description": "Filter epics by IID for autocomplete",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -27858,7 +27858,7 @@ ...@@ -27858,7 +27858,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The id of the integration to remove", "description": "The ID of the integration to remove",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -27960,7 +27960,7 @@ ...@@ -27960,7 +27960,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The id of the integration to mutate", "description": "The ID of the integration to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -28062,7 +28062,7 @@ ...@@ -28062,7 +28062,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The id of the integration to mutate", "description": "The ID of the integration to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -33493,7 +33493,7 @@ ...@@ -33493,7 +33493,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "jiraAccountId", "name": "jiraAccountId",
"description": "Jira account id of the user", "description": "Jira account ID of the user",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -34135,7 +34135,7 @@ ...@@ -34135,7 +34135,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The global id of the snippet to update", "description": "The global ID of the snippet to update",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -36810,7 +36810,7 @@ ...@@ -36810,7 +36810,7 @@
}, },
{ {
"name": "iid", "name": "iid",
"description": "The iid of the merge request to mutate", "description": "The IID of the merge request to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -36958,7 +36958,7 @@ ...@@ -36958,7 +36958,7 @@
}, },
{ {
"name": "iid", "name": "iid",
"description": "The iid of the merge request to mutate", "description": "The IID of the merge request to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -37106,7 +37106,7 @@ ...@@ -37106,7 +37106,7 @@
}, },
{ {
"name": "iid", "name": "iid",
"description": "The iid of the merge request to mutate", "description": "The IID of the merge request to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -37236,7 +37236,7 @@ ...@@ -37236,7 +37236,7 @@
}, },
{ {
"name": "iid", "name": "iid",
"description": "The iid of the merge request to mutate", "description": "The IID of the merge request to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -37362,7 +37362,7 @@ ...@@ -37362,7 +37362,7 @@
}, },
{ {
"name": "iid", "name": "iid",
"description": "The iid of the merge request to mutate", "description": "The IID of the merge request to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -37492,7 +37492,7 @@ ...@@ -37492,7 +37492,7 @@
}, },
{ {
"name": "iid", "name": "iid",
"description": "The iid of the merge request to mutate", "description": "The IID of the merge request to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -37770,7 +37770,7 @@ ...@@ -37770,7 +37770,7 @@
}, },
{ {
"name": "iid", "name": "iid",
"description": "The iid of the merge request to mutate", "description": "The IID of the merge request to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -42934,7 +42934,7 @@ ...@@ -42934,7 +42934,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The global id of the namespace to mutate", "description": "The global ID of the namespace to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -45453,7 +45453,7 @@ ...@@ -45453,7 +45453,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The id of the pipeline to mutate", "description": "The ID of the pipeline to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -45685,7 +45685,7 @@ ...@@ -45685,7 +45685,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The id of the pipeline to mutate", "description": "The ID of the pipeline to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -45885,7 +45885,7 @@ ...@@ -45885,7 +45885,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The id of the pipeline to mutate", "description": "The ID of the pipeline to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -51943,7 +51943,7 @@ ...@@ -51943,7 +51943,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The id of the integration to mutate", "description": "The ID of the integration to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -52045,7 +52045,7 @@ ...@@ -52045,7 +52045,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The id of the integration to mutate", "description": "The ID of the integration to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -55492,7 +55492,7 @@ ...@@ -55492,7 +55492,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "awardableId", "name": "awardableId",
"description": "The global id of the awardable resource", "description": "The global ID of the awardable resource",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -55696,7 +55696,7 @@ ...@@ -55696,7 +55696,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The global id of the DiffNote to update", "description": "The global ID of the DiffNote to update",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -64791,7 +64791,7 @@ ...@@ -64791,7 +64791,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The global id of the todo to mark as done", "description": "The global ID of the todo to mark as done",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -64897,7 +64897,7 @@ ...@@ -64897,7 +64897,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The global id of the todo to restore", "description": "The global ID of the todo to restore",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -64932,7 +64932,7 @@ ...@@ -64932,7 +64932,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "ids", "name": "ids",
"description": "The global ids of the todos to restore (a maximum of 50 is supported at once)", "description": "The global IDs of the todos to restore (a maximum of 50 is supported at once)",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -65040,7 +65040,7 @@ ...@@ -65040,7 +65040,7 @@
}, },
{ {
"name": "updatedIds", "name": "updatedIds",
"description": "The ids of the updated todo items. Deprecated in 13.2: Use todos", "description": "The IDs of the updated todo items. Deprecated in 13.2: Use todos",
"args": [ "args": [
], ],
...@@ -65357,7 +65357,7 @@ ...@@ -65357,7 +65357,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "awardableId", "name": "awardableId",
"description": "The global id of the awardable resource", "description": "The global ID of the awardable resource",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -66001,7 +66001,7 @@ ...@@ -66001,7 +66001,7 @@
}, },
{ {
"name": "iid", "name": "iid",
"description": "The iid of the alert to mutate", "description": "The IID of the alert to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -66319,7 +66319,7 @@ ...@@ -66319,7 +66319,7 @@
}, },
{ {
"name": "assigneeId", "name": "assigneeId",
"description": "The id of user to be assigned to the board", "description": "The ID of user to be assigned to the board",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "UserID", "name": "UserID",
...@@ -66329,7 +66329,7 @@ ...@@ -66329,7 +66329,7 @@
}, },
{ {
"name": "milestoneId", "name": "milestoneId",
"description": "The id of milestone to be assigned to the board", "description": "The ID of milestone to be assigned to the board",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "MilestoneID", "name": "MilestoneID",
...@@ -66942,7 +66942,7 @@ ...@@ -66942,7 +66942,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "iid", "name": "iid",
"description": "The iid of the epic to mutate", "description": "The IID of the epic to mutate",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -67174,7 +67174,7 @@ ...@@ -67174,7 +67174,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The global id of the note to update", "description": "The global ID of the note to update",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -67704,7 +67704,7 @@ ...@@ -67704,7 +67704,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The global id of the note to update", "description": "The global ID of the note to update",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -67870,7 +67870,7 @@ ...@@ -67870,7 +67870,7 @@
}, },
{ {
"name": "iid", "name": "iid",
"description": "The iid of the requirement to update", "description": "The IID of the requirement to update",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -67982,7 +67982,7 @@ ...@@ -67982,7 +67982,7 @@
"inputFields": [ "inputFields": [
{ {
"name": "id", "name": "id",
"description": "The global id of the snippet to update", "description": "The global ID of the snippet to update",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -31,7 +31,7 @@ The following outline re-uses the [maturity metric](https://about.gitlab.com/dir ...@@ -31,7 +31,7 @@ The following outline re-uses the [maturity metric](https://about.gitlab.com/dir
The initial step for integrating a new component with GitLab starts with creating a [Feature proposal in the issue tracker](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Feature%20proposal). The initial step for integrating a new component with GitLab starts with creating a [Feature proposal in the issue tracker](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Feature%20proposal).
Identify the [product category](https://about.gitlab.com/handbook/product/categories/) the component falls under and assign the Engineering Manager and Product Manager responsible for that category. Identify the [product category](https://about.gitlab.com/handbook/product/product-categories/) the component falls under and assign the Engineering Manager and Product Manager responsible for that category.
The general steps for getting any GitLab feature from proposal to release can be found in the [Product development flow](https://about.gitlab.com/handbook/product-development-flow/). The general steps for getting any GitLab feature from proposal to release can be found in the [Product development flow](https://about.gitlab.com/handbook/product-development-flow/).
......
...@@ -33,7 +33,7 @@ The agent can help you perform tasks like these: ...@@ -33,7 +33,7 @@ The agent can help you perform tasks like these:
## Architecture of the Kubernetes Agent ## Architecture of the Kubernetes Agent
The GitLab Kubernetes Agent and the GitLab Kubernetes Agent Server use The GitLab Kubernetes Agent and the GitLab Kubernetes Agent Server use
[bidirectional streaming](https://grpc.io/docs/guides/concepts/#bidirectional-streaming-rpc) [bidirectional streaming](https://grpc.io/docs/what-is-grpc/core-concepts/#bidirectional-streaming-rpc)
to allow the connection acceptor (the gRPC server, GitLab Kubernetes Agent Server) to to allow the connection acceptor (the gRPC server, GitLab Kubernetes Agent Server) to
act as a client. The connection acceptor sends requests as gRPC replies. The client-server act as a client. The connection acceptor sends requests as gRPC replies. The client-server
relationship is inverted because the connection must be initiated from inside the relationship is inverted because the connection must be initiated from inside the
......
...@@ -47,7 +47,7 @@ the `author` field. GitLab team members **should not**. ...@@ -47,7 +47,7 @@ the `author` field. GitLab team members **should not**.
- Any user-facing change **must** have a changelog entry. This includes both visual changes (regardless of how minor), and changes to the rendered DOM which impact how a screen reader may announce the content. - Any user-facing change **must** have a changelog entry. This includes both visual changes (regardless of how minor), and changes to the rendered DOM which impact how a screen reader may announce the content.
- Any client-facing change to our REST and GraphQL APIs **must** have a changelog entry. - Any client-facing change to our REST and GraphQL APIs **must** have a changelog entry.
- Performance improvements **should** have a changelog entry. - Performance improvements **should** have a changelog entry.
- Changes that need to be documented in the Product Analytics [Event Dictionary](https://about.gitlab.com/handbook/product/product-analytics-guide#event-dictionary) - Changes that need to be documented in the Product Analytics [Event Dictionary](https://about.gitlab.com/handbook/product/product-analytics-guide/#event-dictionary)
also require a changelog entry. also require a changelog entry.
- _Any_ contribution from a community member, no matter how small, **may** have - _Any_ contribution from a community member, no matter how small, **may** have
a changelog entry regardless of these guidelines if the contributor wants one. a changelog entry regardless of these guidelines if the contributor wants one.
...@@ -55,7 +55,7 @@ the `author` field. GitLab team members **should not**. ...@@ -55,7 +55,7 @@ the `author` field. GitLab team members **should not**.
- Any docs-only changes **should not** have a changelog entry. - Any docs-only changes **should not** have a changelog entry.
- Any change behind a disabled feature flag **should not** have a changelog entry. - Any change behind a disabled feature flag **should not** have a changelog entry.
- Any change behind an enabled feature flag **should** have a changelog entry. - Any change behind an enabled feature flag **should** have a changelog entry.
- Any change that adds new usage data metrics and changes that needs to be documented in Product Analytics [Event Dictionary](https://about.gitlab.com/handbook/product/product-analytics-guide#event-dictionary) **should** have a changelog entry. - Any change that adds new usage data metrics and changes that needs to be documented in Product Analytics [Event Dictionary](https://about.gitlab.com/handbook/product/product-analytics-guide/#event-dictionary) **should** have a changelog entry.
- A change that adds snowplow events **should** have a changelog entry - - A change that adds snowplow events **should** have a changelog entry -
- A change that [removes a feature flag](feature_flags/development.md) **should** have a changelog entry - - A change that [removes a feature flag](feature_flags/development.md) **should** have a changelog entry -
only if the feature flag did not default to true already. only if the feature flag did not default to true already.
......
...@@ -107,7 +107,7 @@ Rendered example: ...@@ -107,7 +107,7 @@ Rendered example:
## cURL Examples ## cURL Examples
The following sections include a set of [cURL](https://curl.haxx.se) examples The following sections include a set of [cURL](https://curl.se/) examples
you can use in the API documentation. you can use in the API documentation.
CAUTION: **Caution:** CAUTION: **Caution:**
......
...@@ -533,7 +533,9 @@ tenses, words, and phrases: ...@@ -533,7 +533,9 @@ tenses, words, and phrases:
content is accessible to more readers. content is accessible to more readers.
- Don't write in the first person singular. - Don't write in the first person singular.
(Tested in [`FirstPerson.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/FirstPerson.yml).) (Tested in [`FirstPerson.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/FirstPerson.yml).)
<!-- vale gitlab.FirstPerson = NO -->
- Instead of _I_ or _me_, use _we_, _you_, _us_, or _one_. - Instead of _I_ or _me_, use _we_, _you_, _us_, or _one_.
<!-- vale gitlab.FirstPerson = YES -->
- When possible, stay user focused by writing in the second person (_you_ or - When possible, stay user focused by writing in the second person (_you_ or
the imperative). the imperative).
- Don't overuse "that". In many cases, you can remove "that" from a sentence - Don't overuse "that". In many cases, you can remove "that" from a sentence
...@@ -795,6 +797,8 @@ Items nested in lists should always align with the first character of the list ...@@ -795,6 +797,8 @@ Items nested in lists should always align with the first character of the list
item. In unordered lists (using `-`), this means two spaces for each level of item. In unordered lists (using `-`), this means two spaces for each level of
indentation: indentation:
<!-- vale off -->
````markdown ````markdown
- Unordered list item 1 - Unordered list item 1
...@@ -816,8 +820,12 @@ indentation: ...@@ -816,8 +820,12 @@ indentation:
![an image that will nest inside list item 4](image.png) ![an image that will nest inside list item 4](image.png)
```` ````
<!-- vale on -->
For ordered lists, use three spaces for each level of indentation: For ordered lists, use three spaces for each level of indentation:
<!-- vale off -->
````markdown ````markdown
1. Ordered list item 1 1. Ordered list item 1
...@@ -839,6 +847,8 @@ For ordered lists, use three spaces for each level of indentation: ...@@ -839,6 +847,8 @@ For ordered lists, use three spaces for each level of indentation:
![an image that will nest inside list item 4](image.png) ![an image that will nest inside list item 4](image.png)
```` ````
<!-- vale on -->
You can nest full lists inside other lists using the same rules as above. If you You can nest full lists inside other lists using the same rules as above. If you
want to mix types, that's also possible, if you don't mix items at the same want to mix types, that's also possible, if you don't mix items at the same
level: level:
...@@ -904,7 +914,7 @@ Valid for Markdown content only, not for front matter entries: ...@@ -904,7 +914,7 @@ Valid for Markdown content only, not for front matter entries:
- Standard quotes: double quotes (`"`). Example: "This is wrapped in double - Standard quotes: double quotes (`"`). Example: "This is wrapped in double
quotes". quotes".
- Quote inside a quote: double quotes (`"`) wrap single quotes (`'`). Example: - Quote inside a quote: double quotes (`"`) wrap single quotes (`'`). Example:
"I am 'quoting' something in a quote". "This sentence 'quotes' something in a quote".
For other punctuation rules, refer to the For other punctuation rules, refer to the
[GitLab UX guide](https://design.gitlab.com/content/punctuation/). [GitLab UX guide](https://design.gitlab.com/content/punctuation/).
...@@ -1367,6 +1377,8 @@ hidden on the documentation site, but is displayed by `/help`. ...@@ -1367,6 +1377,8 @@ hidden on the documentation site, but is displayed by `/help`.
- For regular fenced code blocks, always use a highlighting class corresponding to - For regular fenced code blocks, always use a highlighting class corresponding to
the language for better readability. Examples: the language for better readability. Examples:
<!-- vale off -->
````markdown ````markdown
```ruby ```ruby
Ruby code Ruby code
...@@ -1385,6 +1397,8 @@ hidden on the documentation site, but is displayed by `/help`. ...@@ -1385,6 +1397,8 @@ hidden on the documentation site, but is displayed by `/help`.
``` ```
```` ````
<!-- vale on -->
Syntax highlighting is required for fenced code blocks added to the GitLab Syntax highlighting is required for fenced code blocks added to the GitLab
documentation. Refer to the following table for the most common language classes, documentation. Refer to the following table for the most common language classes,
or check the [complete list](https://github.com/rouge-ruby/rouge/wiki/List-of-supported-languages-and-lexers) or check the [complete list](https://github.com/rouge-ruby/rouge/wiki/List-of-supported-languages-and-lexers)
...@@ -1771,8 +1785,7 @@ for use in GitLab X.X, and is planned for [removal](link-to-issue) in GitLab X.X ...@@ -1771,8 +1785,7 @@ for use in GitLab X.X, and is planned for [removal](link-to-issue) in GitLab X.X
``` ```
After the feature or product is officially deprecated and removed, remove After the feature or product is officially deprecated and removed, remove
its information from the GitLab documentation based on its information from the GitLab documentation.
the GitLab version where it's actually removed.
### Versions in the past or future ### Versions in the past or future
...@@ -1926,6 +1939,8 @@ Configuration settings include: ...@@ -1926,6 +1939,8 @@ Configuration settings include:
When you document a list of steps, it may entail editing the configuration file When you document a list of steps, it may entail editing the configuration file
and reconfiguring or restarting GitLab. In that case, use these styles: and reconfiguring or restarting GitLab. In that case, use these styles:
<!-- vale off -->
````markdown ````markdown
**For Omnibus installations** **For Omnibus installations**
...@@ -1953,6 +1968,8 @@ and reconfiguring or restarting GitLab. In that case, use these styles: ...@@ -1953,6 +1968,8 @@ and reconfiguring or restarting GitLab. In that case, use these styles:
GitLab for the changes to take effect. GitLab for the changes to take effect.
```` ````
<!-- vale on -->
In this case: In this case:
- Before each step list the installation method is declared in bold. - Before each step list the installation method is declared in bold.
......
...@@ -15,7 +15,7 @@ the [Elasticsearch integration documentation](../integration/elasticsearch.md#en ...@@ -15,7 +15,7 @@ the [Elasticsearch integration documentation](../integration/elasticsearch.md#en
In June 2019, Mario de la Ossa hosted a Deep Dive (GitLab team members only: `https://gitlab.com/gitlab-org/create-stage/issues/1`) on GitLab's [Elasticsearch integration](../integration/elasticsearch.md) to share his domain specific knowledge with anyone who may work in this part of the code base in the future. You can find the [recording on YouTube](https://www.youtube.com/watch?v=vrvl-tN2EaA), and the slides on [Google Slides](https://docs.google.com/presentation/d/1H-pCzI_LNrgrL5pJAIQgvLX8Ji0-jIKOg1QeJQzChug/edit) and in [PDF](https://gitlab.com/gitlab-org/create-stage/uploads/c5aa32b6b07476fa8b597004899ec538/Elasticsearch_Deep_Dive.pdf). Everything covered in this deep dive was accurate as of GitLab 12.0, and while specific details may have changed since then, it should still serve as a good introduction. In June 2019, Mario de la Ossa hosted a Deep Dive (GitLab team members only: `https://gitlab.com/gitlab-org/create-stage/issues/1`) on GitLab's [Elasticsearch integration](../integration/elasticsearch.md) to share his domain specific knowledge with anyone who may work in this part of the code base in the future. You can find the [recording on YouTube](https://www.youtube.com/watch?v=vrvl-tN2EaA), and the slides on [Google Slides](https://docs.google.com/presentation/d/1H-pCzI_LNrgrL5pJAIQgvLX8Ji0-jIKOg1QeJQzChug/edit) and in [PDF](https://gitlab.com/gitlab-org/create-stage/uploads/c5aa32b6b07476fa8b597004899ec538/Elasticsearch_Deep_Dive.pdf). Everything covered in this deep dive was accurate as of GitLab 12.0, and while specific details may have changed since then, it should still serve as a good introduction.
In August 2020, a second Deep Dive was hosted, focusing on [GitLab's specific architecture for multi-indices support](#zero-downtime-reindexing-with-multiple-indices). The [recording on YouTube](https://www.youtube.com/watch?v=0WdPR9oB2fg) and the [slides](https://lulalala.gitlab.io/gitlab-elasticsearch-deepdive) are available. Everything covered in this deep dive was accurate as of GitLab 13.3. In August 2020, a second Deep Dive was hosted, focusing on [GitLab's specific architecture for multi-indices support](#zero-downtime-reindexing-with-multiple-indices). The [recording on YouTube](https://www.youtube.com/watch?v=0WdPR9oB2fg) and the [slides](https://lulalala.gitlab.io/gitlab-elasticsearch-deepdive/) are available. Everything covered in this deep dive was accurate as of GitLab 13.3.
## Supported Versions ## Supported Versions
......
...@@ -279,6 +279,17 @@ Subsequent calls to this method for the same experiment and the same user have n ...@@ -279,6 +279,17 @@ Subsequent calls to this method for the same experiment and the same user have n
Note that this data is completely separate from the [events tracking data](#implement-the-tracking-events). They are not linked together in any way. Note that this data is completely separate from the [events tracking data](#implement-the-tracking-events). They are not linked together in any way.
#### Add context
You can add arbitrary context data in a hash which gets stored as part of the experiment user record.
This data can then be used by data analytics dashboards.
```ruby
before_action do
record_experiment_user(:signup_flow, foo: 42)
end
```
### Record experiment conversion event ### Record experiment conversion event
Along with the tracking of backend and frontend events and the [recording of experiment participants](#record-experiment-user), we can also record when a user performs the desired conversion event action. For example: Along with the tracking of backend and frontend events and the [recording of experiment participants](#record-experiment-user), we can also record when a user performs the desired conversion event action. For example:
......
...@@ -411,7 +411,7 @@ handleClick() { ...@@ -411,7 +411,7 @@ handleClick() {
### Working with pagination ### Working with pagination
GitLab's GraphQL API uses [Relay-style cursor pagination](https://www.apollographql.com/docs/react/data/pagination/#cursor-based) GitLab's GraphQL API uses [Relay-style cursor pagination](https://www.apollographql.com/docs/react/pagination/overview/#cursor-based)
for connection types. This means a "cursor" is used to keep track of where in the data for connection types. This means a "cursor" is used to keep track of where in the data
set the next items should be fetched from. [GraphQL Ruby Connection Concepts](https://graphql-ruby.org/pagination/connection_concepts.html) set the next items should be fetched from. [GraphQL Ruby Connection Concepts](https://graphql-ruby.org/pagination/connection_concepts.html)
is a good overview and introduction to connections. is a good overview and introduction to connections.
......
...@@ -50,7 +50,7 @@ change feature flags or you do not [have access](#access). ...@@ -50,7 +50,7 @@ change feature flags or you do not [have access](#access).
### Enabling a feature for preproduction testing ### Enabling a feature for preproduction testing
As a first step in a feature rollout, you should enable the feature on <https://staging.gitlab.com> As a first step in a feature rollout, you should enable the feature on <https://about.staging.gitlab.com>
and <https://dev.gitlab.org>. and <https://dev.gitlab.org>.
These two environments have different scopes. These two environments have different scopes.
......
...@@ -100,7 +100,7 @@ To propose additions to the glossary please ...@@ -100,7 +100,7 @@ To propose additions to the glossary please
### Inclusive language in French ### Inclusive language in French
<!-- vale gitlab.Spelling = NO --> <!-- vale gitlab.Spelling = NO -->
In French, the "écriture inclusive" is now over (see on [Legifrance](https://www.legifrance.gouv.fr/affichTexte.do?cidTexte=JORFTEXT000036068906&categorieLien=id)). In French, the "écriture inclusive" is now over (see on [Legifrance](https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000036068906/)).
So, to include both genders, write “Utilisateurs et utilisatrices” instead of “Utilisateur·rice·s”. So, to include both genders, write “Utilisateurs et utilisatrices” instead of “Utilisateur·rice·s”.
When space is missing, the male gender should be used alone. When space is missing, the male gender should be used alone.
<!-- vale gitlab.Spelling = YES --> <!-- vale gitlab.Spelling = YES -->
...@@ -42,7 +42,7 @@ In forms we should use the `for` attribute in the label statement: ...@@ -42,7 +42,7 @@ In forms we should use the `for` attribute in the label statement:
## Testing ## Testing
1. On MacOS you can use [VoiceOver](https://www.apple.com/accessibility/mac/vision/) by pressing `cmd+F5`. 1. On MacOS you can use [VoiceOver](http://www.apple.com/accessibility/vision/) by pressing `cmd+F5`.
1. On Windows you can use [Narrator](https://www.microsoft.com/en-us/accessibility/windows) by pressing Windows logo key + Control + Enter. 1. On Windows you can use [Narrator](https://www.microsoft.com/en-us/accessibility/windows) by pressing Windows logo key + Control + Enter.
## Online resources ## Online resources
......
...@@ -488,9 +488,9 @@ We have the following recommendations for [Adding new events](#adding-new-events ...@@ -488,9 +488,9 @@ We have the following recommendations for [Adding new events](#adding-new-events
Events are tracked behind [feature flags](../feature_flags/index.md) due to concerns for Redis performance and scalability. Events are tracked behind [feature flags](../feature_flags/index.md) due to concerns for Redis performance and scalability.
For a full list of events and coresponding feature flags see, [known_events](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters/known_events/) files. For a full list of events and corresponding feature flags see, [known_events](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters/known_events/) files.
To enable or disable tracking for specific event within <https://gitlab.com> or <https://staging.gitlab.com>, run commands such as the following to To enable or disable tracking for specific event within <https://gitlab.com> or <https://about.staging.gitlab.com>, run commands such as the following to
[enable or disable the corresponding feature](../feature_flags/index.md). [enable or disable the corresponding feature](../feature_flags/index.md).
```shell ```shell
...@@ -666,7 +666,7 @@ We also use `#database-lab` and [explain.depesz.com](https://explain.depesz.com/ ...@@ -666,7 +666,7 @@ We also use `#database-lab` and [explain.depesz.com](https://explain.depesz.com/
### 5. Add the metric definition ### 5. Add the metric definition
When adding, changing, or updating metrics, please update the [Event Dictionary's **Usage Ping** table](https://about.gitlab.com/handbook/product/product-analytics-guide#event-dictionary). When adding, changing, or updating metrics, please update the [Event Dictionary's **Usage Ping** table](https://about.gitlab.com/handbook/product/product-analytics-guide/#event-dictionary).
### 6. Add new metric to Versions Application ### 6. Add new metric to Versions Application
......
...@@ -192,8 +192,8 @@ Following you'll find some general common practices you will find as part of our ...@@ -192,8 +192,8 @@ Following you'll find some general common practices you will find as part of our
When it comes to querying DOM elements in your tests, it is best to uniquely and semantically target When it comes to querying DOM elements in your tests, it is best to uniquely and semantically target
the element. the element.
Preferentially, this is done by targeting what the user actually sees using [DOM Testing Library](https://testing-library.com/docs/dom-testing-library/intro). Preferentially, this is done by targeting what the user actually sees using [DOM Testing Library](https://testing-library.com/docs/dom-testing-library/intro/).
When selecting by text it is best to use [`getByRole` or `findByRole`](https://testing-library.com/docs/dom-testing-library/api-queries#byrole) When selecting by text it is best to use [`getByRole` or `findByRole`](https://testing-library.com/docs/dom-testing-library/api-queries/#byrole)
as these enforce accessibility best practices as well. The examples below demonstrate the order of preference. as these enforce accessibility best practices as well. The examples below demonstrate the order of preference.
When writing Vue component unit tests, it can be wise to query children by component, so that the unit test can focus on comprehensive value coverage When writing Vue component unit tests, it can be wise to query children by component, so that the unit test can focus on comprehensive value coverage
......
--- ---
redirect_to: 'https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/' redirect_to: 'https://about.gitlab.com/handbook/marketing/strategic-marketing/roles-personas/'
--- ---
This document was moved to [another location](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/). This document was moved to [another location](https://about.gitlab.com/handbook/marketing/strategic-marketing/roles-personas/).
<!-- This redirect file can be deleted after February 1, 2021. --> <!-- This redirect file can be deleted after February 1, 2021. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> <!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
...@@ -49,7 +49,7 @@ prompt, terminal, and command line) of your preference. Here are some suggestion ...@@ -49,7 +49,7 @@ prompt, terminal, and command line) of your preference. Here are some suggestion
- For macOS users: - For macOS users:
- Built-in: [Terminal](https://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line). Press <kbd>⌘ command</kbd> + <kbd>space</kbd> and type "terminal" to find it. - Built-in: [Terminal](https://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line). Press <kbd>⌘ command</kbd> + <kbd>space</kbd> and type "terminal" to find it.
- [iTerm2](https://www.iterm2.com/), which you can integrate with [zsh](https://git-scm.com/book/id/v2/Appendix-A%3A-Git-in-Other-Environments-Git-in-Zsh) and [oh my zsh](https://ohmyz.sh/) for color highlighting, among other handy features for Git users. - [iTerm2](https://iterm2.com/), which you can integrate with [zsh](https://git-scm.com/book/id/v2/Appendix-A%3A-Git-in-Other-Environments-Git-in-Zsh) and [oh my zsh](https://ohmyz.sh/) for color highlighting, among other handy features for Git users.
- For Windows users: - For Windows users:
- Built-in: **cmd**. Click the search icon on the bottom navbar on Windows and type "cmd" to find it. - Built-in: **cmd**. Click the search icon on the bottom navbar on Windows and type "cmd" to find it.
- [PowerShell](https://docs.microsoft.com/en-us/powershell/scripting/windows-powershell/install/installing-windows-powershell?view=powershell-7): a Windows "powered up" shell, from which you can execute a greater number of commands. - [PowerShell](https://docs.microsoft.com/en-us/powershell/scripting/windows-powershell/install/installing-windows-powershell?view=powershell-7): a Windows "powered up" shell, from which you can execute a greater number of commands.
......
...@@ -27,7 +27,7 @@ For a real use case, read the blog post [Continuous integration: From Jenkins to ...@@ -27,7 +27,7 @@ For a real use case, read the blog post [Continuous integration: From Jenkins to
Moving from a traditional CI plug-in to a single application for the entire software development Moving from a traditional CI plug-in to a single application for the entire software development
life cycle can decrease hours spent on maintaining toolchains by 10% or more. For more details, see life cycle can decrease hours spent on maintaining toolchains by 10% or more. For more details, see
the ['GitLab vs. Jenkins' comparison page](https://about.gitlab.com/devops-tools/jenkins-vs-gitlab.html). the ['GitLab vs. Jenkins' comparison page](https://about.gitlab.com/devops-tools/jenkins-vs-gitlab/).
NOTE: **Note:** NOTE: **Note:**
This documentation focuses only on how to **configure** a Jenkins *integration* with This documentation focuses only on how to **configure** a Jenkins *integration* with
......
...@@ -232,7 +232,7 @@ Potential resolutions: ...@@ -232,7 +232,7 @@ Potential resolutions:
- If you're using GitLab Core or GitLab Starter, be sure you're using - If you're using GitLab Core or GitLab Starter, be sure you're using
GitLab 13.4 or later. GitLab 13.4 or later.
[Contact GitLab Support](https://about.gitlab.com/support) if none of these reasons apply. [Contact GitLab Support](https://about.gitlab.com/support/) if none of these reasons apply.
#### Fixing synchronization issues #### Fixing synchronization issues
...@@ -245,11 +245,11 @@ resynchronize the information. To do so: ...@@ -245,11 +245,11 @@ resynchronize the information. To do so:
1. For each project, there's a sync button displayed next to the **last activity** date. 1. For each project, there's a sync button displayed next to the **last activity** date.
To perform a *soft resync*, click the button, or complete a *full sync* by shift clicking To perform a *soft resync*, click the button, or complete a *full sync* by shift clicking
the button. For more information, see the button. For more information, see
[Atlassian's documentation](https://confluence.atlassian.com/adminjiracloud/synchronize-an-account-972332890.html). [Atlassian's documentation](https://support.atlassian.com/jira-cloud-administration/docs/synchronize-jira-cloud-to-bitbucket/).
### GitLab for Jira app ### GitLab for Jira app
You can integrate GitLab.com and Jira Cloud using the [GitLab for Jira](https://marketplace.atlassian.com/apps/1221011/gitlab-for-jira) app in the Atlassian Marketplace. You can integrate GitLab.com and Jira Cloud using the [GitLab for Jira](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud) app in the Atlassian Marketplace.
This method is recommended when using GitLab.com and Jira Cloud because data is synchronized in realtime, while the DVCS connector updates data only once per hour. If you are not using both of these environments, use the [Jira DVCS Connector](#jira-dvcs-configuration) method. This method is recommended when using GitLab.com and Jira Cloud because data is synchronized in realtime, while the DVCS connector updates data only once per hour. If you are not using both of these environments, use the [Jira DVCS Connector](#jira-dvcs-configuration) method.
...@@ -257,7 +257,7 @@ This method is recommended when using GitLab.com and Jira Cloud because data is ...@@ -257,7 +257,7 @@ This method is recommended when using GitLab.com and Jira Cloud because data is
For a walkthrough of the integration with GitLab for Jira, watch [Configure GitLab Jira Integration using Marketplace App](https://youtu.be/SwR-g1s1zTo) on YouTube. For a walkthrough of the integration with GitLab for Jira, watch [Configure GitLab Jira Integration using Marketplace App](https://youtu.be/SwR-g1s1zTo) on YouTube.
1. Go to **Jira Settings > Apps > Find new apps**, then search for GitLab. 1. Go to **Jira Settings > Apps > Find new apps**, then search for GitLab.
1. Click **GitLab for Jira**, then click **Get it now**. Or go the [App in the marketplace directly](https://marketplace.atlassian.com/apps/1221011/gitlab-for-jira) 1. Click **GitLab for Jira**, then click **Get it now**. Or go the [App in the marketplace directly](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud)
![Install GitLab App on Jira](img/jira_dev_panel_setup_com_1.png) ![Install GitLab App on Jira](img/jira_dev_panel_setup_com_1.png)
1. After installing, click **Get started** to go to the configurations page. This page is always available under **Jira Settings > Apps > Manage apps**. 1. After installing, click **Get started** to go to the configurations page. This page is always available under **Jira Settings > Apps > Manage apps**.
......
...@@ -13,13 +13,13 @@ GitLab can integrate with [Kerberos](https://web.mit.edu/kerberos/) as an authen ...@@ -13,13 +13,13 @@ GitLab can integrate with [Kerberos](https://web.mit.edu/kerberos/) as an authen
[Kerberos](https://web.mit.edu/kerberos/) is a secure method for authenticating a request for a service in a [Kerberos](https://web.mit.edu/kerberos/) is a secure method for authenticating a request for a service in a
computer network. Kerberos was developed in the Athena Project at the computer network. Kerberos was developed in the Athena Project at the
[Massachusetts Institute of Technology (MIT)](http://web.mit.edu/). The name is taken from Greek [Massachusetts Institute of Technology (MIT)](https://web.mit.edu/). The name is taken from Greek
mythology; Kerberos was a three-headed dog who guarded the gates of Hades. mythology; Kerberos was a three-headed dog who guarded the gates of Hades.
## Use-cases ## Use-cases
- GitLab can be configured to allow your users to sign with their Kerberos credentials. - GitLab can be configured to allow your users to sign with their Kerberos credentials.
- You can use Kerberos to [prevent](http://web.mit.edu/sipb/doc/working/guide/guide/node20.html) anyone from intercepting or eavesdropping on the transmitted password. - You can use Kerberos to [prevent](https://web.mit.edu/sipb/doc/working/guide/guide/node20.html) anyone from intercepting or eavesdropping on the transmitted password.
## Configuration ## Configuration
......
...@@ -173,7 +173,7 @@ Find more information how to apply and renew at ...@@ -173,7 +173,7 @@ Find more information how to apply and renew at
## GitLab for Open Source subscriptions ## GitLab for Open Source subscriptions
All [GitLab for Open Source](https://about.gitlab.com/solutions/open-source/program/) All [GitLab for Open Source](https://about.gitlab.com/solutions/open-source/join/)
requests, including subscription renewals, must be made by using the application process. requests, including subscription renewals, must be made by using the application process.
If you have any questions, send an email to `opensource@gitlab.com` for assistance. If you have any questions, send an email to `opensource@gitlab.com` for assistance.
......
...@@ -168,7 +168,7 @@ application will be monitored by the WAF automatically. ...@@ -168,7 +168,7 @@ application will be monitored by the WAF automatically.
Now we can make sure that Ingress is running properly with ModSecurity and send Now we can make sure that Ingress is running properly with ModSecurity and send
a request to ensure our application is responding correctly. You must connect to a request to ensure our application is responding correctly. You must connect to
your cluster either using [Cloud Shell](https://cloud.google.com/shell/) or the [Google Cloud SDK](https://cloud.google.com/sdk/install). your cluster either using [Cloud Shell](https://cloud.google.com/shell/) or the [Google Cloud SDK](https://cloud.google.com/sdk/docs/install).
1. After connecting to your cluster, check if the Ingress-NGINX controller is running and ModSecurity is enabled. 1. After connecting to your cluster, check if the Ingress-NGINX controller is running and ModSecurity is enabled.
......
...@@ -143,7 +143,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres ...@@ -143,7 +143,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
### 3.1. DevOps ### 3.1. DevOps
1. [XebiaLabs: DevOps Terminology](https://xebialabs.com/glossary/) 1. [XebiaLabs: DevOps Terminology](https://digital.ai/glossary)
1. [XebiaLabs: Periodic Table of DevOps Tools](https://digital.ai/periodic-table-of-devops-tools) 1. [XebiaLabs: Periodic Table of DevOps Tools](https://digital.ai/periodic-table-of-devops-tools)
1. [Puppet Labs: State of DevOps 2016 - Book](https://puppet.com/resources/report/2016-state-devops-report/) 1. [Puppet Labs: State of DevOps 2016 - Book](https://puppet.com/resources/report/2016-state-devops-report/)
......
...@@ -37,7 +37,7 @@ The table is sorted by: ...@@ -37,7 +37,7 @@ The table is sorted by:
## Use cases ## Use cases
This feature is designed for [development team leaders](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#delaney-development-team-lead) This feature is designed for [development team leaders](https://about.gitlab.com/handbook/marketing/strategic-marketing/roles-personas/#delaney-development-team-lead)
and others who want to understand broad code review dynamics, and identify patterns to explain them. and others who want to understand broad code review dynamics, and identify patterns to explain them.
You can use Code Review Analytics to: You can use Code Review Analytics to:
......
...@@ -23,7 +23,7 @@ To access Merge Request Analytics, from your project's menu, go to **Analytics > ...@@ -23,7 +23,7 @@ To access Merge Request Analytics, from your project's menu, go to **Analytics >
## Use cases ## Use cases
This feature is designed for [development team leaders](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#delaney-development-team-lead) This feature is designed for [development team leaders](https://about.gitlab.com/handbook/marketing/strategic-marketing/roles-personas/#delaney-development-team-lead)
and others who want to understand broad patterns in code review and productivity. and others who want to understand broad patterns in code review and productivity.
You can use Merge Request Analytics to expose when your team is most and least productive, and You can use Merge Request Analytics to expose when your team is most and least productive, and
......
...@@ -155,7 +155,7 @@ You can use various tools to generate HAR files: ...@@ -155,7 +155,7 @@ You can use various tools to generate HAR files:
- [Fiddler](https://www.telerik.com/fiddler): Web debugging proxy - [Fiddler](https://www.telerik.com/fiddler): Web debugging proxy
- [Insomnia Core](https://insomnia.rest/): API client - [Insomnia Core](https://insomnia.rest/): API client
- [Chrome](https://www.google.com/chrome): Browser - [Chrome](https://www.google.com/chrome/): Browser
- [Firefox](https://www.mozilla.org/en-US/firefox/): Browser - [Firefox](https://www.mozilla.org/en-US/firefox/): Browser
DANGER: **Warning:** DANGER: **Warning:**
......
...@@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41203) in GitLab 13.4, only for public projects on GitLab.com. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41203) in GitLab 13.4, only for public projects on GitLab.com.
As part of [GitLab's role as a CVE Numbering Authority](https://about.gitlab.com/security/cve) As part of [GitLab's role as a CVE Numbering Authority](https://about.gitlab.com/security/cve/)
([CNA](https://cve.mitre.org/cve/cna.html)), you may request ([CNA](https://cve.mitre.org/cve/cna.html)), you may request
[CVE](https://cve.mitre.org/index.html) identifiers from GitLab to track [CVE](https://cve.mitre.org/index.html) identifiers from GitLab to track
vulnerabilities found within your project. vulnerabilities found within your project.
......
...@@ -274,13 +274,13 @@ to specify its location. ...@@ -274,13 +274,13 @@ to specify its location.
### Configuring NPM projects ### Configuring NPM projects
You can configure NPM projects by using an [`.npmrc`](https://docs.npmjs.com/configuring-npm/npmrc.html) You can configure NPM projects by using an [`.npmrc`](https://docs.npmjs.com/configuring-npm/npmrc.html/)
file. file.
#### Using private NPM registries #### Using private NPM registries
If you have a private NPM registry you can use the If you have a private NPM registry you can use the
[`registry`](https://docs.npmjs.com/using-npm/config#registry) [`registry`](https://docs.npmjs.com/using-npm/config/#registry)
setting to specify its location. setting to specify its location.
For example: For example:
...@@ -294,7 +294,7 @@ registry = https://npm.example.com ...@@ -294,7 +294,7 @@ registry = https://npm.example.com
You can supply a custom root certificate to complete TLS verification by using the You can supply a custom root certificate to complete TLS verification by using the
`ADDITIONAL_CA_CERT_BUNDLE` [environment variable](#available-variables). `ADDITIONAL_CA_CERT_BUNDLE` [environment variable](#available-variables).
To disable TLS verification you can provide the [`strict-ssl`](https://docs.npmjs.com/using-npm/config#strict-ssl) To disable TLS verification you can provide the [`strict-ssl`](https://docs.npmjs.com/using-npm/config/#strict-ssl)
setting. setting.
For example: For example:
......
...@@ -263,7 +263,7 @@ You can create a spreadsheet template to manage a pattern of consistently repeat ...@@ -263,7 +263,7 @@ You can create a spreadsheet template to manage a pattern of consistently repeat
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an introduction to epic templates, see [GitLab Epics and Epic Template Tip](https://www.youtube.com/watch?v=D74xKFNw8vg). For an introduction to epic templates, see [GitLab Epics and Epic Template Tip](https://www.youtube.com/watch?v=D74xKFNw8vg).
For more on epic templates, see [Epic Templates - Repeatable sets of issues](https://about.gitlab.com/handbook/marketing/product-marketing/getting-started/104/). For more on epic templates, see [Epic Templates - Repeatable sets of issues](https://about.gitlab.com/handbook/marketing/strategic-marketing/getting-started/104/).
## Manage multi-level child epics **(ULTIMATE)** ## Manage multi-level child epics **(ULTIMATE)**
......
...@@ -22,7 +22,7 @@ We encourage you to view this document as [rendered by GitLab itself](https://gi ...@@ -22,7 +22,7 @@ We encourage you to view this document as [rendered by GitLab itself](https://gi
GitLab uses "GitLab Flavored Markdown" (GFM). It extends the [CommonMark specification](https://spec.commonmark.org/current/) GitLab uses "GitLab Flavored Markdown" (GFM). It extends the [CommonMark specification](https://spec.commonmark.org/current/)
(which is based on standard Markdown) in several ways to add additional useful functionality. (which is based on standard Markdown) in several ways to add additional useful functionality.
It was inspired by [GitHub Flavored Markdown](https://docs.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax). It was inspired by [GitHub Flavored Markdown](https://docs.github.com/en/free-pro-team@latest/github/writing-on-github/basic-writing-and-formatting-syntax).
You can use GFM in the following areas: You can use GFM in the following areas:
......
...@@ -70,7 +70,10 @@ so that anyone who can access the project can use the package as a dependency. ...@@ -70,7 +70,10 @@ so that anyone who can access the project can use the package as a dependency.
Prerequisites: Prerequisites:
- A package in a GitLab repository. - A package in a GitLab repository. Composer packages should be versioned based on
the [Composer specification](https://getcomposer.org/doc/04-schema.md#version).
If the version is not valid, for example, it has three dots (`1.0.0.0`), an
error (`Validation failed: Version is invalid`) occurs when you publish.
- A valid `composer.json` file. - A valid `composer.json` file.
- The Packages feature is enabled in a GitLab repository. - The Packages feature is enabled in a GitLab repository.
- The project ID, which is on the project's home page. - The project ID, which is on the project's home page.
......
...@@ -489,6 +489,8 @@ Cleanup policies can be run on all projects, with these exceptions: ...@@ -489,6 +489,8 @@ Cleanup policies can be run on all projects, with these exceptions:
The cleanup policy collects all tags in the Container Registry and excludes tags The cleanup policy collects all tags in the Container Registry and excludes tags
until only the tags to be deleted remain. until only the tags to be deleted remain.
The cleanup policy searches for images based on the tag name. Support for the full path [has not yet been implemented](https://gitlab.com/gitlab-org/gitlab/-/issues/281071), but would allow you to clean up dynamically-named tags.
The cleanup policy: The cleanup policy:
1. Collects all tags for a given repository in a list. 1. Collects all tags for a given repository in a list.
...@@ -536,6 +538,8 @@ If you edit the policy and click **Save** again, the interval is reset. ...@@ -536,6 +538,8 @@ If you edit the policy and click **Save** again, the interval is reset.
Cleanup policies use regex patterns to determine which tags should be preserved or removed, both in the UI and the API. Cleanup policies use regex patterns to determine which tags should be preserved or removed, both in the UI and the API.
Regex patterns are automatically surrounded with `\A` and `\Z` anchors. Do not include any `\A`, `\Z`, `^` or `$` token in the regex patterns as they are not necessary.
Here are examples of regex patterns you may want to use: Here are examples of regex patterns you may want to use:
- Match all tags: - Match all tags:
......
...@@ -12,7 +12,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -12,7 +12,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
Publish NPM packages in your project's Package Registry. Then install the Publish NPM packages in your project's Package Registry. Then install the
packages whenever you need to use them as a dependency. packages whenever you need to use them as a dependency.
Only [scoped](https://docs.npmjs.com/misc/scope) packages are supported. Only [scoped](https://docs.npmjs.com/misc/scope/) packages are supported.
## Build an NPM package ## Build an NPM package
...@@ -25,7 +25,7 @@ the [next section](#authenticate-to-the-package-registry). ...@@ -25,7 +25,7 @@ the [next section](#authenticate-to-the-package-registry).
### Install NPM ### Install NPM
Install Node.js and NPM in your local development environment by following Install Node.js and NPM in your local development environment by following
the instructions at [npmjs.com](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). the instructions at [npmjs.com](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm/).
When installation is complete, verify you can use NPM in your terminal by When installation is complete, verify you can use NPM in your terminal by
running: running:
...@@ -360,7 +360,7 @@ In GitLab 12.6 and later, packages published to the Package Registry expose the ...@@ -360,7 +360,7 @@ In GitLab 12.6 and later, packages published to the Package Registry expose the
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9425) in GitLab Premium 12.8. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9425) in GitLab Premium 12.8.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Core in 13.3. > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Core in 13.3.
You can add [distribution tags](https://docs.npmjs.com/cli/dist-tag) to newly-published packages. You can add [distribution tags](https://docs.npmjs.com/cli/dist-tag/) to newly-published packages.
Tags are optional and can be assigned to only one package at a time. Tags are optional and can be assigned to only one package at a time.
When you publish a package without a tag, the `latest` tag is added by default. When you publish a package without a tag, the `latest` tag is added by default.
......
...@@ -7,7 +7,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -7,7 +7,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Migrating from ClearCase # Migrating from ClearCase
[ClearCase](https://www.ibm.com/us-en/marketplace/rational-clearcase) is a set of [ClearCase](https://www.ibm.com/products/rational-clearcase) is a set of
tools developed by IBM which also include a centralized version control system tools developed by IBM which also include a centralized version control system
similar to Git. similar to Git.
......
...@@ -60,7 +60,7 @@ must meet one of the following conditions prior to the import: ...@@ -60,7 +60,7 @@ must meet one of the following conditions prior to the import:
- Have previously logged in to a GitLab account using the GitHub icon. - Have previously logged in to a GitLab account using the GitHub icon.
- Have a GitHub account with a - Have a GitHub account with a
[primary email address](https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address) [primary email address](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address)
that matches their GitLab account's email address. that matches their GitLab account's email address.
If a user referenced in the project is not found in GitLab's database, the project creator (typically the user If a user referenced in the project is not found in GitLab's database, the project creator (typically the user
...@@ -89,7 +89,7 @@ Before you begin, ensure that any GitHub users who you want to map to GitLab use ...@@ -89,7 +89,7 @@ Before you begin, ensure that any GitHub users who you want to map to GitLab use
- A GitLab account that has logged in using the GitHub icon - A GitLab account that has logged in using the GitHub icon
\- or - \- or -
- A GitLab account with an email address that matches the [public email address](https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address) of the GitHub user - A GitLab account with an email address that matches the [public email address](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address) of the GitHub user
User-matching attempts occur in that order, and if a user is not identified either way, the activity is associated with User-matching attempts occur in that order, and if a user is not identified either way, the activity is associated with
the user account that is performing the import. the user account that is performing the import.
......
...@@ -20,7 +20,7 @@ and is automatically configured on [GitHub import](../../../integration/github.m ...@@ -20,7 +20,7 @@ and is automatically configured on [GitHub import](../../../integration/github.m
### Complete these steps on GitHub ### Complete these steps on GitHub
This integration requires a [GitHub API token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) This integration requires a [GitHub API token](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token)
with `repo:status` access granted: with `repo:status` access granted:
1. Go to your "Personal access tokens" page at <https://github.com/settings/tokens> 1. Go to your "Personal access tokens" page at <https://github.com/settings/tokens>
......
...@@ -19,7 +19,7 @@ The following Jira integrations allow different types of cross-referencing betwe ...@@ -19,7 +19,7 @@ The following Jira integrations allow different types of cross-referencing betwe
- [**Jira integration**](jira.md) - This is built in to GitLab. In a given GitLab project, it can be configured to connect to any Jira instance, self-managed or Cloud. - [**Jira integration**](jira.md) - This is built in to GitLab. In a given GitLab project, it can be configured to connect to any Jira instance, self-managed or Cloud.
- [**Jira development panel integration**](../../../integration/jira_development_panel.md) - This connects all GitLab projects under a specified group or personal namespace. - [**Jira development panel integration**](../../../integration/jira_development_panel.md) - This connects all GitLab projects under a specified group or personal namespace.
- If you're using Jira Cloud and GitLab.com, install the [GitLab for Jira](https://marketplace.atlassian.com/apps/1221011/gitlab-for-jira) app in the Atlassian Marketplace and see its [documentation](../../../integration/jira_development_panel.md#gitlab-for-jira-app). - If you're using Jira Cloud and GitLab.com, install the [GitLab for Jira](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud) app in the Atlassian Marketplace and see its [documentation](../../../integration/jira_development_panel.md#gitlab-for-jira-app).
- For all other environments, use the [Jira DVCS Connector configuration instructions](../../../integration/jira_development_panel.md#configuration). - For all other environments, use the [Jira DVCS Connector configuration instructions](../../../integration/jira_development_panel.md#configuration).
### Feature comparison ### Feature comparison
......
...@@ -10,7 +10,7 @@ You can configure GitLab to send notifications to a Webex Teams space. ...@@ -10,7 +10,7 @@ You can configure GitLab to send notifications to a Webex Teams space.
## Create a webhook for the space ## Create a webhook for the space
1. Go to the [Incoming Webhooks app page](https://apphub.webex.com/teams/applications/incoming-webhooks-cisco-systems-38054). 1. Go to the [Incoming Webhooks app page](https://apphub.webex.com/messaging/applications/incoming-webhooks-cisco-systems-38054).
1. Click **Connect** and log in to Webex Teams, if required. 1. Click **Connect** and log in to Webex Teams, if required.
1. Enter a name for the webhook and select the space to receive the notifications. 1. Enter a name for the webhook and select the space to receive the notifications.
1. Click **ADD**. 1. Click **ADD**.
......
...@@ -167,7 +167,7 @@ from the GitLab project. ...@@ -167,7 +167,7 @@ from the GitLab project.
> - Domain verification is **required for GitLab.com users**; > - Domain verification is **required for GitLab.com users**;
for GitLab self-managed instances, your GitLab administrator has the option for GitLab self-managed instances, your GitLab administrator has the option
to [disabled custom domain verification](../../../../administration/pages/index.md#custom-domain-verification). to [disabled custom domain verification](../../../../administration/pages/index.md#custom-domain-verification).
> - [DNS propagation may take some time (up to 24h)](https://www.inmotionhosting.com/support/domain-names/dns-nameserver-changes/domain-names-dns-changes/), > - [DNS propagation may take some time (up to 24h)](https://www.inmotionhosting.com/support/domain-names/dns-nameserver-changes/complete-guide-to-dns-records/),
although it's usually a matter of minutes to complete. Until it does, verification although it's usually a matter of minutes to complete. Until it does, verification
will fail and attempts to visit your domain will respond with a 404. will fail and attempts to visit your domain will respond with a 404.
> - Once your domain has been verified, leave the verification record > - Once your domain has been verified, leave the verification record
......
...@@ -56,7 +56,7 @@ reiterating the importance of HTTPS. ...@@ -56,7 +56,7 @@ reiterating the importance of HTTPS.
## Issuing Certificates ## Issuing Certificates
GitLab Pages accepts certificates provided in the [PEM](https://knowledge.digicert.com/quovadis) format, issued by GitLab Pages accepts certificates provided in the [PEM](https://knowledge.digicert.com/quovadis.html) format, issued by
[Certificate Authorities](https://en.wikipedia.org/wiki/Certificate_authority) or as [Certificate Authorities](https://en.wikipedia.org/wiki/Certificate_authority) or as
[self-signed certificates](https://en.wikipedia.org/wiki/Self-signed_certificate). Note that [self-signed certificates are typically not used](https://www.mcafee.com/blogs/other-blogs/mcafee-labs/self-signed-certificates-secure-so-why-ban/) [self-signed certificates](https://en.wikipedia.org/wiki/Self-signed_certificate). Note that [self-signed certificates are typically not used](https://www.mcafee.com/blogs/other-blogs/mcafee-labs/self-signed-certificates-secure-so-why-ban/)
for public websites for security reasons and to ensure that browsers trust your site's certificate. for public websites for security reasons and to ensure that browsers trust your site's certificate.
......
...@@ -124,7 +124,7 @@ After the mirror is created, this option can currently only be modified via the ...@@ -124,7 +124,7 @@ After the mirror is created, this option can currently only be modified via the
To set up a mirror from GitLab to GitHub, you need to follow these steps: To set up a mirror from GitLab to GitHub, you need to follow these steps:
1. Create a [GitHub personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with the `public_repo` box checked. 1. Create a [GitHub personal access token](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token) with the `public_repo` box checked.
1. Fill in the **Git repository URL** field using this format: `https://<your_github_username>@github.com/<your_github_group>/<your_github_project>.git`. 1. Fill in the **Git repository URL** field using this format: `https://<your_github_username>@github.com/<your_github_group>/<your_github_project>.git`.
1. Fill in **Password** field with your GitHub personal access token. 1. Fill in **Password** field with your GitHub personal access token.
1. Click the **Mirror repository** button. 1. Click the **Mirror repository** button.
...@@ -320,7 +320,7 @@ fingerprints in the open for you to check: ...@@ -320,7 +320,7 @@ fingerprints in the open for you to check:
- [AWS CodeCommit](https://docs.aws.amazon.com/codecommit/latest/userguide/regions.html#regions-fingerprints) - [AWS CodeCommit](https://docs.aws.amazon.com/codecommit/latest/userguide/regions.html#regions-fingerprints)
- [Bitbucket](https://support.atlassian.com/bitbucket-cloud/docs/configure-ssh-and-two-step-verification/) - [Bitbucket](https://support.atlassian.com/bitbucket-cloud/docs/configure-ssh-and-two-step-verification/)
- [GitHub](https://docs.github.com/en/github/authenticating-to-github/githubs-ssh-key-fingerprints) - [GitHub](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/githubs-ssh-key-fingerprints)
- [GitLab.com](../../gitlab_com/index.md#ssh-host-keys-fingerprints) - [GitLab.com](../../gitlab_com/index.md#ssh-host-keys-fingerprints)
- [Launchpad](https://help.launchpad.net/SSHFingerprints) - [Launchpad](https://help.launchpad.net/SSHFingerprints)
- [Savannah](http://savannah.gnu.org/maintenance/SshAccess/) - [Savannah](http://savannah.gnu.org/maintenance/SshAccess/)
......
...@@ -29,12 +29,12 @@ module Mutations ...@@ -29,12 +29,12 @@ module Mutations
::Types::GlobalIDType[::User], ::Types::GlobalIDType[::User],
required: false, required: false,
loads: ::Types::UserType, loads: ::Types::UserType,
description: 'The id of user to be assigned to the board' description: 'The ID of user to be assigned to the board'
argument :milestone_id, argument :milestone_id,
::Types::GlobalIDType[::Milestone], ::Types::GlobalIDType[::Milestone],
required: false, required: false,
description: 'The id of milestone to be assigned to the board' description: 'The ID of milestone to be assigned to the board'
argument :weight, argument :weight,
GraphQL::INT_TYPE, GraphQL::INT_TYPE,
......
...@@ -12,7 +12,7 @@ module Mutations ...@@ -12,7 +12,7 @@ module Mutations
argument :id, AgentID, argument :id, AgentID,
required: true, required: true,
description: 'Global id of the cluster agent that will be deleted' description: 'Global ID of the cluster agent that will be deleted'
def resolve(id:) def resolve(id:)
cluster_agent = authorized_find!(id: id) cluster_agent = authorized_find!(id: id)
......
...@@ -10,7 +10,7 @@ module Mutations ...@@ -10,7 +10,7 @@ module Mutations
argument :base_epic_id, argument :base_epic_id,
::Types::GlobalIDType[::Epic], ::Types::GlobalIDType[::Epic],
required: true, required: true,
description: 'The id of the base epic of the tree' description: 'The ID of the base epic of the tree'
argument :moved, argument :moved,
Types::EpicTree::EpicTreeNodeInputType, Types::EpicTree::EpicTreeNodeInputType,
......
...@@ -15,7 +15,7 @@ module Mutations ...@@ -15,7 +15,7 @@ module Mutations
argument :issue_iid, GraphQL::STRING_TYPE, argument :issue_iid, GraphQL::STRING_TYPE,
required: true, required: true,
description: 'The iid of the issue to be added' description: 'The IID of the issue to be added'
field :epic_issue, field :epic_issue,
Types::EpicIssueType, Types::EpicIssueType,
......
...@@ -7,7 +7,7 @@ module Mutations ...@@ -7,7 +7,7 @@ module Mutations
argument :iid, GraphQL::ID_TYPE, argument :iid, GraphQL::ID_TYPE,
required: true, required: true,
description: "The iid of the epic to mutate" description: "The IID of the epic to mutate"
argument :group_path, GraphQL::ID_TYPE, argument :group_path, GraphQL::ID_TYPE,
required: true, required: true,
......
...@@ -5,7 +5,7 @@ module Mutations ...@@ -5,7 +5,7 @@ module Mutations
class Base < ::Mutations::BaseMutation class Base < ::Mutations::BaseMutation
argument :id, ::Types::GlobalIDType[::Namespace], argument :id, ::Types::GlobalIDType[::Namespace],
required: true, required: true,
description: 'The global id of the namespace to mutate' description: 'The global ID of the namespace to mutate'
field :namespace, field :namespace,
Types::NamespaceType, Types::NamespaceType,
......
...@@ -13,7 +13,7 @@ module Mutations ...@@ -13,7 +13,7 @@ module Mutations
argument :iid, GraphQL::STRING_TYPE, argument :iid, GraphQL::STRING_TYPE,
required: true, required: true,
description: 'The iid of the requirement to update' description: 'The IID of the requirement to update'
argument :last_test_report_state, Types::RequirementsManagement::TestReportStateEnum, argument :last_test_report_state, Types::RequirementsManagement::TestReportStateEnum,
required: false, required: false,
......
...@@ -39,7 +39,7 @@ module Resolvers ...@@ -39,7 +39,7 @@ module Resolvers
argument :iid_starts_with, GraphQL::STRING_TYPE, argument :iid_starts_with, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'Filter epics by iid for autocomplete' description: 'Filter epics by IID for autocomplete'
argument :include_descendant_groups, GraphQL::BOOLEAN_TYPE, argument :include_descendant_groups, GraphQL::BOOLEAN_TYPE,
required: false, required: false,
......
...@@ -10,12 +10,12 @@ module Types ...@@ -10,12 +10,12 @@ module Types
argument :id, argument :id,
::Types::GlobalIDType[::EpicTreeSorting], ::Types::GlobalIDType[::EpicTreeSorting],
required: true, required: true,
description: 'The id of the epic_issue or epic that is being moved' description: 'The ID of the epic_issue or epic that is being moved'
argument :adjacent_reference_id, argument :adjacent_reference_id,
::Types::GlobalIDType[::EpicTreeSorting], ::Types::GlobalIDType[::EpicTreeSorting],
required: false, required: false,
description: 'The id of the epic_issue or issue that the actual epic or issue is switched with' description: 'The ID of the epic_issue or issue that the actual epic or issue is switched with'
argument :relative_position, argument :relative_position,
MoveTypeEnum, MoveTypeEnum,
......
...@@ -7,7 +7,7 @@ code_quality: ...@@ -7,7 +7,7 @@ code_quality:
variables: variables:
DOCKER_DRIVER: overlay2 DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "" DOCKER_TLS_CERTDIR: ""
CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.18" CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.18-gitlab.1"
needs: [] needs: []
script: script:
- export SOURCE_CODE=$PWD - export SOURCE_CODE=$PWD
......
...@@ -51,13 +51,6 @@ RSpec.describe 'Container Registry', :js do ...@@ -51,13 +51,6 @@ RSpec.describe 'Container Registry', :js do
expect(page).to have_content 'my/image' expect(page).to have_content 'my/image'
end end
it 'image repository delete is disabled' do
visit_container_registry
delete_btn = find('[title="Remove repository"]')
expect(delete_btn).to be_disabled
end
it 'navigates to repo details' do it 'navigates to repo details' do
visit_container_registry_details('my/image') visit_container_registry_details('my/image')
......
...@@ -94,7 +94,8 @@ RSpec.describe 'Container Registry', :js do ...@@ -94,7 +94,8 @@ RSpec.describe 'Container Registry', :js do
end end
it('pagination navigate to the second page') do it('pagination navigate to the second page') do
visit_second_page visit_details_second_page
expect(page).to have_content '20' expect(page).to have_content '20'
end end
end end
...@@ -116,22 +117,23 @@ RSpec.describe 'Container Registry', :js do ...@@ -116,22 +117,23 @@ RSpec.describe 'Container Registry', :js do
context 'when there are more than 10 images' do context 'when there are more than 10 images' do
before do before do
create_list(:container_repository, 12, project: project)
project.container_repositories << container_repository project.container_repositories << container_repository
create_list(:container_repository, 12, project: project)
visit_container_registry visit_container_registry
end end
it 'shows pagination' do it 'shows pagination' do
expect(page).to have_css '.gl-pagination' expect(page).to have_css '.gl-keyset-pagination'
end end
it 'pagination goes to second page' do it 'pagination goes to second page' do
visit_second_page visit_list_next_page
expect(page).to have_content 'my/image' expect(page).to have_content 'my/image'
end end
it 'pagination is preserved after navigating back from details' do it 'pagination is preserved after navigating back from details' do
visit_second_page visit_list_next_page
click_link 'my/image' click_link 'my/image'
breadcrumb = find '.breadcrumbs' breadcrumb = find '.breadcrumbs'
breadcrumb.click_link 'Container Registry' breadcrumb.click_link 'Container Registry'
...@@ -148,7 +150,12 @@ RSpec.describe 'Container Registry', :js do ...@@ -148,7 +150,12 @@ RSpec.describe 'Container Registry', :js do
click_link name click_link name
end end
def visit_second_page def visit_list_next_page
pagination = find '.gl-keyset-pagination'
pagination.click_button 'Next'
end
def visit_details_second_page
pagination = find '.gl-pagination' pagination = find '.gl-pagination'
pagination.click_link '2' pagination.click_link '2'
end end
......
...@@ -158,6 +158,32 @@ describe('Job State actions', () => { ...@@ -158,6 +158,32 @@ describe('Job State actions', () => {
); );
}); });
}); });
it('fetchTrace is called only if the job has started or has a trace', done => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { id: 121212, name: 'karma' });
mockedState.job.started = true;
testAction(
fetchJob,
null,
mockedState,
[],
[
{
type: 'requestJob',
},
{
payload: { id: 121212, name: 'karma' },
type: 'receiveJobSuccess',
},
{
type: 'fetchTrace',
},
],
done,
);
});
}); });
describe('receiveJobSuccess', () => { describe('receiveJobSuccess', () => {
......
...@@ -153,6 +153,7 @@ describe('Jobs Store Mutations', () => { ...@@ -153,6 +153,7 @@ describe('Jobs Store Mutations', () => {
mutations[types.SET_TRACE_TIMEOUT](stateCopy, id); mutations[types.SET_TRACE_TIMEOUT](stateCopy, id);
expect(stateCopy.traceTimeout).toEqual(id); expect(stateCopy.traceTimeout).toEqual(id);
expect(stateCopy.isTraceComplete).toBe(false);
}); });
}); });
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlSprintf } from '@gitlab/ui'; import { GlIcon, GlSprintf } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Component from '~/registry/explorer/components/list_page/image_list_row.vue'; import Component from '~/registry/explorer/components/list_page/image_list_row.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue'; import ListItem from '~/vue_shared/components/registry/list_item.vue';
...@@ -11,13 +12,15 @@ import { ...@@ -11,13 +12,15 @@ import {
REMOVE_REPOSITORY_LABEL, REMOVE_REPOSITORY_LABEL,
ASYNC_DELETE_IMAGE_ERROR_MESSAGE, ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
CLEANUP_TIMED_OUT_ERROR_MESSAGE, CLEANUP_TIMED_OUT_ERROR_MESSAGE,
IMAGE_DELETE_SCHEDULED_STATUS,
IMAGE_FAILED_DELETED_STATUS,
} from '~/registry/explorer/constants'; } from '~/registry/explorer/constants';
import { RouterLink } from '../../stubs'; import { RouterLink } from '../../stubs';
import { imagesListResponse } from '../../mock_data'; import { imagesListResponse } from '../../mock_data';
describe('Image List Row', () => { describe('Image List Row', () => {
let wrapper; let wrapper;
const item = imagesListResponse.data[0]; const [item] = imagesListResponse;
const findDetailsLink = () => wrapper.find('[data-testid="details-link"]'); const findDetailsLink = () => wrapper.find('[data-testid="details-link"]');
const findTagsCount = () => wrapper.find('[data-testid="tagsCount"]'); const findTagsCount = () => wrapper.find('[data-testid="tagsCount"]');
...@@ -50,13 +53,15 @@ describe('Image List Row', () => { ...@@ -50,13 +53,15 @@ describe('Image List Row', () => {
describe('main tooltip', () => { describe('main tooltip', () => {
it(`the title is ${ROW_SCHEDULED_FOR_DELETION}`, () => { it(`the title is ${ROW_SCHEDULED_FOR_DELETION}`, () => {
mountComponent(); mountComponent();
const tooltip = getBinding(wrapper.element, 'gl-tooltip'); const tooltip = getBinding(wrapper.element, 'gl-tooltip');
expect(tooltip).toBeDefined(); expect(tooltip).toBeDefined();
expect(tooltip.value.title).toBe(ROW_SCHEDULED_FOR_DELETION); expect(tooltip.value.title).toBe(ROW_SCHEDULED_FOR_DELETION);
}); });
it('is disabled when item is being deleted', () => { it('is disabled when item is being deleted', () => {
mountComponent({ item: { ...item, deleting: true } }); mountComponent({ item: { ...item, status: IMAGE_DELETE_SCHEDULED_STATUS } });
const tooltip = getBinding(wrapper.element, 'gl-tooltip'); const tooltip = getBinding(wrapper.element, 'gl-tooltip');
expect(tooltip.value.disabled).toBe(false); expect(tooltip.value.disabled).toBe(false);
}); });
...@@ -65,12 +70,13 @@ describe('Image List Row', () => { ...@@ -65,12 +70,13 @@ describe('Image List Row', () => {
describe('image title and path', () => { describe('image title and path', () => {
it('contains a link to the details page', () => { it('contains a link to the details page', () => {
mountComponent(); mountComponent();
const link = findDetailsLink(); const link = findDetailsLink();
expect(link.html()).toContain(item.path); expect(link.html()).toContain(item.path);
expect(link.props('to')).toMatchObject({ expect(link.props('to')).toMatchObject({
name: 'details', name: 'details',
params: { params: {
id: item.id, id: getIdFromGraphQLId(item.id),
}, },
}); });
}); });
...@@ -85,16 +91,18 @@ describe('Image List Row', () => { ...@@ -85,16 +91,18 @@ describe('Image List Row', () => {
describe('warning icon', () => { describe('warning icon', () => {
it.each` it.each`
failedDelete | cleanup_policy_started_at | shown | title status | expirationPolicyStartedAt | shown | title
${true} | ${true} | ${true} | ${ASYNC_DELETE_IMAGE_ERROR_MESSAGE} ${IMAGE_FAILED_DELETED_STATUS} | ${true} | ${true} | ${ASYNC_DELETE_IMAGE_ERROR_MESSAGE}
${false} | ${true} | ${true} | ${CLEANUP_TIMED_OUT_ERROR_MESSAGE} ${''} | ${true} | ${true} | ${CLEANUP_TIMED_OUT_ERROR_MESSAGE}
${false} | ${false} | ${false} | ${''} ${''} | ${false} | ${false} | ${''}
`( `(
'when failedDelete is $failedDelete and cleanup_policy_started_at is $cleanup_policy_started_at', 'when status is $status and expirationPolicyStartedAt is $expirationPolicyStartedAt',
({ cleanup_policy_started_at, failedDelete, shown, title }) => { ({ expirationPolicyStartedAt, status, shown, title }) => {
mountComponent({ item: { ...item, failedDelete, cleanup_policy_started_at } }); mountComponent({ item: { ...item, status, expirationPolicyStartedAt } });
const icon = findWarningIcon(); const icon = findWarningIcon();
expect(icon.exists()).toBe(shown); expect(icon.exists()).toBe(shown);
if (shown) { if (shown) {
const tooltip = getBinding(icon.element, 'gl-tooltip'); const tooltip = getBinding(icon.element, 'gl-tooltip');
expect(tooltip.value.title).toBe(title); expect(tooltip.value.title).toBe(title);
...@@ -112,30 +120,33 @@ describe('Image List Row', () => { ...@@ -112,30 +120,33 @@ describe('Image List Row', () => {
it('has the correct props', () => { it('has the correct props', () => {
mountComponent(); mountComponent();
expect(findDeleteBtn().attributes()).toMatchObject({
expect(findDeleteBtn().props()).toMatchObject({
title: REMOVE_REPOSITORY_LABEL, title: REMOVE_REPOSITORY_LABEL,
tooltipdisabled: `${Boolean(item.destroy_path)}`, tooltipDisabled: item.canDelete,
tooltiptitle: LIST_DELETE_BUTTON_DISABLED, tooltipTitle: LIST_DELETE_BUTTON_DISABLED,
}); });
}); });
it('emits a delete event', () => { it('emits a delete event', () => {
mountComponent(); mountComponent();
findDeleteBtn().vm.$emit('delete'); findDeleteBtn().vm.$emit('delete');
expect(wrapper.emitted('delete')).toEqual([[item]]); expect(wrapper.emitted('delete')).toEqual([[item]]);
}); });
it.each` it.each`
destroy_path | deleting | state canDelete | status | state
${null} | ${null} | ${'true'} ${false} | ${''} | ${true}
${null} | ${true} | ${'true'} ${false} | ${IMAGE_DELETE_SCHEDULED_STATUS} | ${true}
${'foo'} | ${true} | ${'true'} ${true} | ${IMAGE_DELETE_SCHEDULED_STATUS} | ${true}
${'foo'} | ${false} | ${undefined} ${true} | ${''} | ${false}
`( `(
'disabled is $state when destroy_path is $destroy_path and deleting is $deleting', 'disabled is $state when canDelete is $canDelete and status is $status',
({ destroy_path, deleting, state }) => { ({ canDelete, status, state }) => {
mountComponent({ item: { ...item, destroy_path, deleting } }); mountComponent({ item: { ...item, canDelete, status } });
expect(findDeleteBtn().attributes('disabled')).toBe(state);
expect(findDeleteBtn().props('disabled')).toBe(state);
}, },
); );
}); });
...@@ -155,11 +166,13 @@ describe('Image List Row', () => { ...@@ -155,11 +166,13 @@ describe('Image List Row', () => {
describe('tags count text', () => { describe('tags count text', () => {
it('with one tag in the image', () => { it('with one tag in the image', () => {
mountComponent({ item: { ...item, tags_count: 1 } }); mountComponent({ item: { ...item, tagsCount: 1 } });
expect(findTagsCount().text()).toMatchInterpolatedText('1 Tag'); expect(findTagsCount().text()).toMatchInterpolatedText('1 Tag');
}); });
it('with more than one tag in the image', () => { it('with more than one tag in the image', () => {
mountComponent({ item: { ...item, tags_count: 3 } }); mountComponent({ item: { ...item, tagsCount: 3 } });
expect(findTagsCount().text()).toMatchInterpolatedText('3 Tags'); expect(findTagsCount().text()).toMatchInterpolatedText('3 Tags');
}); });
}); });
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlPagination } from '@gitlab/ui'; import { GlKeysetPagination } from '@gitlab/ui';
import Component from '~/registry/explorer/components/list_page/image_list.vue'; import Component from '~/registry/explorer/components/list_page/image_list.vue';
import ImageListRow from '~/registry/explorer/components/list_page/image_list_row.vue'; import ImageListRow from '~/registry/explorer/components/list_page/image_list_row.vue';
import { imagesListResponse, imagePagination } from '../../mock_data'; import { imagesListResponse, pageInfo as defaultPageInfo } from '../../mock_data';
describe('Image List', () => { describe('Image List', () => {
let wrapper; let wrapper;
const findRow = () => wrapper.findAll(ImageListRow); const findRow = () => wrapper.findAll(ImageListRow);
const findPagination = () => wrapper.find(GlPagination); const findPagination = () => wrapper.find(GlKeysetPagination);
const mountComponent = () => { const mountComponent = (pageInfo = defaultPageInfo) => {
wrapper = shallowMount(Component, { wrapper = shallowMount(Component, {
propsData: { propsData: {
images: imagesListResponse.data, images: imagesListResponse,
pagination: imagePagination, pageInfo,
}, },
}); });
}; };
beforeEach(() => {
mountComponent();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
...@@ -31,10 +27,14 @@ describe('Image List', () => { ...@@ -31,10 +27,14 @@ describe('Image List', () => {
describe('list', () => { describe('list', () => {
it('contains one list element for each image', () => { it('contains one list element for each image', () => {
expect(findRow().length).toBe(imagesListResponse.data.length); mountComponent();
expect(findRow().length).toBe(imagesListResponse.length);
}); });
it('when delete event is emitted on the row it emits up a delete event', () => { it('when delete event is emitted on the row it emits up a delete event', () => {
mountComponent();
findRow() findRow()
.at(0) .at(0)
.vm.$emit('delete', 'foo'); .vm.$emit('delete', 'foo');
...@@ -44,19 +44,41 @@ describe('Image List', () => { ...@@ -44,19 +44,41 @@ describe('Image List', () => {
describe('pagination', () => { describe('pagination', () => {
it('exists', () => { it('exists', () => {
mountComponent();
expect(findPagination().exists()).toBe(true); expect(findPagination().exists()).toBe(true);
}); });
it('is wired to the correct pagination props', () => { it.each`
const pagination = findPagination(); hasNextPage | hasPreviousPage | isVisible
expect(pagination.props('perPage')).toBe(imagePagination.perPage); ${true} | ${true} | ${true}
expect(pagination.props('totalItems')).toBe(imagePagination.total); ${true} | ${false} | ${true}
expect(pagination.props('value')).toBe(imagePagination.page); ${false} | ${true} | ${true}
`(
'when hasNextPage is $hasNextPage and hasPreviousPage is $hasPreviousPage: is $isVisible that the component is visible',
({ hasNextPage, hasPreviousPage, isVisible }) => {
mountComponent({ hasNextPage, hasPreviousPage });
expect(findPagination().exists()).toBe(isVisible);
expect(findPagination().props('hasPreviousPage')).toBe(hasPreviousPage);
expect(findPagination().props('hasNextPage')).toBe(hasNextPage);
},
);
it('emits "prev-page" when the user clicks the back page button', () => {
mountComponent({ hasPreviousPage: true });
findPagination().vm.$emit('prev');
expect(wrapper.emitted('prev-page')).toEqual([[]]);
}); });
it('emits a pageChange event when the page change', () => { it('emits "next-page" when the user clicks the forward page button', () => {
findPagination().vm.$emit(GlPagination.model.event, 2); mountComponent({ hasNextPage: true });
expect(wrapper.emitted('pageChange')).toEqual([[2]]);
findPagination().vm.$emit('next');
expect(wrapper.emitted('next-page')).toEqual([[]]);
}); });
}); });
}); });
...@@ -45,21 +45,32 @@ export const registryServerResponse = [ ...@@ -45,21 +45,32 @@ export const registryServerResponse = [
}, },
]; ];
export const imagesListResponse = { export const imagesListResponse = [
data: [
{ {
path: 'foo', __typename: 'ContainerRepository',
location: 'location', id: 'gid://gitlab/ContainerRepository/26',
destroy_path: 'path', name: 'rails-12009',
path: 'gitlab-org/gitlab-test/rails-12009',
status: null,
location: '0.0.0.0:5000/gitlab-org/gitlab-test/rails-12009',
canDelete: true,
createdAt: '2020-11-03T13:29:21Z',
tagsCount: 18,
expirationPolicyStartedAt: null,
}, },
{ {
path: 'bar', __typename: 'ContainerRepository',
location: 'location-2', id: 'gid://gitlab/ContainerRepository/11',
destroy_path: 'path-2', name: 'rails-20572',
path: 'gitlab-org/gitlab-test/rails-20572',
status: null,
location: '0.0.0.0:5000/gitlab-org/gitlab-test/rails-20572',
canDelete: true,
createdAt: '2020-09-21T06:57:43Z',
tagsCount: 1,
expirationPolicyStartedAt: null,
}, },
], ];
headers,
};
export const tagsListResponse = { export const tagsListResponse = {
data: [ data: [
...@@ -90,12 +101,12 @@ export const tagsListResponse = { ...@@ -90,12 +101,12 @@ export const tagsListResponse = {
headers, headers,
}; };
export const imagePagination = { export const pageInfo = {
perPage: 10, hasNextPage: true,
page: 1, hasPreviousPage: true,
total: 14, startCursor: 'eyJpZCI6IjI2In0',
totalPages: 2, endCursor: 'eyJpZCI6IjgifQ',
nextPage: 2, __typename: 'ContainerRepositoryConnection',
}; };
export const imageDetailsMock = { export const imageDetailsMock = {
...@@ -108,3 +119,76 @@ export const imageDetailsMock = { ...@@ -108,3 +119,76 @@ export const imageDetailsMock = {
cleanup_policy_started_at: null, cleanup_policy_started_at: null,
delete_api_path: 'http://0.0.0.0:3000/api/v4/projects/1/registry/repositories/1', delete_api_path: 'http://0.0.0.0:3000/api/v4/projects/1/registry/repositories/1',
}; };
export const graphQLImageListMock = {
data: {
project: {
__typename: 'Project',
containerRepositoriesCount: 2,
containerRepositories: {
__typename: 'ContainerRepositoryConnection',
nodes: imagesListResponse,
pageInfo,
},
},
},
};
export const graphQLEmptyImageListMock = {
data: {
project: {
__typename: 'Project',
containerRepositoriesCount: 2,
containerRepositories: {
__typename: 'ContainerRepositoryConnection',
nodes: [],
pageInfo,
},
},
},
};
export const graphQLEmptyGroupImageListMock = {
data: {
group: {
__typename: 'Group',
containerRepositoriesCount: 2,
containerRepositories: {
__typename: 'ContainerRepositoryConnection',
nodes: [],
pageInfo,
},
},
},
};
export const deletedContainerRepository = {
id: 'gid://gitlab/ContainerRepository/11',
status: 'DELETE_SCHEDULED',
path: 'gitlab-org/gitlab-test/rails-12009',
__typename: 'ContainerRepository',
};
export const graphQLImageDeleteMock = {
data: {
destroyContainerRepository: {
containerRepository: {
...deletedContainerRepository,
},
errors: [],
__typename: 'DestroyContainerRepositoryPayload',
},
},
};
export const graphQLImageDeleteMockError = {
data: {
destroyContainerRepository: {
containerRepository: {
...deletedContainerRepository,
},
errors: ['foo'],
__typename: 'DestroyContainerRepositoryPayload',
},
},
};
import { shallowMount } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { GlSkeletonLoader, GlSprintf, GlAlert, GlSearchBoxByClick } from '@gitlab/ui'; import { GlSkeletonLoader, GlSprintf, GlAlert, GlSearchBoxByClick } from '@gitlab/ui';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import component from '~/registry/explorer/pages/list.vue'; import component from '~/registry/explorer/pages/list.vue';
...@@ -10,26 +12,36 @@ import RegistryHeader from '~/registry/explorer/components/list_page/registry_he ...@@ -10,26 +12,36 @@ import RegistryHeader from '~/registry/explorer/components/list_page/registry_he
import ImageList from '~/registry/explorer/components/list_page/image_list.vue'; import ImageList from '~/registry/explorer/components/list_page/image_list.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import { createStore } from '~/registry/explorer/stores/'; import { createStore } from '~/registry/explorer/stores/';
import { import { SET_INITIAL_STATE } from '~/registry/explorer/stores/mutation_types';
SET_MAIN_LOADING,
SET_IMAGES_LIST_SUCCESS,
SET_PAGINATION,
SET_INITIAL_STATE,
} from '~/registry/explorer/stores/mutation_types';
import { import {
DELETE_IMAGE_SUCCESS_MESSAGE, DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE, DELETE_IMAGE_ERROR_MESSAGE,
IMAGE_REPOSITORY_LIST_LABEL, IMAGE_REPOSITORY_LIST_LABEL,
SEARCH_PLACEHOLDER_TEXT, SEARCH_PLACEHOLDER_TEXT,
} from '~/registry/explorer/constants'; } from '~/registry/explorer/constants';
import { imagesListResponse } from '../mock_data';
import getProjectContainerRepositories from '~/registry/explorer/graphql/queries/get_project_container_repositories.graphql';
import getGroupContainerRepositories from '~/registry/explorer/graphql/queries/get_group_container_repositories.graphql';
import deleteContainerRepository from '~/registry/explorer/graphql/mutations/delete_container_repository.graphql';
import {
graphQLImageListMock,
graphQLImageDeleteMock,
deletedContainerRepository,
graphQLImageDeleteMockError,
graphQLEmptyImageListMock,
graphQLEmptyGroupImageListMock,
pageInfo,
} from '../mock_data';
import { GlModal, GlEmptyState } from '../stubs'; import { GlModal, GlEmptyState } from '../stubs';
import { $toast } from '../../shared/mocks'; import { $toast } from '../../shared/mocks';
const localVue = createLocalVue();
describe('List Page', () => { describe('List Page', () => {
let wrapper; let wrapper;
let dispatchSpy;
let store; let store;
let apolloProvider;
const findDeleteModal = () => wrapper.find(GlModal); const findDeleteModal = () => wrapper.find(GlModal);
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader); const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
...@@ -47,8 +59,30 @@ describe('List Page', () => { ...@@ -47,8 +59,30 @@ describe('List Page', () => {
const findSearchBox = () => wrapper.find(GlSearchBoxByClick); const findSearchBox = () => wrapper.find(GlSearchBoxByClick);
const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]'); const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]');
const mountComponent = ({ mocks } = {}) => { const waitForApolloRequestRender = async () => {
await waitForPromises();
await wrapper.vm.$nextTick();
};
const mountComponent = ({
mocks,
resolver = jest.fn().mockResolvedValue(graphQLImageListMock),
groupResolver = jest.fn().mockResolvedValue(graphQLImageListMock),
mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock),
} = {}) => {
localVue.use(VueApollo);
const requestHandlers = [
[getProjectContainerRepositories, resolver],
[getGroupContainerRepositories, groupResolver],
[deleteContainerRepository, mutationResolver],
];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMount(component, { wrapper = shallowMount(component, {
localVue,
apolloProvider,
store, store,
stubs: { stubs: {
GlModal, GlModal,
...@@ -69,37 +103,21 @@ describe('List Page', () => { ...@@ -69,37 +103,21 @@ describe('List Page', () => {
beforeEach(() => { beforeEach(() => {
store = createStore(); store = createStore();
dispatchSpy = jest.spyOn(store, 'dispatch');
dispatchSpy.mockResolvedValue();
store.commit(SET_IMAGES_LIST_SUCCESS, imagesListResponse.data);
store.commit(SET_PAGINATION, imagesListResponse.headers);
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('API calls', () => { it('contains registry header', async () => {
it.each`
imageList | name | called
${[]} | ${'foo'} | ${['requestImagesList']}
${imagesListResponse.data} | ${undefined} | ${['requestImagesList']}
${imagesListResponse.data} | ${'foo'} | ${undefined}
`(
'with images equal $imageList and name $name dispatch calls $called',
({ imageList, name, called }) => {
store.commit(SET_IMAGES_LIST_SUCCESS, imageList);
dispatchSpy.mockClear();
mountComponent({ mocks: { $route: { name } } });
expect(dispatchSpy.mock.calls[0]).toEqual(called);
},
);
});
it('contains registry header', () => {
mountComponent(); mountComponent();
await waitForApolloRequestRender();
expect(findRegistryHeader().exists()).toBe(true); expect(findRegistryHeader().exists()).toBe(true);
expect(findRegistryHeader().props()).toMatchObject({
imagesCount: 2,
});
}); });
describe('connection error', () => { describe('connection error', () => {
...@@ -111,7 +129,6 @@ describe('List Page', () => { ...@@ -111,7 +129,6 @@ describe('List Page', () => {
beforeEach(() => { beforeEach(() => {
store.commit(SET_INITIAL_STATE, config); store.commit(SET_INITIAL_STATE, config);
mountComponent();
}); });
afterEach(() => { afterEach(() => {
...@@ -119,78 +136,103 @@ describe('List Page', () => { ...@@ -119,78 +136,103 @@ describe('List Page', () => {
}); });
it('should show an empty state', () => { it('should show an empty state', () => {
mountComponent();
expect(findEmptyState().exists()).toBe(true); expect(findEmptyState().exists()).toBe(true);
}); });
it('empty state should have an svg-path', () => { it('empty state should have an svg-path', () => {
mountComponent();
expect(findEmptyState().attributes('svg-path')).toBe(config.containersErrorImage); expect(findEmptyState().attributes('svg-path')).toBe(config.containersErrorImage);
}); });
it('empty state should have a description', () => { it('empty state should have a description', () => {
mountComponent();
expect(findEmptyState().html()).toContain('connection error'); expect(findEmptyState().html()).toContain('connection error');
}); });
it('should not show the loading or default state', () => { it('should not show the loading or default state', () => {
mountComponent();
expect(findSkeletonLoader().exists()).toBe(false); expect(findSkeletonLoader().exists()).toBe(false);
expect(findImageList().exists()).toBe(false); expect(findImageList().exists()).toBe(false);
}); });
}); });
describe('isLoading is true', () => { describe('isLoading is true', () => {
beforeEach(() => { it('shows the skeleton loader', () => {
store.commit(SET_MAIN_LOADING, true);
mountComponent(); mountComponent();
});
afterEach(() => store.commit(SET_MAIN_LOADING, false));
it('shows the skeleton loader', () => {
expect(findSkeletonLoader().exists()).toBe(true); expect(findSkeletonLoader().exists()).toBe(true);
}); });
it('imagesList is not visible', () => { it('imagesList is not visible', () => {
mountComponent();
expect(findImageList().exists()).toBe(false); expect(findImageList().exists()).toBe(false);
}); });
it('cli commands is not visible', () => { it('cli commands is not visible', () => {
mountComponent();
expect(findCliCommands().exists()).toBe(false); expect(findCliCommands().exists()).toBe(false);
}); });
}); });
describe('list is empty', () => { describe('list is empty', () => {
beforeEach(() => { describe('project page', () => {
store.commit(SET_IMAGES_LIST_SUCCESS, []); const resolver = jest.fn().mockResolvedValue(graphQLEmptyImageListMock);
mountComponent();
return waitForPromises(); it('cli commands is not visible', async () => {
}); mountComponent({ resolver });
await waitForApolloRequestRender();
it('cli commands is not visible', () => {
expect(findCliCommands().exists()).toBe(false); expect(findCliCommands().exists()).toBe(false);
}); });
it('project empty state is visible', () => { it('project empty state is visible', async () => {
mountComponent({ resolver });
await waitForApolloRequestRender();
expect(findProjectEmptyState().exists()).toBe(true); expect(findProjectEmptyState().exists()).toBe(true);
}); });
});
describe('group page', () => {
const groupResolver = jest.fn().mockResolvedValue(graphQLEmptyGroupImageListMock);
describe('is group page is true', () => {
beforeEach(() => { beforeEach(() => {
store.commit(SET_INITIAL_STATE, { isGroupPage: true }); store.commit(SET_INITIAL_STATE, { isGroupPage: true });
mountComponent();
}); });
afterEach(() => { afterEach(() => {
store.commit(SET_INITIAL_STATE, { isGroupPage: undefined }); store.commit(SET_INITIAL_STATE, { isGroupPage: undefined });
}); });
it('group empty state is visible', () => { it('group empty state is visible', async () => {
mountComponent({ groupResolver });
await waitForApolloRequestRender();
expect(findGroupEmptyState().exists()).toBe(true); expect(findGroupEmptyState().exists()).toBe(true);
}); });
it('cli commands is not visible', () => { it('cli commands is not visible', async () => {
mountComponent({ groupResolver });
await waitForApolloRequestRender();
expect(findCliCommands().exists()).toBe(false); expect(findCliCommands().exists()).toBe(false);
}); });
it('list header is not visible', () => { it('list header is not visible', async () => {
mountComponent({ groupResolver });
await waitForApolloRequestRender();
expect(findListHeader().exists()).toBe(false); expect(findListHeader().exists()).toBe(false);
}); });
}); });
...@@ -198,55 +240,91 @@ describe('List Page', () => { ...@@ -198,55 +240,91 @@ describe('List Page', () => {
describe('list is not empty', () => { describe('list is not empty', () => {
describe('unfiltered state', () => { describe('unfiltered state', () => {
beforeEach(() => { it('quick start is visible', async () => {
mountComponent(); mountComponent();
});
it('quick start is visible', () => { await waitForApolloRequestRender();
expect(findCliCommands().exists()).toBe(true); expect(findCliCommands().exists()).toBe(true);
}); });
it('list component is visible', () => { it('list component is visible', async () => {
mountComponent();
await waitForApolloRequestRender();
expect(findImageList().exists()).toBe(true); expect(findImageList().exists()).toBe(true);
}); });
it('list header is visible', () => { it('list header is visible', async () => {
mountComponent();
await waitForApolloRequestRender();
const header = findListHeader(); const header = findListHeader();
expect(header.exists()).toBe(true); expect(header.exists()).toBe(true);
expect(header.text()).toBe(IMAGE_REPOSITORY_LIST_LABEL); expect(header.text()).toBe(IMAGE_REPOSITORY_LIST_LABEL);
}); });
describe('delete image', () => { describe('delete image', () => {
const itemToDelete = { path: 'bar' }; const deleteImage = async () => {
it('should call deleteItem when confirming deletion', () => { await wrapper.vm.$nextTick();
dispatchSpy.mockResolvedValue();
findImageList().vm.$emit('delete', itemToDelete); findImageList().vm.$emit('delete', deletedContainerRepository);
expect(wrapper.vm.itemToDelete).toEqual(itemToDelete);
findDeleteModal().vm.$emit('ok'); findDeleteModal().vm.$emit('ok');
expect(store.dispatch).toHaveBeenCalledWith(
'requestDeleteImage', await waitForApolloRequestRender();
wrapper.vm.itemToDelete, };
);
it('should call deleteItem when confirming deletion', async () => {
const mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock);
mountComponent({ mutationResolver });
await deleteImage();
expect(wrapper.vm.itemToDelete).toEqual(deletedContainerRepository);
expect(mutationResolver).toHaveBeenCalledWith({ id: deletedContainerRepository.id });
const updatedImage = findImageList()
.props('images')
.find(i => i.id === deletedContainerRepository.id);
expect(updatedImage.status).toBe(deletedContainerRepository.status);
}); });
it('should show a success alert when delete request is successful', () => { it('should show a success alert when delete request is successful', async () => {
dispatchSpy.mockResolvedValue(); const mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock);
findImageList().vm.$emit('delete', itemToDelete); mountComponent({ mutationResolver });
expect(wrapper.vm.itemToDelete).toEqual(itemToDelete);
return wrapper.vm.handleDeleteImage().then(() => { await deleteImage();
const alert = findDeleteAlert(); const alert = findDeleteAlert();
expect(alert.exists()).toBe(true); expect(alert.exists()).toBe(true);
expect(alert.text().replace(/\s\s+/gm, ' ')).toBe( expect(alert.text().replace(/\s\s+/gm, ' ')).toBe(
DELETE_IMAGE_SUCCESS_MESSAGE.replace('%{title}', wrapper.vm.itemToDelete.path), DELETE_IMAGE_SUCCESS_MESSAGE.replace('%{title}', wrapper.vm.itemToDelete.path),
); );
}); });
describe('when delete request fails it shows an alert', () => {
it('user recoverable error', async () => {
const mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMockError);
mountComponent({ mutationResolver });
await deleteImage();
const alert = findDeleteAlert();
expect(alert.exists()).toBe(true);
expect(alert.text().replace(/\s\s+/gm, ' ')).toBe(
DELETE_IMAGE_ERROR_MESSAGE.replace('%{title}', wrapper.vm.itemToDelete.path),
);
}); });
it('should show an error alert when delete request fails', () => { it('network error', async () => {
dispatchSpy.mockRejectedValue(); const mutationResolver = jest.fn().mockRejectedValue();
findImageList().vm.$emit('delete', itemToDelete); mountComponent({ mutationResolver });
expect(wrapper.vm.itemToDelete).toEqual(itemToDelete);
return wrapper.vm.handleDeleteImage().then(() => { await deleteImage();
const alert = findDeleteAlert(); const alert = findDeleteAlert();
expect(alert.exists()).toBe(true); expect(alert.exists()).toBe(true);
expect(alert.text().replace(/\s\s+/gm, ' ')).toBe( expect(alert.text().replace(/\s\s+/gm, ' ')).toBe(
...@@ -258,38 +336,68 @@ describe('List Page', () => { ...@@ -258,38 +336,68 @@ describe('List Page', () => {
}); });
describe('search', () => { describe('search', () => {
it('has a search box element', () => { const doSearch = async () => {
await waitForApolloRequestRender();
findSearchBox().vm.$emit('submit', 'centos6');
await wrapper.vm.$nextTick();
};
it('has a search box element', async () => {
mountComponent(); mountComponent();
await waitForApolloRequestRender();
const searchBox = findSearchBox(); const searchBox = findSearchBox();
expect(searchBox.exists()).toBe(true); expect(searchBox.exists()).toBe(true);
expect(searchBox.attributes('placeholder')).toBe(SEARCH_PLACEHOLDER_TEXT); expect(searchBox.attributes('placeholder')).toBe(SEARCH_PLACEHOLDER_TEXT);
}); });
it('performs a search', () => { it('performs a search', async () => {
mountComponent(); const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
findSearchBox().vm.$emit('submit', 'foo'); mountComponent({ resolver });
expect(store.dispatch).toHaveBeenCalledWith('requestImagesList', {
name: 'foo', await doSearch();
});
expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ name: 'centos6' }));
}); });
it('when search result is empty displays an empty search message', () => { it('when search result is empty displays an empty search message', async () => {
mountComponent(); const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
store.commit(SET_IMAGES_LIST_SUCCESS, []); mountComponent({ resolver });
return wrapper.vm.$nextTick().then(() => {
resolver.mockResolvedValue(graphQLEmptyImageListMock);
await doSearch();
expect(findEmptySearchMessage().exists()).toBe(true); expect(findEmptySearchMessage().exists()).toBe(true);
}); });
}); });
});
describe('pagination', () => { describe('pagination', () => {
it('pageChange event triggers the appropriate store function', () => { it('prev-page event triggers a fetchMore request', async () => {
mountComponent(); const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
findImageList().vm.$emit('pageChange', 2); mountComponent({ resolver });
expect(store.dispatch).toHaveBeenCalledWith('requestImagesList', {
pagination: { page: 2 }, await waitForApolloRequestRender();
name: wrapper.vm.search,
findImageList().vm.$emit('prev-page');
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({ first: null, before: pageInfo.startCursor }),
);
}); });
it('next-page event triggers a fetchMore request', async () => {
const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
mountComponent({ resolver });
await waitForApolloRequestRender();
findImageList().vm.$emit('next-page');
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({ after: pageInfo.endCursor }),
);
}); });
}); });
}); });
...@@ -324,11 +432,11 @@ describe('List Page', () => { ...@@ -324,11 +432,11 @@ describe('List Page', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(Tracking, 'event'); jest.spyOn(Tracking, 'event');
dispatchSpy.mockResolvedValue();
}); });
it('send an event when delete button is clicked', () => { it('send an event when delete button is clicked', () => {
findImageList().vm.$emit('delete', {}); findImageList().vm.$emit('delete', {});
testTrackingCall('click_button'); testTrackingCall('click_button');
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment