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 }) => {
logState,
pagePath,
});
return Promise.all([dispatch('fetchJob'), dispatch('fetchTrace')]);
dispatch('fetchJob');
};
export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint);
......@@ -39,6 +38,7 @@ export const toggleSidebar = ({ dispatch, state }) => {
};
let eTagPoll;
let isTraceReadyForRender;
export const clearEtagPoll = () => {
eTagPoll = null;
......@@ -70,7 +70,14 @@ export const fetchJob = ({ state, dispatch }) => {
});
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 {
axios
.get(state.jobEndpoint)
......@@ -80,9 +87,15 @@ export const fetchJob = ({ state, dispatch }) => {
Visibility.change(() => {
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');
} else {
dispatch('stopPolling');
dispatch('stopPollingTrace');
}
});
};
......@@ -163,6 +176,8 @@ export const fetchTrace = ({ dispatch, state }) =>
params: { state: state.traceState },
})
.then(({ data }) => {
isTraceReadyForRender = data.complete;
dispatch('toggleScrollisInBottom', isScrolledToBottom());
dispatch('receiveTraceSuccess', data);
......
......@@ -49,6 +49,7 @@ export default {
[types.SET_TRACE_TIMEOUT](state, id) {
state.traceTimeout = id;
state.isTraceComplete = false;
},
/**
......
<script>
import { GlPagination } from '@gitlab/ui';
import { GlKeysetPagination } from '@gitlab/ui';
import ImageListRow from './image_list_row.vue';
export default {
name: 'ImageList',
components: {
GlPagination,
GlKeysetPagination,
ImageListRow,
},
props: {
......@@ -13,19 +13,14 @@ export default {
type: Array,
required: true,
},
pagination: {
pageInfo: {
type: Object,
required: true,
},
},
computed: {
currentPage: {
get() {
return this.pagination.page;
},
set(page) {
this.$emit('pageChange', page);
},
showPagination() {
return this.pageInfo.hasPreviousPage || this.pageInfo.hasNextPage;
},
},
};
......@@ -40,13 +35,15 @@ export default {
:first="index === 0"
@delete="$emit('delete', $event)"
/>
<gl-pagination
v-model="currentPage"
:per-page="pagination.perPage"
:total-items="pagination.total"
align="center"
class="w-100 gl-mt-3"
<div class="gl-display-flex gl-justify-content-center">
<gl-keyset-pagination
v-if="showPagination"
:has-next-page="pageInfo.hasNextPage"
:has-previous-page="pageInfo.hasPreviousPage"
class="gl-mt-3"
@prev="$emit('prev-page')"
@next="$emit('next-page')"
/>
</div>
</div>
</template>
<script>
import { GlTooltipDirective, GlIcon, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import DeleteButton from '../delete_button.vue';
......@@ -11,6 +13,8 @@ import {
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
CLEANUP_TIMED_OUT_ERROR_MESSAGE,
IMAGE_DELETE_SCHEDULED_STATUS,
IMAGE_FAILED_DELETED_STATUS,
} from '../../constants/index';
export default {
......@@ -38,19 +42,29 @@ export default {
},
computed: {
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() {
return n__(
'ContainerRegistry|%{count} Tag',
'ContainerRegistry|%{count} Tags',
this.item.tags_count,
this.item.tagsCount,
);
},
warningIconText() {
if (this.item.failedDelete) {
if (this.failedDelete) {
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 null;
......@@ -63,23 +77,23 @@ export default {
<list-item
v-gl-tooltip="{
placement: 'left',
disabled: !item.deleting,
disabled: !deleting,
title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
}"
v-bind="$attrs"
:disabled="item.deleting"
:disabled="deleting"
>
<template #left-primary>
<router-link
class="gl-text-body gl-font-weight-bold"
data-testid="details-link"
:to="{ name: 'details', params: { id: item.id } }"
:to="{ name: 'details', params: { id } }"
>
{{ item.path }}
</router-link>
<clipboard-button
v-if="item.location"
:disabled="item.deleting"
:disabled="deleting"
:text="item.location"
:title="item.location"
category="tertiary"
......@@ -97,7 +111,7 @@ export default {
<gl-icon name="tag" class="gl-mr-2" />
<gl-sprintf :message="tagsCountText">
<template #count>
{{ item.tags_count }}
{{ item.tagsCount }}
</template>
</gl-sprintf>
</span>
......@@ -106,7 +120,7 @@ export default {
<delete-button
:title="$options.i18n.REMOVE_REPOSITORY_LABEL"
:disabled="disabledDelete"
:tooltip-disabled="Boolean(item.destroy_path)"
:tooltip-disabled="item.canDelete"
:tooltip-title="$options.i18n.LIST_DELETE_BUTTON_DISABLED"
@delete="$emit('delete', item)"
/>
......
......@@ -44,5 +44,6 @@ export const EMPTY_RESULT_MESSAGE = s__(
// Parameters
export const IMAGE_DELETE_SCHEDULED_STATUS = 'delete_scheduled';
export const IMAGE_FAILED_DELETED_STATUS = 'delete_failed';
export const IMAGE_DELETE_SCHEDULED_STATUS = 'DELETE_SCHEDULED';
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';
import RegistryBreadcrumb from './components/registry_breadcrumb.vue';
import { createStore } from './stores';
import createRouter from './router';
import { apolloProvider } from './graphql/index';
Vue.use(Translate);
Vue.use(GlToast);
......@@ -27,6 +28,7 @@ export default () => {
el,
store,
router,
apolloProvider,
components: {
RegistryExplorer,
},
......
<script>
import { mapState, mapActions } from 'vuex';
import { mapState } from 'vuex';
import {
GlEmptyState,
GlTooltipDirective,
......@@ -11,6 +11,7 @@ import {
GlSearchBoxByClick,
} from '@gitlab/ui';
import Tracking from '~/tracking';
import createFlash from '~/flash';
import ProjectEmptyState from '../components/list_page/project_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';
import ImageList from '../components/list_page/image_list.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 {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
......@@ -29,6 +34,8 @@ import {
IMAGE_REPOSITORY_LIST_LABEL,
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
} from '../constants/index';
export default {
......@@ -66,21 +73,63 @@ export default {
EMPTY_RESULT_TITLE,
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() {
return {
images: [],
pageInfo: {},
containerRepositoriesCount: 0,
itemToDelete: {},
deleteAlertType: null,
search: null,
isEmpty: false,
searchValue: null,
name: null,
mutationLoading: false,
};
},
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() {
return {
label: 'registry_repository_delete',
};
},
isLoading() {
return this.$apollo.queries.images.loading || this.mutationLoading;
},
showCommands() {
return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length);
},
......@@ -93,19 +142,7 @@ export default {
: DELETE_IMAGE_ERROR_MESSAGE;
},
},
mounted() {
this.loadImageList(this.$route.name);
},
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) {
this.track('click_button');
this.itemToDelete = item;
......@@ -113,18 +150,59 @@ export default {
},
handleDeleteImage() {
this.track('confirm_delete');
return this.requestDeleteImage(this.itemToDelete)
.then(() => {
this.mutationLoading = true;
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';
}
})
.catch(() => {
this.deleteAlertType = 'danger';
})
.finally(() => {
this.mutationLoading = false;
});
},
dismissDeleteAlert() {
this.deleteAlertType = null;
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>
......@@ -134,7 +212,7 @@ export default {
<gl-alert
v-if="showDeleteAlert"
:variant="deleteAlertType"
class="mt-2"
class="gl-mt-5"
dismissible
@dismiss="dismissDeleteAlert"
>
......@@ -165,7 +243,7 @@ export default {
<template v-else>
<registry-header
:images-count="pagination.total"
:images-count="containerRepositoriesCount"
:expiration-policy="config.expirationPolicy"
:help-page-path="config.helpPagePath"
:expiration-policy-help-page-path="config.expirationPolicyHelpPagePath"
......@@ -176,7 +254,7 @@ export default {
</template>
</registry-header>
<div v-if="isLoading" class="mt-2">
<div v-if="isLoading" class="gl-mt-5">
<gl-skeleton-loader
v-for="index in $options.loader.repeat"
:key="index"
......@@ -190,16 +268,17 @@ export default {
</gl-skeleton-loader>
</div>
<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-flex-fill-1">
<h5>{{ $options.i18n.IMAGE_REPOSITORY_LIST_LABEL }}</h5>
</div>
<div>
<gl-search-box-by-click
v-model="search"
v-model="searchValue"
:placeholder="$options.i18n.SEARCH_PLACEHOLDER_TEXT"
@submit="requestImagesList({ name: $event })"
@clear="name = null"
@submit="name = $event"
/>
</div>
</div>
......@@ -207,9 +286,10 @@ export default {
<image-list
v-if="images.length"
:images="images"
:pagination="pagination"
@pageChange="requestImagesList({ pagination: { page: $event }, name: search })"
:page-info="pageInfo"
@delete="deleteImage"
@prev-page="fetchPreviousPage"
@next-page="fetchNextPage"
/>
<gl-empty-state
......
......@@ -11,7 +11,7 @@ module Mutations
argument :iid, GraphQL::STRING_TYPE,
required: true,
description: "The iid of the alert to mutate"
description: "The IID of the alert to mutate"
field :alert,
Types::AlertManagement::AlertType,
......
......@@ -8,7 +8,7 @@ module Mutations
argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration],
required: true,
description: "The id of the integration to remove"
description: "The ID of the integration to remove"
def resolve(id:)
integration = authorized_find!(id: id)
......
......@@ -8,7 +8,7 @@ module Mutations
argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration],
required: true,
description: "The id of the integration to mutate"
description: "The ID of the integration to mutate"
def resolve(id:)
integration = authorized_find!(id: id)
......
......@@ -8,7 +8,7 @@ module Mutations
argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration],
required: true,
description: "The id of the integration to mutate"
description: "The ID of the integration to mutate"
argument :name, GraphQL::STRING_TYPE,
required: false,
......
......@@ -8,7 +8,7 @@ module Mutations
argument :id, Types::GlobalIDType[::PrometheusService],
required: true,
description: "The id of the integration to mutate"
description: "The ID of the integration to mutate"
def resolve(id:)
integration = authorized_find!(id: id)
......
......@@ -8,7 +8,7 @@ module Mutations
argument :id, Types::GlobalIDType[::PrometheusService],
required: true,
description: "The id of the integration to mutate"
description: "The ID of the integration to mutate"
argument :active, GraphQL::BOOLEAN_TYPE,
required: false,
......
......@@ -12,7 +12,7 @@ module Mutations
argument :awardable_id,
::Types::GlobalIDType[::Awardable],
required: true,
description: 'The global id of the awardable resource'
description: 'The global ID of the awardable resource'
argument :name,
GraphQL::STRING_TYPE,
......
......@@ -7,7 +7,7 @@ module Mutations
argument :id, PipelineID,
required: true,
description: 'The id of the pipeline to mutate'
description: 'The ID of the pipeline to mutate'
private
......
......@@ -11,7 +11,7 @@ module Mutations
argument :iid, GraphQL::ID_TYPE,
required: true,
description: "The iid of the issue to modify designs for"
description: "The IID of the issue to modify designs for"
private
......
......@@ -10,7 +10,7 @@ module Mutations
argument :id,
Types::GlobalIDType[Discussion],
required: true,
description: 'The global id of the discussion'
description: 'The global ID of the discussion'
argument :resolve,
GraphQL::BOOLEAN_TYPE,
......
......@@ -11,7 +11,7 @@ module Mutations
argument :iid, GraphQL::STRING_TYPE,
required: true,
description: "The iid of the merge request to mutate"
description: "The IID of the merge request to mutate"
field :merge_request,
Types::MergeRequestType,
......
......@@ -20,12 +20,12 @@ module Mutations
argument :environment_id,
::Types::GlobalIDType[::Environment],
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,
::Types::GlobalIDType[::Clusters::Cluster],
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,
required: true,
......
......@@ -11,7 +11,7 @@ module Mutations
argument :noteable_id,
::Types::GlobalIDType[::Noteable],
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,
GraphQL::STRING_TYPE,
......
......@@ -9,7 +9,7 @@ module Mutations
argument :discussion_id,
::Types::GlobalIDType[::Discussion],
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
......
......@@ -10,7 +10,7 @@ module Mutations
argument :id,
::Types::GlobalIDType[::Note],
required: true,
description: 'The global id of the note to destroy'
description: 'The global ID of the note to destroy'
def resolve(id:)
note = authorized_find!(id: id)
......
......@@ -16,7 +16,7 @@ module Mutations
loads: Types::Notes::NoteType,
as: :note,
required: true,
description: 'The global id of the DiffNote to update'
description: 'The global ID of the DiffNote to update'
argument :position,
Types::Notes::UpdateDiffImagePositionInputType,
......
......@@ -11,7 +11,7 @@ module Mutations
argument :id,
::Types::GlobalIDType[::Note],
required: true,
description: 'The global id of the note to update'
description: 'The global ID of the note to update'
def resolve(args)
note = authorized_find!(id: args[:id])
......
......@@ -9,7 +9,7 @@ module Mutations
argument :id, ::Types::GlobalIDType[::Snippet],
required: true,
description: 'The global id of the snippet to destroy'
description: 'The global ID of the snippet to destroy'
def resolve(id:)
snippet = authorized_find!(id: id)
......
......@@ -7,7 +7,7 @@ module Mutations
argument :id, ::Types::GlobalIDType[::Snippet],
required: true,
description: 'The global id of the snippet to update'
description: 'The global ID of the snippet to update'
def resolve(id:)
snippet = authorized_find!(id: id)
......
......@@ -9,7 +9,7 @@ module Mutations
argument :id, ::Types::GlobalIDType[::Snippet],
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,
required: false,
......
......@@ -10,7 +10,7 @@ module Mutations
argument :id,
::Types::GlobalIDType[::Todo],
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,
null: false,
......
......@@ -10,7 +10,7 @@ module Mutations
argument :id,
::Types::GlobalIDType[::Todo],
required: true,
description: 'The global id of the todo to restore'
description: 'The global ID of the todo to restore'
field :todo, Types::TodoType,
null: false,
......
......@@ -10,11 +10,11 @@ module Mutations
argument :ids,
[::Types::GlobalIDType[::Todo]],
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]],
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' }
field :todos, [::Types::TodoType],
......
......@@ -8,7 +8,7 @@ module Types
argument :jira_account_id,
GraphQL::STRING_TYPE,
required: true,
description: 'Jira account id of the user'
description: 'Jira account ID of the user'
argument :gitlab_id,
GraphQL::INT_TYPE,
required: false,
......
......@@ -16,4 +16,5 @@
"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_group_page: "true",
"group_path": @group.full_path,
character_error: @character_error.to_s } }
......@@ -17,6 +17,6 @@
"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'),
"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,
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
swimlane
swimlanes
syslog
tanuki
tcpdump
Thanos
Tiller
......
......@@ -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
ensure the cluster remains intact. For larger clusters, it is possible to restart
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
restarts it can sustain.
......
......@@ -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
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
`use_tcp` to `true`.
......
......@@ -30,7 +30,7 @@ Autogenerated input type of AddAwardEmoji
"""
input AddAwardEmojiInput {
"""
The global id of the awardable resource
The global ID of the awardable resource
"""
awardableId: AwardableID!
......@@ -837,7 +837,7 @@ input AlertSetAssigneesInput {
clientMutationId: String
"""
The iid of the alert to mutate
The IID of the alert to mutate
"""
iid: String!
......@@ -892,7 +892,7 @@ input AlertTodoCreateInput {
clientMutationId: String
"""
The iid of the alert to mutate
The IID of the alert to mutate
"""
iid: String!
......@@ -992,7 +992,7 @@ Autogenerated input type of AwardEmojiAdd
"""
input AwardEmojiAddInput {
"""
The global id of the awardable resource
The global ID of the awardable resource
"""
awardableId: AwardableID!
......@@ -1032,7 +1032,7 @@ Autogenerated input type of AwardEmojiRemove
"""
input AwardEmojiRemoveInput {
"""
The global id of the awardable resource
The global ID of the awardable resource
"""
awardableId: AwardableID!
......@@ -1072,7 +1072,7 @@ Autogenerated input type of AwardEmojiToggle
"""
input AwardEmojiToggleInput {
"""
The global id of the awardable resource
The global ID of the awardable resource
"""
awardableId: AwardableID!
......@@ -1435,7 +1435,7 @@ type BoardEpic implements CurrentUserTodos & Noteable {
iid: ID
"""
Filter epics by iid for autocomplete
Filter epics by IID for autocomplete
"""
iidStartsWith: String
......@@ -2632,7 +2632,7 @@ input ClusterAgentDeleteInput {
clientMutationId: String
"""
Global id of the cluster agent that will be deleted
Global ID of the cluster agent that will be deleted
"""
id: ClustersAgentID!
}
......@@ -3691,7 +3691,7 @@ input CreateAlertIssueInput {
clientMutationId: String
"""
The iid of the alert to mutate
The IID of the alert to mutate
"""
iid: String!
......@@ -3741,7 +3741,7 @@ input CreateAnnotationInput {
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
......@@ -3761,7 +3761,7 @@ input CreateAnnotationInput {
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
......@@ -4046,7 +4046,7 @@ input CreateDiffNoteInput {
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!
......@@ -4176,7 +4176,7 @@ input CreateImageDiffNoteInput {
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!
......@@ -4401,12 +4401,12 @@ input CreateNoteInput {
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
"""
The global id of the resource to add a note to
The global ID of the resource to add a note to
"""
noteableId: NoteableID!
}
......@@ -6035,7 +6035,7 @@ input DesignManagementDeleteInput {
filenames: [String!]!
"""
The iid of the issue to modify designs for
The IID of the issue to modify designs for
"""
iid: ID!
......@@ -6135,7 +6135,7 @@ input DesignManagementUploadInput {
files: [Upload!]!
"""
The iid of the issue to modify designs for
The IID of the issue to modify designs for
"""
iid: ID!
......@@ -6445,7 +6445,7 @@ input DestroyNoteInput {
clientMutationId: String
"""
The global id of the note to destroy
The global ID of the note to destroy
"""
id: NoteID!
}
......@@ -6480,7 +6480,7 @@ input DestroySnippetInput {
clientMutationId: String
"""
The global id of the snippet to destroy
The global ID of the snippet to destroy
"""
id: SnippetID!
}
......@@ -6999,7 +6999,7 @@ input DiscussionToggleResolveInput {
clientMutationId: String
"""
The global id of the discussion
The global ID of the discussion
"""
id: DiscussionID!
......@@ -7270,7 +7270,7 @@ type Epic implements CurrentUserTodos & Noteable {
iid: ID
"""
Filter epics by iid for autocomplete
Filter epics by IID for autocomplete
"""
iidStartsWith: String
......@@ -7683,12 +7683,12 @@ input EpicAddIssueInput {
groupPath: ID!
"""
The iid of the epic to mutate
The IID of the epic to mutate
"""
iid: ID!
"""
The iid of the issue to be added
The IID of the issue to be added
"""
issueIid: String!
......@@ -8338,7 +8338,7 @@ input EpicSetSubscriptionInput {
groupPath: ID!
"""
The iid of the epic to mutate
The IID of the epic to mutate
"""
iid: ID!
......@@ -8422,12 +8422,12 @@ A node of an epic tree.
"""
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
"""
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!
......@@ -8447,7 +8447,7 @@ Autogenerated input type of EpicTreeReorder
"""
input EpicTreeReorderInput {
"""
The id of the base epic of the tree
The ID of the base epic of the tree
"""
baseEpicId: EpicID!
......@@ -8965,7 +8965,7 @@ type Group {
iid: ID
"""
Filter epics by iid for autocomplete
Filter epics by IID for autocomplete
"""
iidStartsWith: String
......@@ -9053,7 +9053,7 @@ type Group {
iid: ID
"""
Filter epics by iid for autocomplete
Filter epics by IID for autocomplete
"""
iidStartsWith: String
......@@ -10176,7 +10176,7 @@ input HttpIntegrationDestroyInput {
clientMutationId: String
"""
The id of the integration to remove
The ID of the integration to remove
"""
id: AlertManagementHttpIntegrationID!
}
......@@ -10211,7 +10211,7 @@ input HttpIntegrationResetTokenInput {
clientMutationId: String
"""
The id of the integration to mutate
The ID of the integration to mutate
"""
id: AlertManagementHttpIntegrationID!
}
......@@ -10251,7 +10251,7 @@ input HttpIntegrationUpdateInput {
clientMutationId: String
"""
The id of the integration to mutate
The ID of the integration to mutate
"""
id: AlertManagementHttpIntegrationID!
......@@ -12186,7 +12186,7 @@ input JiraUsersMappingInputType {
gitlabId: Int
"""
Jira account id of the user
Jira account ID of the user
"""
jiraAccountId: String!
}
......@@ -12379,7 +12379,7 @@ input MarkAsSpamSnippetInput {
clientMutationId: String
"""
The global id of the snippet to update
The global ID of the snippet to update
"""
id: SnippetID!
}
......@@ -13323,7 +13323,7 @@ input MergeRequestSetAssigneesInput {
clientMutationId: String
"""
The iid of the merge request to mutate
The IID of the merge request to mutate
"""
iid: String!
......@@ -13368,7 +13368,7 @@ input MergeRequestSetLabelsInput {
clientMutationId: String
"""
The iid of the merge request to mutate
The IID of the merge request to mutate
"""
iid: String!
......@@ -13418,7 +13418,7 @@ input MergeRequestSetLockedInput {
clientMutationId: String
"""
The iid of the merge request to mutate
The IID of the merge request to mutate
"""
iid: String!
......@@ -13463,7 +13463,7 @@ input MergeRequestSetMilestoneInput {
clientMutationId: String
"""
The iid of the merge request to mutate
The IID of the merge request to mutate
"""
iid: String!
......@@ -13508,7 +13508,7 @@ input MergeRequestSetSubscriptionInput {
clientMutationId: String
"""
The iid of the merge request to mutate
The IID of the merge request to mutate
"""
iid: String!
......@@ -13553,7 +13553,7 @@ input MergeRequestSetWipInput {
clientMutationId: String
"""
The iid of the merge request to mutate
The IID of the merge request to mutate
"""
iid: String!
......@@ -13699,7 +13699,7 @@ input MergeRequestUpdateInput {
description: String
"""
The iid of the merge request to mutate
The IID of the merge request to mutate
"""
iid: String!
......@@ -14446,7 +14446,7 @@ input NamespaceIncreaseStorageTemporarilyInput {
clientMutationId: String
"""
The global id of the namespace to mutate
The global ID of the namespace to mutate
"""
id: NamespaceID!
}
......@@ -15326,7 +15326,7 @@ input PipelineCancelInput {
clientMutationId: String
"""
The id of the pipeline to mutate
The ID of the pipeline to mutate
"""
id: CiPipelineID!
}
......@@ -15392,7 +15392,7 @@ input PipelineDestroyInput {
clientMutationId: String
"""
The id of the pipeline to mutate
The ID of the pipeline to mutate
"""
id: CiPipelineID!
}
......@@ -15454,7 +15454,7 @@ input PipelineRetryInput {
clientMutationId: String
"""
The id of the pipeline to mutate
The ID of the pipeline to mutate
"""
id: CiPipelineID!
}
......@@ -17779,7 +17779,7 @@ input PrometheusIntegrationResetTokenInput {
clientMutationId: String
"""
The id of the integration to mutate
The ID of the integration to mutate
"""
id: PrometheusServiceID!
}
......@@ -17824,7 +17824,7 @@ input PrometheusIntegrationUpdateInput {
clientMutationId: String
"""
The id of the integration to mutate
The ID of the integration to mutate
"""
id: PrometheusServiceID!
}
......@@ -19160,7 +19160,7 @@ Autogenerated input type of RemoveAwardEmoji
"""
input RemoveAwardEmojiInput {
"""
The global id of the awardable resource
The global ID of the awardable resource
"""
awardableId: AwardableID!
......@@ -19235,7 +19235,7 @@ input RepositionImageDiffNoteInput {
clientMutationId: String
"""
The global id of the DiffNote to update
The global ID of the DiffNote to update
"""
id: DiffNoteID!
......@@ -22205,7 +22205,7 @@ input TodoMarkDoneInput {
clientMutationId: String
"""
The global id of the todo to mark as done
The global ID of the todo to mark as done
"""
id: TodoID!
}
......@@ -22240,7 +22240,7 @@ input TodoRestoreInput {
clientMutationId: String
"""
The global id of the todo to restore
The global ID of the todo to restore
"""
id: TodoID!
}
......@@ -22255,7 +22255,7 @@ input TodoRestoreManyInput {
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!]!
}
......@@ -22280,7 +22280,7 @@ type TodoRestoreManyPayload {
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")
}
......@@ -22387,7 +22387,7 @@ Autogenerated input type of ToggleAwardEmoji
"""
input ToggleAwardEmojiInput {
"""
The global id of the awardable resource
The global ID of the awardable resource
"""
awardableId: AwardableID!
......@@ -22609,7 +22609,7 @@ input UpdateAlertStatusInput {
clientMutationId: String
"""
The iid of the alert to mutate
The IID of the alert to mutate
"""
iid: String!
......@@ -22704,7 +22704,7 @@ Autogenerated input type of UpdateBoard
"""
input UpdateBoardInput {
"""
The id of user to be assigned to the board
The ID of user to be assigned to the board
"""
assigneeId: UserID
......@@ -22739,7 +22739,7 @@ input UpdateBoardInput {
labels: [String!]
"""
The id of milestone to be assigned to the board
The ID of milestone to be assigned to the board
"""
milestoneId: MilestoneID
......@@ -22991,7 +22991,7 @@ input UpdateEpicInput {
groupPath: ID!
"""
The iid of the epic to mutate
The IID of the epic to mutate
"""
iid: ID!
......@@ -23056,7 +23056,7 @@ input UpdateImageDiffNoteInput {
clientMutationId: String
"""
The global id of the note to update
The global ID of the note to update
"""
id: NoteID!
......@@ -23266,7 +23266,7 @@ input UpdateNoteInput {
confidential: Boolean
"""
The global id of the note to update
The global ID of the note to update
"""
id: NoteID!
}
......@@ -23306,7 +23306,7 @@ input UpdateRequirementInput {
description: String
"""
The iid of the requirement to update
The IID of the requirement to update
"""
iid: String!
......@@ -23371,7 +23371,7 @@ input UpdateSnippetInput {
description: String
"""
The global id of the snippet to update
The global ID of the snippet to update
"""
id: SnippetID!
......
......@@ -105,7 +105,7 @@
"inputFields": [
{
"name": "awardableId",
"description": "The global id of the awardable resource",
"description": "The global ID of the awardable resource",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -2103,7 +2103,7 @@
},
{
"name": "iid",
"description": "The iid of the alert to mutate",
"description": "The IID of the alert to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -2279,7 +2279,7 @@
},
{
"name": "iid",
"description": "The iid of the alert to mutate",
"description": "The IID of the alert to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -2563,7 +2563,7 @@
"inputFields": [
{
"name": "awardableId",
"description": "The global id of the awardable resource",
"description": "The global ID of the awardable resource",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -2679,7 +2679,7 @@
"inputFields": [
{
"name": "awardableId",
"description": "The global id of the awardable resource",
"description": "The global ID of the awardable resource",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -2795,7 +2795,7 @@
"inputFields": [
{
"name": "awardableId",
"description": "The global id of the awardable resource",
"description": "The global ID of the awardable resource",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -3869,7 +3869,7 @@
},
{
"name": "iidStartsWith",
"description": "Filter epics by iid for autocomplete",
"description": "Filter epics by IID for autocomplete",
"type": {
"kind": "SCALAR",
"name": "String",
......@@ -7113,7 +7113,7 @@
"inputFields": [
{
"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": {
"kind": "NON_NULL",
"name": null,
......@@ -10073,7 +10073,7 @@
},
{
"name": "iid",
"description": "The iid of the alert to mutate",
"description": "The IID of the alert to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -10203,7 +10203,7 @@
"inputFields": [
{
"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": {
"kind": "SCALAR",
"name": "EnvironmentID",
......@@ -10213,7 +10213,7 @@
},
{
"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": {
"kind": "SCALAR",
"name": "ClustersClusterID",
......@@ -11025,7 +11025,7 @@
"inputFields": [
{
"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": {
"kind": "NON_NULL",
"name": null,
......@@ -11373,7 +11373,7 @@
"inputFields": [
{
"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": {
"kind": "NON_NULL",
"name": null,
......@@ -11951,7 +11951,7 @@
"inputFields": [
{
"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": {
"kind": "NON_NULL",
"name": null,
......@@ -11989,7 +11989,7 @@
},
{
"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": {
"kind": "SCALAR",
"name": "DiscussionID",
......@@ -16611,7 +16611,7 @@
},
{
"name": "iid",
"description": "The iid of the issue to modify designs for",
"description": "The IID of the issue to modify designs for",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -16891,7 +16891,7 @@
},
{
"name": "iid",
"description": "The iid of the issue to modify designs for",
"description": "The IID of the issue to modify designs for",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -17766,7 +17766,7 @@
"inputFields": [
{
"name": "id",
"description": "The global id of the note to destroy",
"description": "The global ID of the note to destroy",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -17868,7 +17868,7 @@
"inputFields": [
{
"name": "id",
"description": "The global id of the snippet to destroy",
"description": "The global ID of the snippet to destroy",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -19477,7 +19477,7 @@
"inputFields": [
{
"name": "id",
"description": "The global id of the discussion",
"description": "The global ID of the discussion",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -20366,7 +20366,7 @@
},
{
"name": "iidStartsWith",
"description": "Filter epics by iid for autocomplete",
"description": "Filter epics by IID for autocomplete",
"type": {
"kind": "SCALAR",
"name": "String",
......@@ -21375,7 +21375,7 @@
"inputFields": [
{
"name": "iid",
"description": "The iid of the epic to mutate",
"description": "The IID of the epic to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -21417,7 +21417,7 @@
},
{
"name": "issueIid",
"description": "The iid of the issue to be added",
"description": "The IID of the issue to be added",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -23282,7 +23282,7 @@
"inputFields": [
{
"name": "iid",
"description": "The iid of the epic to mutate",
"description": "The IID of the epic to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -23499,7 +23499,7 @@
"inputFields": [
{
"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": {
"kind": "NON_NULL",
"name": null,
......@@ -23513,7 +23513,7 @@
},
{
"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": {
"kind": "SCALAR",
"name": "EpicTreeSortingID",
......@@ -23554,7 +23554,7 @@
"inputFields": [
{
"name": "baseEpicId",
"description": "The id of the base epic of the tree",
"description": "The ID of the base epic of the tree",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -24959,7 +24959,7 @@
},
{
"name": "iidStartsWith",
"description": "Filter epics by iid for autocomplete",
"description": "Filter epics by IID for autocomplete",
"type": {
"kind": "SCALAR",
"name": "String",
......@@ -25118,7 +25118,7 @@
},
{
"name": "iidStartsWith",
"description": "Filter epics by iid for autocomplete",
"description": "Filter epics by IID for autocomplete",
"type": {
"kind": "SCALAR",
"name": "String",
......@@ -27858,7 +27858,7 @@
"inputFields": [
{
"name": "id",
"description": "The id of the integration to remove",
"description": "The ID of the integration to remove",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -27960,7 +27960,7 @@
"inputFields": [
{
"name": "id",
"description": "The id of the integration to mutate",
"description": "The ID of the integration to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -28062,7 +28062,7 @@
"inputFields": [
{
"name": "id",
"description": "The id of the integration to mutate",
"description": "The ID of the integration to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -33493,7 +33493,7 @@
"inputFields": [
{
"name": "jiraAccountId",
"description": "Jira account id of the user",
"description": "Jira account ID of the user",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -34135,7 +34135,7 @@
"inputFields": [
{
"name": "id",
"description": "The global id of the snippet to update",
"description": "The global ID of the snippet to update",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -36810,7 +36810,7 @@
},
{
"name": "iid",
"description": "The iid of the merge request to mutate",
"description": "The IID of the merge request to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -36958,7 +36958,7 @@
},
{
"name": "iid",
"description": "The iid of the merge request to mutate",
"description": "The IID of the merge request to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -37106,7 +37106,7 @@
},
{
"name": "iid",
"description": "The iid of the merge request to mutate",
"description": "The IID of the merge request to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -37236,7 +37236,7 @@
},
{
"name": "iid",
"description": "The iid of the merge request to mutate",
"description": "The IID of the merge request to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -37362,7 +37362,7 @@
},
{
"name": "iid",
"description": "The iid of the merge request to mutate",
"description": "The IID of the merge request to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -37492,7 +37492,7 @@
},
{
"name": "iid",
"description": "The iid of the merge request to mutate",
"description": "The IID of the merge request to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -37770,7 +37770,7 @@
},
{
"name": "iid",
"description": "The iid of the merge request to mutate",
"description": "The IID of the merge request to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -42934,7 +42934,7 @@
"inputFields": [
{
"name": "id",
"description": "The global id of the namespace to mutate",
"description": "The global ID of the namespace to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -45453,7 +45453,7 @@
"inputFields": [
{
"name": "id",
"description": "The id of the pipeline to mutate",
"description": "The ID of the pipeline to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -45685,7 +45685,7 @@
"inputFields": [
{
"name": "id",
"description": "The id of the pipeline to mutate",
"description": "The ID of the pipeline to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -45885,7 +45885,7 @@
"inputFields": [
{
"name": "id",
"description": "The id of the pipeline to mutate",
"description": "The ID of the pipeline to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -51943,7 +51943,7 @@
"inputFields": [
{
"name": "id",
"description": "The id of the integration to mutate",
"description": "The ID of the integration to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -52045,7 +52045,7 @@
"inputFields": [
{
"name": "id",
"description": "The id of the integration to mutate",
"description": "The ID of the integration to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -55492,7 +55492,7 @@
"inputFields": [
{
"name": "awardableId",
"description": "The global id of the awardable resource",
"description": "The global ID of the awardable resource",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -55696,7 +55696,7 @@
"inputFields": [
{
"name": "id",
"description": "The global id of the DiffNote to update",
"description": "The global ID of the DiffNote to update",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -64791,7 +64791,7 @@
"inputFields": [
{
"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": {
"kind": "NON_NULL",
"name": null,
......@@ -64897,7 +64897,7 @@
"inputFields": [
{
"name": "id",
"description": "The global id of the todo to restore",
"description": "The global ID of the todo to restore",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -64932,7 +64932,7 @@
"inputFields": [
{
"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": {
"kind": "NON_NULL",
"name": null,
......@@ -65040,7 +65040,7 @@
},
{
"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": [
],
......@@ -65357,7 +65357,7 @@
"inputFields": [
{
"name": "awardableId",
"description": "The global id of the awardable resource",
"description": "The global ID of the awardable resource",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -66001,7 +66001,7 @@
},
{
"name": "iid",
"description": "The iid of the alert to mutate",
"description": "The IID of the alert to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -66319,7 +66319,7 @@
},
{
"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": {
"kind": "SCALAR",
"name": "UserID",
......@@ -66329,7 +66329,7 @@
},
{
"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": {
"kind": "SCALAR",
"name": "MilestoneID",
......@@ -66942,7 +66942,7 @@
"inputFields": [
{
"name": "iid",
"description": "The iid of the epic to mutate",
"description": "The IID of the epic to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -67174,7 +67174,7 @@
"inputFields": [
{
"name": "id",
"description": "The global id of the note to update",
"description": "The global ID of the note to update",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -67704,7 +67704,7 @@
"inputFields": [
{
"name": "id",
"description": "The global id of the note to update",
"description": "The global ID of the note to update",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -67870,7 +67870,7 @@
},
{
"name": "iid",
"description": "The iid of the requirement to update",
"description": "The IID of the requirement to update",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -67982,7 +67982,7 @@
"inputFields": [
{
"name": "id",
"description": "The global id of the snippet to update",
"description": "The global ID of the snippet to update",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -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).
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/).
......
......@@ -33,7 +33,7 @@ The agent can help you perform tasks like these:
## Architecture of the Kubernetes Agent
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
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
......
......@@ -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 client-facing change to our REST and GraphQL APIs **must** 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.
- _Any_ contribution from a community member, no matter how small, **may** have
a changelog entry regardless of these guidelines if the contributor wants one.
......@@ -55,7 +55,7 @@ the `author` field. GitLab team members **should not**.
- 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 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 [removes a feature flag](feature_flags/development.md) **should** have a changelog entry -
only if the feature flag did not default to true already.
......
......@@ -107,7 +107,7 @@ Rendered example:
## 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.
CAUTION: **Caution:**
......
......@@ -533,7 +533,9 @@ tenses, words, and phrases:
content is accessible to more readers.
- 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).)
<!-- vale gitlab.FirstPerson = NO -->
- 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
the imperative).
- 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
item. In unordered lists (using `-`), this means two spaces for each level of
indentation:
<!-- vale off -->
````markdown
- Unordered list item 1
......@@ -816,8 +820,12 @@ indentation:
![an image that will nest inside list item 4](image.png)
````
<!-- vale on -->
For ordered lists, use three spaces for each level of indentation:
<!-- vale off -->
````markdown
1. Ordered list item 1
......@@ -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)
````
<!-- vale on -->
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
level:
......@@ -904,7 +914,7 @@ Valid for Markdown content only, not for front matter entries:
- Standard quotes: double quotes (`"`). Example: "This is wrapped in double
quotes".
- 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
[GitLab UX guide](https://design.gitlab.com/content/punctuation/).
......@@ -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
the language for better readability. Examples:
<!-- vale off -->
````markdown
```ruby
Ruby code
......@@ -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
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)
......@@ -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
its information from the GitLab documentation based on
the GitLab version where it's actually removed.
its information from the GitLab documentation.
### Versions in the past or future
......@@ -1926,6 +1939,8 @@ Configuration settings include:
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:
<!-- vale off -->
````markdown
**For Omnibus installations**
......@@ -1953,6 +1968,8 @@ and reconfiguring or restarting GitLab. In that case, use these styles:
GitLab for the changes to take effect.
````
<!-- vale on -->
In this case:
- Before each step list the installation method is declared in bold.
......
......@@ -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 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
......
......@@ -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.
#### 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
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() {
### 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
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.
......
......@@ -50,7 +50,7 @@ change feature flags or you do not [have access](#access).
### 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>.
These two environments have different scopes.
......
......@@ -100,7 +100,7 @@ To propose additions to the glossary please
### Inclusive language in French
<!-- 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”.
When space is missing, the male gender should be used alone.
<!-- vale gitlab.Spelling = YES -->
......@@ -42,7 +42,7 @@ In forms we should use the `for` attribute in the label statement:
## 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.
## Online resources
......
......@@ -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.
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).
```shell
......@@ -666,7 +666,7 @@ We also use `#database-lab` and [explain.depesz.com](https://explain.depesz.com/
### 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
......
......@@ -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
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).
When selecting by text it is best to use [`getByRole` or `findByRole`](https://testing-library.com/docs/dom-testing-library/api-queries#byrole)
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)
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
......
---
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. -->
<!-- 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
- 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.
- [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:
- 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.
......
......@@ -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
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:**
This documentation focuses only on how to **configure** a Jenkins *integration* with
......
......@@ -232,7 +232,7 @@ Potential resolutions:
- If you're using GitLab Core or GitLab Starter, be sure you're using
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
......@@ -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.
To perform a *soft resync*, click the button, or complete a *full sync* by shift clicking
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
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.
......@@ -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.
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)
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
[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
[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.
## Use-cases
- 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
......
......@@ -173,7 +173,7 @@ Find more information how to apply and renew at
## 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.
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.
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
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.
......
......@@ -143,7 +143,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
### 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. [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:
## 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.
You can use Code Review Analytics to:
......
......@@ -23,7 +23,7 @@ To access Merge Request Analytics, from your project's menu, go to **Analytics >
## 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.
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:
- [Fiddler](https://www.telerik.com/fiddler): Web debugging proxy
- [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
DANGER: **Warning:**
......
......@@ -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.
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
[CVE](https://cve.mitre.org/index.html) identifiers from GitLab to track
vulnerabilities found within your project.
......
......@@ -274,13 +274,13 @@ to specify its location.
### 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.
#### Using private NPM registries
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.
For example:
......@@ -294,7 +294,7 @@ registry = https://npm.example.com
You can supply a custom root certificate to complete TLS verification by using the
`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.
For example:
......
......@@ -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>
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)**
......
......@@ -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/)
(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:
......
......@@ -70,7 +70,10 @@ so that anyone who can access the project can use the package as a dependency.
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.
- The Packages feature is enabled in a GitLab repository.
- 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:
The cleanup policy collects all tags in the Container Registry and excludes tags
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:
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.
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:
- Match all tags:
......
......@@ -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
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
......@@ -25,7 +25,7 @@ the [next section](#authenticate-to-the-package-registry).
### Install NPM
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
running:
......@@ -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.
> - [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.
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
# 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
similar to Git.
......
......@@ -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 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.
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
- A GitLab account that has logged in using the GitHub icon
\- 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
the user account that is performing the import.
......
......@@ -20,7 +20,7 @@ and is automatically configured on [GitHub import](../../../integration/github.m
### 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:
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
- [**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.
- 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).
### Feature comparison
......
......@@ -10,7 +10,7 @@ You can configure GitLab to send notifications to a Webex Teams 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. Enter a name for the webhook and select the space to receive the notifications.
1. Click **ADD**.
......
......@@ -167,7 +167,7 @@ from the GitLab project.
> - Domain verification is **required for GitLab.com users**;
for GitLab self-managed instances, your GitLab administrator has the option
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
will fail and attempts to visit your domain will respond with a 404.
> - Once your domain has been verified, leave the verification record
......
......@@ -56,7 +56,7 @@ reiterating the importance of HTTPS.
## 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
[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.
......
......@@ -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:
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 **Password** field with your GitHub personal access token.
1. Click the **Mirror repository** button.
......@@ -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)
- [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)
- [Launchpad](https://help.launchpad.net/SSHFingerprints)
- [Savannah](http://savannah.gnu.org/maintenance/SshAccess/)
......
......@@ -29,12 +29,12 @@ module Mutations
::Types::GlobalIDType[::User],
required: false,
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,
::Types::GlobalIDType[::Milestone],
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,
GraphQL::INT_TYPE,
......
......@@ -12,7 +12,7 @@ module Mutations
argument :id, AgentID,
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:)
cluster_agent = authorized_find!(id: id)
......
......@@ -10,7 +10,7 @@ module Mutations
argument :base_epic_id,
::Types::GlobalIDType[::Epic],
required: true,
description: 'The id of the base epic of the tree'
description: 'The ID of the base epic of the tree'
argument :moved,
Types::EpicTree::EpicTreeNodeInputType,
......
......@@ -15,7 +15,7 @@ module Mutations
argument :issue_iid, GraphQL::STRING_TYPE,
required: true,
description: 'The iid of the issue to be added'
description: 'The IID of the issue to be added'
field :epic_issue,
Types::EpicIssueType,
......
......@@ -7,7 +7,7 @@ module Mutations
argument :iid, GraphQL::ID_TYPE,
required: true,
description: "The iid of the epic to mutate"
description: "The IID of the epic to mutate"
argument :group_path, GraphQL::ID_TYPE,
required: true,
......
......@@ -5,7 +5,7 @@ module Mutations
class Base < ::Mutations::BaseMutation
argument :id, ::Types::GlobalIDType[::Namespace],
required: true,
description: 'The global id of the namespace to mutate'
description: 'The global ID of the namespace to mutate'
field :namespace,
Types::NamespaceType,
......
......@@ -13,7 +13,7 @@ module Mutations
argument :iid, GraphQL::STRING_TYPE,
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,
required: false,
......
......@@ -39,7 +39,7 @@ module Resolvers
argument :iid_starts_with, GraphQL::STRING_TYPE,
required: false,
description: 'Filter epics by iid for autocomplete'
description: 'Filter epics by IID for autocomplete'
argument :include_descendant_groups, GraphQL::BOOLEAN_TYPE,
required: false,
......
......@@ -10,12 +10,12 @@ module Types
argument :id,
::Types::GlobalIDType[::EpicTreeSorting],
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,
::Types::GlobalIDType[::EpicTreeSorting],
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,
MoveTypeEnum,
......
......@@ -7,7 +7,7 @@ code_quality:
variables:
DOCKER_DRIVER: overlay2
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: []
script:
- export SOURCE_CODE=$PWD
......
......@@ -51,13 +51,6 @@ RSpec.describe 'Container Registry', :js do
expect(page).to have_content 'my/image'
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
visit_container_registry_details('my/image')
......
......@@ -94,7 +94,8 @@ RSpec.describe 'Container Registry', :js do
end
it('pagination navigate to the second page') do
visit_second_page
visit_details_second_page
expect(page).to have_content '20'
end
end
......@@ -116,22 +117,23 @@ RSpec.describe 'Container Registry', :js do
context 'when there are more than 10 images' do
before do
create_list(:container_repository, 12, project: project)
project.container_repositories << container_repository
create_list(:container_repository, 12, project: project)
visit_container_registry
end
it 'shows pagination' do
expect(page).to have_css '.gl-pagination'
expect(page).to have_css '.gl-keyset-pagination'
end
it 'pagination goes to second page' do
visit_second_page
visit_list_next_page
expect(page).to have_content 'my/image'
end
it 'pagination is preserved after navigating back from details' do
visit_second_page
visit_list_next_page
click_link 'my/image'
breadcrumb = find '.breadcrumbs'
breadcrumb.click_link 'Container Registry'
......@@ -148,7 +150,12 @@ RSpec.describe 'Container Registry', :js do
click_link name
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.click_link '2'
end
......
......@@ -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', () => {
......
......@@ -153,6 +153,7 @@ describe('Jobs Store Mutations', () => {
mutations[types.SET_TRACE_TIMEOUT](stateCopy, id);
expect(stateCopy.traceTimeout).toEqual(id);
expect(stateCopy.isTraceComplete).toBe(false);
});
});
......
import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlSprintf } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Component from '~/registry/explorer/components/list_page/image_list_row.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
......@@ -11,13 +12,15 @@ import {
REMOVE_REPOSITORY_LABEL,
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
CLEANUP_TIMED_OUT_ERROR_MESSAGE,
IMAGE_DELETE_SCHEDULED_STATUS,
IMAGE_FAILED_DELETED_STATUS,
} from '~/registry/explorer/constants';
import { RouterLink } from '../../stubs';
import { imagesListResponse } from '../../mock_data';
describe('Image List Row', () => {
let wrapper;
const item = imagesListResponse.data[0];
const [item] = imagesListResponse;
const findDetailsLink = () => wrapper.find('[data-testid="details-link"]');
const findTagsCount = () => wrapper.find('[data-testid="tagsCount"]');
......@@ -50,13 +53,15 @@ describe('Image List Row', () => {
describe('main tooltip', () => {
it(`the title is ${ROW_SCHEDULED_FOR_DELETION}`, () => {
mountComponent();
const tooltip = getBinding(wrapper.element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(tooltip.value.title).toBe(ROW_SCHEDULED_FOR_DELETION);
});
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');
expect(tooltip.value.disabled).toBe(false);
});
......@@ -65,12 +70,13 @@ describe('Image List Row', () => {
describe('image title and path', () => {
it('contains a link to the details page', () => {
mountComponent();
const link = findDetailsLink();
expect(link.html()).toContain(item.path);
expect(link.props('to')).toMatchObject({
name: 'details',
params: {
id: item.id,
id: getIdFromGraphQLId(item.id),
},
});
});
......@@ -85,16 +91,18 @@ describe('Image List Row', () => {
describe('warning icon', () => {
it.each`
failedDelete | cleanup_policy_started_at | shown | title
${true} | ${true} | ${true} | ${ASYNC_DELETE_IMAGE_ERROR_MESSAGE}
${false} | ${true} | ${true} | ${CLEANUP_TIMED_OUT_ERROR_MESSAGE}
${false} | ${false} | ${false} | ${''}
status | expirationPolicyStartedAt | shown | title
${IMAGE_FAILED_DELETED_STATUS} | ${true} | ${true} | ${ASYNC_DELETE_IMAGE_ERROR_MESSAGE}
${''} | ${true} | ${true} | ${CLEANUP_TIMED_OUT_ERROR_MESSAGE}
${''} | ${false} | ${false} | ${''}
`(
'when failedDelete is $failedDelete and cleanup_policy_started_at is $cleanup_policy_started_at',
({ cleanup_policy_started_at, failedDelete, shown, title }) => {
mountComponent({ item: { ...item, failedDelete, cleanup_policy_started_at } });
'when status is $status and expirationPolicyStartedAt is $expirationPolicyStartedAt',
({ expirationPolicyStartedAt, status, shown, title }) => {
mountComponent({ item: { ...item, status, expirationPolicyStartedAt } });
const icon = findWarningIcon();
expect(icon.exists()).toBe(shown);
if (shown) {
const tooltip = getBinding(icon.element, 'gl-tooltip');
expect(tooltip.value.title).toBe(title);
......@@ -112,30 +120,33 @@ describe('Image List Row', () => {
it('has the correct props', () => {
mountComponent();
expect(findDeleteBtn().attributes()).toMatchObject({
expect(findDeleteBtn().props()).toMatchObject({
title: REMOVE_REPOSITORY_LABEL,
tooltipdisabled: `${Boolean(item.destroy_path)}`,
tooltiptitle: LIST_DELETE_BUTTON_DISABLED,
tooltipDisabled: item.canDelete,
tooltipTitle: LIST_DELETE_BUTTON_DISABLED,
});
});
it('emits a delete event', () => {
mountComponent();
findDeleteBtn().vm.$emit('delete');
expect(wrapper.emitted('delete')).toEqual([[item]]);
});
it.each`
destroy_path | deleting | state
${null} | ${null} | ${'true'}
${null} | ${true} | ${'true'}
${'foo'} | ${true} | ${'true'}
${'foo'} | ${false} | ${undefined}
canDelete | status | state
${false} | ${''} | ${true}
${false} | ${IMAGE_DELETE_SCHEDULED_STATUS} | ${true}
${true} | ${IMAGE_DELETE_SCHEDULED_STATUS} | ${true}
${true} | ${''} | ${false}
`(
'disabled is $state when destroy_path is $destroy_path and deleting is $deleting',
({ destroy_path, deleting, state }) => {
mountComponent({ item: { ...item, destroy_path, deleting } });
expect(findDeleteBtn().attributes('disabled')).toBe(state);
'disabled is $state when canDelete is $canDelete and status is $status',
({ canDelete, status, state }) => {
mountComponent({ item: { ...item, canDelete, status } });
expect(findDeleteBtn().props('disabled')).toBe(state);
},
);
});
......@@ -155,11 +166,13 @@ describe('Image List Row', () => {
describe('tags count text', () => {
it('with one tag in the image', () => {
mountComponent({ item: { ...item, tags_count: 1 } });
mountComponent({ item: { ...item, tagsCount: 1 } });
expect(findTagsCount().text()).toMatchInterpolatedText('1 Tag');
});
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');
});
});
......
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 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', () => {
let wrapper;
const findRow = () => wrapper.findAll(ImageListRow);
const findPagination = () => wrapper.find(GlPagination);
const findPagination = () => wrapper.find(GlKeysetPagination);
const mountComponent = () => {
const mountComponent = (pageInfo = defaultPageInfo) => {
wrapper = shallowMount(Component, {
propsData: {
images: imagesListResponse.data,
pagination: imagePagination,
images: imagesListResponse,
pageInfo,
},
});
};
beforeEach(() => {
mountComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
......@@ -31,10 +27,14 @@ describe('Image List', () => {
describe('list', () => {
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', () => {
mountComponent();
findRow()
.at(0)
.vm.$emit('delete', 'foo');
......@@ -44,19 +44,41 @@ describe('Image List', () => {
describe('pagination', () => {
it('exists', () => {
mountComponent();
expect(findPagination().exists()).toBe(true);
});
it('is wired to the correct pagination props', () => {
const pagination = findPagination();
expect(pagination.props('perPage')).toBe(imagePagination.perPage);
expect(pagination.props('totalItems')).toBe(imagePagination.total);
expect(pagination.props('value')).toBe(imagePagination.page);
it.each`
hasNextPage | hasPreviousPage | isVisible
${true} | ${true} | ${true}
${true} | ${false} | ${true}
${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', () => {
findPagination().vm.$emit(GlPagination.model.event, 2);
expect(wrapper.emitted('pageChange')).toEqual([[2]]);
it('emits "next-page" when the user clicks the forward page button', () => {
mountComponent({ hasNextPage: true });
findPagination().vm.$emit('next');
expect(wrapper.emitted('next-page')).toEqual([[]]);
});
});
});
......@@ -45,21 +45,32 @@ export const registryServerResponse = [
},
];
export const imagesListResponse = {
data: [
export const imagesListResponse = [
{
path: 'foo',
location: 'location',
destroy_path: 'path',
__typename: 'ContainerRepository',
id: 'gid://gitlab/ContainerRepository/26',
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',
location: 'location-2',
destroy_path: 'path-2',
__typename: 'ContainerRepository',
id: 'gid://gitlab/ContainerRepository/11',
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 = {
data: [
......@@ -90,12 +101,12 @@ export const tagsListResponse = {
headers,
};
export const imagePagination = {
perPage: 10,
page: 1,
total: 14,
totalPages: 2,
nextPage: 2,
export const pageInfo = {
hasNextPage: true,
hasPreviousPage: true,
startCursor: 'eyJpZCI6IjI2In0',
endCursor: 'eyJpZCI6IjgifQ',
__typename: 'ContainerRepositoryConnection',
};
export const imageDetailsMock = {
......@@ -108,3 +119,76 @@ export const imageDetailsMock = {
cleanup_policy_started_at: null,
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 createMockApollo from 'jest/helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Tracking from '~/tracking';
import component from '~/registry/explorer/pages/list.vue';
......@@ -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 TitleArea from '~/vue_shared/components/registry/title_area.vue';
import { createStore } from '~/registry/explorer/stores/';
import {
SET_MAIN_LOADING,
SET_IMAGES_LIST_SUCCESS,
SET_PAGINATION,
SET_INITIAL_STATE,
} from '~/registry/explorer/stores/mutation_types';
import { SET_INITIAL_STATE } from '~/registry/explorer/stores/mutation_types';
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
IMAGE_REPOSITORY_LIST_LABEL,
SEARCH_PLACEHOLDER_TEXT,
} 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 { $toast } from '../../shared/mocks';
const localVue = createLocalVue();
describe('List Page', () => {
let wrapper;
let dispatchSpy;
let store;
let apolloProvider;
const findDeleteModal = () => wrapper.find(GlModal);
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
......@@ -47,8 +59,30 @@ describe('List Page', () => {
const findSearchBox = () => wrapper.find(GlSearchBoxByClick);
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, {
localVue,
apolloProvider,
store,
stubs: {
GlModal,
......@@ -69,37 +103,21 @@ describe('List Page', () => {
beforeEach(() => {
store = createStore();
dispatchSpy = jest.spyOn(store, 'dispatch');
dispatchSpy.mockResolvedValue();
store.commit(SET_IMAGES_LIST_SUCCESS, imagesListResponse.data);
store.commit(SET_PAGINATION, imagesListResponse.headers);
});
afterEach(() => {
wrapper.destroy();
});
describe('API calls', () => {
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', () => {
it('contains registry header', async () => {
mountComponent();
await waitForApolloRequestRender();
expect(findRegistryHeader().exists()).toBe(true);
expect(findRegistryHeader().props()).toMatchObject({
imagesCount: 2,
});
});
describe('connection error', () => {
......@@ -111,7 +129,6 @@ describe('List Page', () => {
beforeEach(() => {
store.commit(SET_INITIAL_STATE, config);
mountComponent();
});
afterEach(() => {
......@@ -119,78 +136,103 @@ describe('List Page', () => {
});
it('should show an empty state', () => {
mountComponent();
expect(findEmptyState().exists()).toBe(true);
});
it('empty state should have an svg-path', () => {
mountComponent();
expect(findEmptyState().attributes('svg-path')).toBe(config.containersErrorImage);
});
it('empty state should have a description', () => {
mountComponent();
expect(findEmptyState().html()).toContain('connection error');
});
it('should not show the loading or default state', () => {
mountComponent();
expect(findSkeletonLoader().exists()).toBe(false);
expect(findImageList().exists()).toBe(false);
});
});
describe('isLoading is true', () => {
beforeEach(() => {
store.commit(SET_MAIN_LOADING, true);
it('shows the skeleton loader', () => {
mountComponent();
});
afterEach(() => store.commit(SET_MAIN_LOADING, false));
it('shows the skeleton loader', () => {
expect(findSkeletonLoader().exists()).toBe(true);
});
it('imagesList is not visible', () => {
mountComponent();
expect(findImageList().exists()).toBe(false);
});
it('cli commands is not visible', () => {
mountComponent();
expect(findCliCommands().exists()).toBe(false);
});
});
describe('list is empty', () => {
beforeEach(() => {
store.commit(SET_IMAGES_LIST_SUCCESS, []);
mountComponent();
return waitForPromises();
});
describe('project page', () => {
const resolver = jest.fn().mockResolvedValue(graphQLEmptyImageListMock);
it('cli commands is not visible', async () => {
mountComponent({ resolver });
await waitForApolloRequestRender();
it('cli commands is not visible', () => {
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);
});
});
describe('group page', () => {
const groupResolver = jest.fn().mockResolvedValue(graphQLEmptyGroupImageListMock);
describe('is group page is true', () => {
beforeEach(() => {
store.commit(SET_INITIAL_STATE, { isGroupPage: true });
mountComponent();
});
afterEach(() => {
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);
});
it('cli commands is not visible', () => {
it('cli commands is not visible', async () => {
mountComponent({ groupResolver });
await waitForApolloRequestRender();
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);
});
});
......@@ -198,55 +240,91 @@ describe('List Page', () => {
describe('list is not empty', () => {
describe('unfiltered state', () => {
beforeEach(() => {
it('quick start is visible', async () => {
mountComponent();
});
it('quick start is visible', () => {
await waitForApolloRequestRender();
expect(findCliCommands().exists()).toBe(true);
});
it('list component is visible', () => {
it('list component is visible', async () => {
mountComponent();
await waitForApolloRequestRender();
expect(findImageList().exists()).toBe(true);
});
it('list header is visible', () => {
it('list header is visible', async () => {
mountComponent();
await waitForApolloRequestRender();
const header = findListHeader();
expect(header.exists()).toBe(true);
expect(header.text()).toBe(IMAGE_REPOSITORY_LIST_LABEL);
});
describe('delete image', () => {
const itemToDelete = { path: 'bar' };
it('should call deleteItem when confirming deletion', () => {
dispatchSpy.mockResolvedValue();
findImageList().vm.$emit('delete', itemToDelete);
expect(wrapper.vm.itemToDelete).toEqual(itemToDelete);
const deleteImage = async () => {
await wrapper.vm.$nextTick();
findImageList().vm.$emit('delete', deletedContainerRepository);
findDeleteModal().vm.$emit('ok');
expect(store.dispatch).toHaveBeenCalledWith(
'requestDeleteImage',
wrapper.vm.itemToDelete,
);
await waitForApolloRequestRender();
};
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', () => {
dispatchSpy.mockResolvedValue();
findImageList().vm.$emit('delete', itemToDelete);
expect(wrapper.vm.itemToDelete).toEqual(itemToDelete);
return wrapper.vm.handleDeleteImage().then(() => {
it('should show a success alert when delete request is successful', async () => {
const mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock);
mountComponent({ mutationResolver });
await deleteImage();
const alert = findDeleteAlert();
expect(alert.exists()).toBe(true);
expect(alert.text().replace(/\s\s+/gm, ' ')).toBe(
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', () => {
dispatchSpy.mockRejectedValue();
findImageList().vm.$emit('delete', itemToDelete);
expect(wrapper.vm.itemToDelete).toEqual(itemToDelete);
return wrapper.vm.handleDeleteImage().then(() => {
it('network error', async () => {
const mutationResolver = jest.fn().mockRejectedValue();
mountComponent({ mutationResolver });
await deleteImage();
const alert = findDeleteAlert();
expect(alert.exists()).toBe(true);
expect(alert.text().replace(/\s\s+/gm, ' ')).toBe(
......@@ -258,38 +336,68 @@ describe('List Page', () => {
});
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();
await waitForApolloRequestRender();
const searchBox = findSearchBox();
expect(searchBox.exists()).toBe(true);
expect(searchBox.attributes('placeholder')).toBe(SEARCH_PLACEHOLDER_TEXT);
});
it('performs a search', () => {
mountComponent();
findSearchBox().vm.$emit('submit', 'foo');
expect(store.dispatch).toHaveBeenCalledWith('requestImagesList', {
name: 'foo',
});
it('performs a search', async () => {
const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
mountComponent({ resolver });
await doSearch();
expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ name: 'centos6' }));
});
it('when search result is empty displays an empty search message', () => {
mountComponent();
store.commit(SET_IMAGES_LIST_SUCCESS, []);
return wrapper.vm.$nextTick().then(() => {
it('when search result is empty displays an empty search message', async () => {
const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
mountComponent({ resolver });
resolver.mockResolvedValue(graphQLEmptyImageListMock);
await doSearch();
expect(findEmptySearchMessage().exists()).toBe(true);
});
});
});
describe('pagination', () => {
it('pageChange event triggers the appropriate store function', () => {
mountComponent();
findImageList().vm.$emit('pageChange', 2);
expect(store.dispatch).toHaveBeenCalledWith('requestImagesList', {
pagination: { page: 2 },
name: wrapper.vm.search,
it('prev-page event triggers a fetchMore request', async () => {
const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
mountComponent({ resolver });
await waitForApolloRequestRender();
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', () => {
beforeEach(() => {
jest.spyOn(Tracking, 'event');
dispatchSpy.mockResolvedValue();
});
it('send an event when delete button is clicked', () => {
findImageList().vm.$emit('delete', {});
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