Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-02-14 12:14:02 +00:00
parent 41482e5dce
commit 2896c7471a
159 changed files with 959 additions and 629 deletions

View File

@ -12,11 +12,9 @@ Style/OpenStructUse:
- lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
- lib/gitlab/testing/request_inspector_middleware.rb
- lib/mattermost/session.rb
- spec/controllers/groups/clusters_controller_spec.rb
- spec/controllers/projects/clusters_controller_spec.rb
- spec/factories/go_module_versions.rb
- spec/factories/wiki_pages.rb
- spec/features/projects/clusters_spec.rb
- spec/graphql/mutations/branches/create_spec.rb
- spec/graphql/mutations/clusters/agent_tokens/create_spec.rb
- spec/graphql/mutations/clusters/agents/create_spec.rb

View File

@ -22,6 +22,7 @@ query accessTokensGetProjects(
avatarUrl
}
pageInfo {
__typename
...PageInfo
}
}

View File

@ -1,4 +1,4 @@
import { ApolloLink, Observable } from 'apollo-link';
import { ApolloLink, Observable } from '@apollo/client/core';
import { print } from 'graphql';
import cable from '~/actioncable_consumer';
import { uuids } from '~/lib/utils/uuids';

View File

@ -1,4 +1,4 @@
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import { defaultDataIdFromObject } from '@apollo/client/core';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';

View File

@ -1,15 +1,9 @@
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import produce from 'immer';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import introspectionQueryResultData from './graphql/fragmentTypes.json';
import getCurrentIntegrationQuery from './graphql/queries/get_current_integration.query.graphql';
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
});
Vue.use(VueApollo);
const resolvers = {
@ -55,9 +49,5 @@ const resolvers = {
};
export default new VueApollo({
defaultClient: createDefaultClient(resolvers, {
cacheConfig: {
fragmentMatcher,
},
}),
defaultClient: createDefaultClient(resolvers),
});

View File

@ -1 +0,0 @@
{"__schema":{"types":[{"kind":"UNION","name":"AlertManagementIntegration","possibleTypes":[{"name":"AlertManagementHttpIntegration"},{"name":"AlertManagementPrometheusIntegration"}]}]}}

View File

@ -5,6 +5,7 @@ query getIntegrations($projectPath: ID!) {
id
alertManagementIntegrations {
nodes {
__typename
...IntegrationItem
}
}

View File

@ -1,4 +1,5 @@
fragment Count on UsageTrendsMeasurement {
__typename
count
recordedAt
}

View File

@ -1,10 +1,5 @@
import { IntrospectionFragmentMatcher, defaultDataIdFromObject } from 'apollo-cache-inmemory';
import { defaultDataIdFromObject } from '@apollo/client/core';
import createDefaultClient from '~/lib/graphql';
import introspectionQueryResultData from '~/sidebar/fragmentTypes.json';
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
});
export const gqlClient = createDefaultClient(
{},
@ -14,8 +9,6 @@ export const gqlClient = createDefaultClient(
// eslint-disable-next-line no-underscore-dangle
return object.__typename === 'BoardList' ? object.iid : defaultDataIdFromObject(object);
},
fragmentMatcher,
},
},
);

View File

@ -1,27 +1,14 @@
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import BoardsSelector from 'ee_else_ce/boards/components/boards_selector.vue';
import store from '~/boards/stores';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import introspectionQueryResultData from '~/sidebar/fragmentTypes.json';
Vue.use(VueApollo);
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
});
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
cacheConfig: {
fragmentMatcher,
},
},
),
defaultClient: createDefaultClient(),
});
export default (params = {}) => {

View File

@ -1,4 +1,4 @@
import { ApolloLink, Observable } from 'apollo-link';
import { ApolloLink, Observable } from '@apollo/client/core';
export const apolloCaptchaLink = new ApolloLink((operation, forward) =>
forward(operation).flatMap((result) => {

View File

@ -1,25 +1,10 @@
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { vulnerabilityLocationTypes } from '~/graphql_shared/fragment_types/vulnerability_location_types';
Vue.use(VueApollo);
// We create a fragment matcher so that we can create a fragment from an interface
// Without this, Apollo throws a heuristic fragment matcher warning
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData: vulnerabilityLocationTypes,
});
const defaultClient = createDefaultClient(
{},
{
cacheConfig: {
fragmentMatcher,
},
},
);
const defaultClient = createDefaultClient();
export default new VueApollo({
defaultClient,

View File

@ -1,11 +1,10 @@
import { defaultDataIdFromObject, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import { defaultDataIdFromObject } from '@apollo/client/core';
import produce from 'immer';
import { uniqueId } from 'lodash';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import introspectionQueryResultData from './graphql/fragmentTypes.json';
import activeDiscussionQuery from './graphql/queries/active_discussion.query.graphql';
import getDesignQuery from './graphql/queries/get_design.query.graphql';
import typeDefs from './graphql/typedefs.graphql';
@ -13,10 +12,6 @@ import { addPendingTodoToStore } from './utils/cache_update';
import { extractTodoIdFromDeletePath, createPendingTodo } from './utils/design_management_utils';
import { CREATE_DESIGN_TODO_EXISTS_ERROR } from './utils/error_messages';
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
});
Vue.use(VueApollo);
const resolvers = {
@ -85,7 +80,6 @@ const defaultClient = createDefaultClient(
}
return defaultDataIdFromObject(object);
},
fragmentMatcher,
},
typeDefs,
},

View File

@ -1 +0,0 @@
{"__schema":{"types":[{"kind":"INTERFACE","name":"User","possibleTypes":[{"name":"UserCore"}]},{"kind":"UNION","name":"NoteableType","possibleTypes":[{"name":"Design"},{"name":"Issue"},{"name":"MergeRequest"}]}]}}

View File

@ -1,17 +0,0 @@
export const vulnerabilityLocationTypes = {
__schema: {
types: [
{
kind: 'UNION',
name: 'VulnerabilityLocation',
possibleTypes: [
{ name: 'VulnerabilityLocationContainerScanning' },
{ name: 'VulnerabilityLocationDast' },
{ name: 'VulnerabilityLocationDependencyScanning' },
{ name: 'VulnerabilityLocationSast' },
{ name: 'VulnerabilityLocationSecretDetection' },
],
},
],
},
};

View File

@ -0,0 +1 @@
{"AlertManagementIntegration":["AlertManagementHttpIntegration","AlertManagementPrometheusIntegration"],"CurrentUserTodos":["BoardEpic","Design","Epic","EpicIssue","Issue","MergeRequest"],"DependencyLinkMetadata":["NugetDependencyLinkMetadata"],"DesignFields":["Design","DesignAtVersion"],"Entry":["Blob","Submodule","TreeEntry"],"Eventable":["BoardEpic","Epic"],"Issuable":["Epic","Issue","MergeRequest"],"JobNeedUnion":["CiBuildNeed","CiJob"],"MemberInterface":["GroupMember","ProjectMember"],"NoteableInterface":["AlertManagementAlert","BoardEpic","Design","Epic","EpicIssue","Issue","MergeRequest","Snippet","Vulnerability"],"NoteableType":["Design","Issue","MergeRequest"],"OrchestrationPolicy":["ScanExecutionPolicy","ScanResultPolicy"],"PackageFileMetadata":["ConanFileMetadata","HelmFileMetadata"],"PackageMetadata":["ComposerMetadata","ConanMetadata","MavenMetadata","NugetMetadata","PypiMetadata"],"ResolvableInterface":["Discussion","Note"],"Service":["BaseService","JiraService"],"TimeboxReportInterface":["Iteration","Milestone"],"User":["MergeRequestAssignee","MergeRequestReviewer","UserCore"],"VulnerabilityDetail":["VulnerabilityDetailBase","VulnerabilityDetailBoolean","VulnerabilityDetailCode","VulnerabilityDetailCommit","VulnerabilityDetailDiff","VulnerabilityDetailFileLocation","VulnerabilityDetailInt","VulnerabilityDetailList","VulnerabilityDetailMarkdown","VulnerabilityDetailModuleLocation","VulnerabilityDetailTable","VulnerabilityDetailText","VulnerabilityDetailUrl"],"VulnerabilityLocation":["VulnerabilityLocationClusterImageScanning","VulnerabilityLocationContainerScanning","VulnerabilityLocationCoverageFuzzing","VulnerabilityLocationDast","VulnerabilityLocationDependencyScanning","VulnerabilityLocationGeneric","VulnerabilityLocationSast","VulnerabilityLocationSecretDetection"]}

View File

@ -181,6 +181,9 @@ export default {
return data[this.namespace]?.issues.nodes ?? [];
},
result({ data }) {
if (!data) {
return;
}
this.pageInfo = data[this.namespace]?.issues.pageInfo ?? {};
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
},

View File

@ -1,4 +1,5 @@
fragment IssueFragment on Issue {
__typename
id
iid
closedAt
@ -18,6 +19,7 @@ fragment IssueFragment on Issue {
webUrl
assignees {
nodes {
__typename
id
avatarUrl
name
@ -26,6 +28,7 @@ fragment IssueFragment on Issue {
}
}
author {
__typename
id
avatarUrl
name

View File

@ -51,7 +51,9 @@ export default {
},
data() {
return {
jobs: {},
jobs: {
list: [],
},
hasError: false,
isAlertDismissed: false,
scope: null,

View File

@ -1,4 +1,4 @@
import { ApolloLink } from 'apollo-link';
import { ApolloLink } from '@apollo/client/core';
import { memoize } from 'lodash';
export const FEATURE_CATEGORY_HEADER = 'x-gitlab-feature-category';

View File

@ -1,5 +1,5 @@
import { Observable } from 'apollo-link';
import { onError } from 'apollo-link-error';
import { Observable } from '@apollo/client/core';
import { onError } from '@apollo/client/link/error';
import { isNavigatingAway } from '~/lib/utils/is_navigating_away';
/**

View File

@ -1,11 +1,9 @@
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloClient } from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import { BatchHttpLink } from 'apollo-link-batch-http';
import { HttpLink } from 'apollo-link-http';
import { ApolloClient, InMemoryCache, ApolloLink, HttpLink } from '@apollo/client/core';
import { BatchHttpLink } from '@apollo/client/link/batch-http';
import { createUploadLink } from 'apollo-upload-client';
import ActionCableLink from '~/actioncable_link';
import { apolloCaptchaLink } from '~/captcha/apollo_captcha_link';
import possibleTypes from '~/graphql_shared/possibleTypes.json';
import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
import csrf from '~/lib/utils/csrf';
import { objectToQuery, queryToObject } from '~/lib/utils/url_utility';
@ -21,6 +19,33 @@ export const fetchPolicies = {
CACHE_ONLY: 'cache-only',
};
export const typePolicies = {
Repository: {
merge: true,
},
UserPermissions: {
merge: true,
},
MergeRequestPermissions: {
merge: true,
},
ContainerRepositoryConnection: {
merge: true,
},
TimelogConnection: {
merge: true,
},
BranchList: {
merge: true,
},
InstanceSecurityDashboard: {
merge: true,
},
PipelinePermissions: {
merge: true,
},
};
export const stripWhitespaceFromQuery = (url, path) => {
/* eslint-disable-next-line no-unused-vars */
const [_, params] = url.split(path);
@ -46,6 +71,30 @@ export const stripWhitespaceFromQuery = (url, path) => {
return `${path}?${reassembled}`;
};
const acs = [];
let pendingApolloMutations = 0;
// ### Why track pendingApolloMutations, but calculate pendingApolloRequests?
//
// In Apollo 2, we had a single link for counting operations.
//
// With Apollo 3, the `forward().map(...)` of deduped queries is never called.
// So, we resorted to calculating the sum of `inFlightLinkObservables?.size`.
// However! Mutations don't use `inFLightLinkObservables`, but since they are likely
// not deduped we can count them...
//
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55062#note_838943715
// https://www.apollographql.com/docs/react/v2/networking/network-layer/#query-deduplication
Object.defineProperty(window, 'pendingApolloRequests', {
get() {
return acs.reduce(
(sum, ac) => sum + (ac?.queryManager?.inFlightLinkObservables?.size || 0),
pendingApolloMutations,
);
},
});
export default (resolvers = {}, config = {}) => {
const {
baseUrl,
@ -56,6 +105,7 @@ export default (resolvers = {}, config = {}) => {
path = '/api/graphql',
useGet = false,
} = config;
let ac = null;
let uri = `${gon.relative_url_root || ''}${path}`;
if (baseUrl) {
@ -75,16 +125,6 @@ export default (resolvers = {}, config = {}) => {
batchMax,
};
const requestCounterLink = new ApolloLink((operation, forward) => {
window.pendingApolloRequests = window.pendingApolloRequests || 0;
window.pendingApolloRequests += 1;
return forward(operation).map((response) => {
window.pendingApolloRequests -= 1;
return response;
});
});
/*
This custom fetcher intervention is to deal with an issue where we are using GET to access
eTag polling, but Apollo Client adds excessive whitespace, which causes the
@ -138,6 +178,22 @@ export default (resolvers = {}, config = {}) => {
);
};
const hasMutation = (operation) =>
(operation?.query?.definitions || []).some((x) => x.operation === 'mutation');
const requestCounterLink = new ApolloLink((operation, forward) => {
if (hasMutation(operation)) {
pendingApolloMutations += 1;
}
return forward(operation).map((response) => {
if (hasMutation(operation)) {
pendingApolloMutations -= 1;
}
return response;
});
});
const appLink = ApolloLink.split(
hasSubscriptionOperation,
new ActionCableLink(),
@ -155,19 +211,23 @@ export default (resolvers = {}, config = {}) => {
),
);
return new ApolloClient({
ac = new ApolloClient({
typeDefs,
link: appLink,
cache: new InMemoryCache({
typePolicies,
possibleTypes,
...cacheConfig,
freezeResults: true,
}),
resolvers,
assumeImmutableResults: true,
defaultOptions: {
query: {
fetchPolicy,
},
},
});
acs.push(ac);
return ac;
};

View File

@ -1,4 +1,4 @@
import { ApolloLink, Observable } from 'apollo-link';
import { ApolloLink, Observable } from '@apollo/client/core';
import { parse } from 'graphql';
import { isEqual, pickBy } from 'lodash';

View File

@ -96,6 +96,9 @@ export default {
return data[this.graphqlResource]?.containerRepositories.nodes;
},
result({ data }) {
if (!data) {
return;
}
this.pageInfo = data[this.graphqlResource]?.containerRepositories?.pageInfo;
this.containerRepositoriesCount = data[this.graphqlResource]?.containerRepositoriesCount;
},

View File

@ -1,17 +0,0 @@
{
"__schema": {
"types": [
{
"kind": "UNION",
"name": "PackageMetadata",
"possibleTypes": [
{ "name": "ComposerMetadata" },
{ "name": "ConanMetadata" },
{ "name": "MavenMetadata" },
{ "name": "NugetMetadata" },
{ "name": "PypiMetadata" }
]
}
]
}
}

View File

@ -1,22 +1,9 @@
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import introspectionQueryResultData from './fragmentTypes.json';
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
});
Vue.use(VueApollo);
export const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
cacheConfig: {
fragmentMatcher,
},
},
),
defaultClient: createDefaultClient(),
});

View File

@ -2,7 +2,8 @@
import { GlProgressBar, GlSprintf, GlAlert } from '@gitlab/ui';
import eventHub from '~/invite_members/event_hub';
import { s__ } from '~/locale';
import { ACTION_LABELS, ACTION_SECTIONS } from '../constants';
import { getCookie, removeCookie, parseBoolean } from '~/lib/utils/common_utils';
import { ACTION_LABELS, ACTION_SECTIONS, INVITE_MODAL_OPEN_COOKIE } from '../constants';
import LearnGitlabSectionCard from './learn_gitlab_section_card.vue';
export default {
@ -26,7 +27,7 @@ export default {
required: true,
type: Object,
},
inviteMembersOpen: {
inviteMembers: {
type: Boolean,
required: false,
default: false,
@ -53,7 +54,7 @@ export default {
},
},
mounted() {
if (this.inviteMembersOpen) {
if (this.inviteMembers && this.getCookieForInviteMembers()) {
this.openInviteMembersModal('celebrate');
}
@ -63,6 +64,13 @@ export default {
eventHub.$off('showSuccessfulInvitationsAlert', this.handleShowSuccessfulInvitationsAlert);
},
methods: {
getCookieForInviteMembers() {
const value = parseBoolean(getCookie(INVITE_MODAL_OPEN_COOKIE));
removeCookie(INVITE_MODAL_OPEN_COOKIE);
return value;
},
openInviteMembersModal(mode) {
eventHub.$emit('openModal', { mode, inviteeType: 'members', source: 'learn-gitlab' });
},

View File

@ -95,3 +95,5 @@ export const ACTION_SECTIONS = {
),
},
};
export const INVITE_MODAL_OPEN_COOKIE = 'confetti_post_signup';

View File

@ -1,6 +1,6 @@
import Vue from 'vue';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import LearnGitlab from '../components/learn_gitlab.vue';
function initLearnGitlab() {
@ -13,13 +13,13 @@ function initLearnGitlab() {
const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
const sections = convertObjectPropsToCamelCase(JSON.parse(el.dataset.sections));
const project = convertObjectPropsToCamelCase(JSON.parse(el.dataset.project));
const { inviteMembersOpen } = el.dataset;
const { inviteMembers } = el.dataset;
return new Vue({
el,
render(createElement) {
return createElement(LearnGitlab, {
props: { actions, sections, project, inviteMembersOpen },
props: { actions, sections, project, inviteMembers: parseBoolean(inviteMembers) },
});
},
});

View File

@ -49,7 +49,7 @@ export default {
pipelineEtag: {
query: getPipelineEtag,
update(data) {
return data.etags.pipeline;
return data.etags?.pipeline;
},
},
pipeline: {

View File

@ -196,7 +196,7 @@ export default {
currentBranch: {
query: getCurrentBranch,
update(data) {
return data.workBranches.current.name;
return data.workBranches?.current?.name;
},
},
starterTemplate: {
@ -214,7 +214,7 @@ export default {
return data.project?.ciTemplate?.content || '';
},
result({ data }) {
this.updateCiConfig(data.project?.ciTemplate?.content || '');
this.updateCiConfig(data?.project?.ciTemplate?.content || '');
},
error() {
this.reportFailure(LOAD_FAILURE_UNKNOWN);

View File

@ -36,6 +36,9 @@ export default {
return data.project?.pipeline?.jobs?.nodes || [];
},
result({ data }) {
if (!data) {
return;
}
this.jobsPageInfo = data.project?.pipeline?.jobs?.pageInfo || {};
},
error() {

View File

@ -1 +0,0 @@
{"__schema":{"types":[{"kind":"UNION","name":"JobNeedUnion","possibleTypes":[{"name":"CiBuildNeed"},{"name":"CiJob"}]}]}}

View File

@ -1,19 +1,10 @@
import VueApollo from 'vue-apollo';
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql';
import introspectionQueryResultData from './graphql/fragmentTypes.json';
export const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
});
export const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
cacheConfig: {
fragmentMatcher,
},
useGet: true,
},
),

View File

@ -9,6 +9,8 @@ import {
linkedIssueTypesMap,
addRelatedIssueErrorMap,
addRelatedItemErrorMap,
issuablesFormCategoryHeaderTextMap,
issuablesFormInputTextMap,
} from '../constants';
import RelatedIssuableInput from './related_issuable_input.vue';
@ -134,6 +136,12 @@ export default {
epics: mergeUrlParams({ confidential_only: true }, this.autoCompleteSources.epics),
};
},
issuableCategoryHeaderText() {
return issuablesFormCategoryHeaderTextMap[this.issuableType];
},
issuableInputText() {
return issuablesFormInputTextMap[this.issuableType];
},
},
methods: {
onPendingIssuableRemoveRequest(params) {
@ -162,7 +170,7 @@ export default {
<form @submit.prevent="onFormSubmit">
<template v-if="showCategorizedIssues">
<gl-form-group
:label="__('The current issue')"
:label="issuableCategoryHeaderText"
label-for="linked-issue-type-radio"
label-class="label-bold"
class="mb-2"
@ -175,7 +183,7 @@ export default {
/>
</gl-form-group>
<p class="bold">
{{ __('the following issue(s)') }}
{{ issuableInputText }}
</p>
</template>
<related-issuable-input

View File

@ -5,6 +5,9 @@ import {
issuableQaClassMap,
linkedIssueTypesMap,
linkedIssueTypesTextMap,
issuablesBlockHeaderTextMap,
issuablesBlockHelpTextMap,
issuablesBlockAddButtonTextMap,
} from '../constants';
import AddIssuableForm from './add_issuable_form.vue';
import RelatedIssuesList from './related_issues_list.vue';
@ -105,6 +108,15 @@ export default {
hasBody() {
return this.isFormVisible || this.shouldShowTokenBody;
},
headerText() {
return issuablesBlockHeaderTextMap[this.issuableType];
},
helpLinkText() {
return issuablesBlockHelpTextMap[this.issuableType];
},
addIssuableButtonText() {
return issuablesBlockAddButtonTextMap[this.issuableType];
},
badgeLabel() {
return this.isFetching && this.relatedIssues.length === 0 ? '...' : this.relatedIssues.length;
},
@ -138,13 +150,14 @@ export default {
href="#related-issues"
aria-hidden="true"
/>
<slot name="header-text">{{ __('Linked issues') }}</slot>
<slot name="header-text">{{ headerText }}</slot>
<gl-link
v-if="hasHelpPath"
:href="helpPath"
target="_blank"
class="gl-display-flex gl-align-items-center gl-ml-2 gl-text-gray-500"
:aria-label="__('Read more about related issues')"
data-testid="help-link"
:aria-label="helpLinkText"
>
<gl-icon name="question" :size="12" />
</gl-link>
@ -160,7 +173,7 @@ export default {
v-if="canAdmin"
data-qa-selector="related_issues_plus_button"
icon="plus"
:aria-label="__('Add a related issue')"
:aria-label="addIssuableButtonText"
:class="qaClass"
@click="$emit('toggleAddRelatedIssuesForm', $event)"
/>

View File

@ -104,3 +104,28 @@ export const PathIdSeparator = {
Epic: '&',
Issue: '#',
};
export const issuablesBlockHeaderTextMap = {
[issuableTypesMap.ISSUE]: __('Linked issues'),
[issuableTypesMap.EPIC]: __('Linked epics'),
};
export const issuablesBlockHelpTextMap = {
[issuableTypesMap.ISSUE]: __('Read more about related issues'),
[issuableTypesMap.EPIC]: __('Read more about related epics'),
};
export const issuablesBlockAddButtonTextMap = {
[issuableTypesMap.ISSUE]: __('Add a related issue'),
[issuableTypesMap.EPIC]: __('Add a related epic'),
};
export const issuablesFormCategoryHeaderTextMap = {
[issuableTypesMap.ISSUE]: __('The current issue'),
[issuableTypesMap.EPIC]: __('The current epic'),
};
export const issuablesFormInputTextMap = {
[issuableTypesMap.ISSUE]: __('the following issue(s)'),
[issuableTypesMap.EPIC]: __('the following epic(s)'),
};

View File

@ -8,6 +8,7 @@ export default function initRelatedIssues() {
// eslint-disable-next-line no-new
new Vue({
el: relatedIssuesRootElement,
name: 'RelatedIssuesApp',
components: {
relatedIssuesRoot: RelatedIssuesRoot,
},

View File

@ -1 +0,0 @@
{"__schema":{"types":[{"kind":"INTERFACE","name":"Entry","possibleTypes":[{"name":"Blob"},{"name":"Submodule"},{"name":"TreeEntry"}]}]}}

View File

@ -1,19 +1,11 @@
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import introspectionQueryResultData from './fragmentTypes.json';
import { fetchLogsTree } from './log_tree';
Vue.use(VueApollo);
// We create a fragment matcher so that we can create a fragment from an interface
// Without this, Apollo throws a heuristic fragment matcher warning
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
});
const defaultClient = createDefaultClient(
{
Query: {
@ -43,7 +35,6 @@ const defaultClient = createDefaultClient(
},
{
cacheConfig: {
fragmentMatcher,
dataIdFromObject: (obj) => {
/* eslint-disable @gitlab/require-i18n-strings */
// eslint-disable-next-line no-underscore-dangle

View File

@ -28,10 +28,12 @@ query getGroupRunners(
edges {
webUrl
node {
__typename
...RunnerNode
}
}
pageInfo {
__typename
...PageInfo
}
}

View File

@ -4,6 +4,7 @@ query getRunner($id: CiRunnerID!) {
# We have an id in deeply nested fragment
# eslint-disable-next-line @graphql-eslint/require-id-when-available
runner(id: $id) {
__typename
...RunnerDetails
}
}

View File

@ -29,6 +29,7 @@ query getRunners(
editAdminUrl
}
pageInfo {
__typename
...PageInfo
}
}

View File

@ -1,4 +1,5 @@
fragment RunnerNode on CiRunner {
__typename
id
description
runnerType

View File

@ -96,6 +96,9 @@ export default {
return data.workspace?.issuable;
},
result({ data }) {
if (!data) {
return;
}
const issuable = data.workspace?.issuable;
if (issuable) {
this.selected = cloneDeep(issuable.assignees.nodes);

View File

@ -66,6 +66,9 @@ export default {
return data.workspace?.issuable?.confidential || false;
},
result({ data }) {
if (!data) {
return;
}
this.$emit('confidentialityUpdated', data.workspace?.issuable?.confidential);
},
error() {

View File

@ -86,6 +86,9 @@ export default {
return data.workspace?.issuable || {};
},
result({ data }) {
if (!data) {
return;
}
this.$emit(`${this.dateType}Updated`, data.workspace?.issuable?.[this.dateType]);
},
error() {

View File

@ -61,6 +61,9 @@ export default {
return data.workspace?.issuable?.subscribed || false;
},
result({ data }) {
if (!data) {
return;
}
this.emailsDisabled = this.parentIsGroup
? data.workspace?.emailsDisabled
: data.workspace?.issuable?.emailsDisabled;

View File

@ -59,6 +59,10 @@ export default {
return data.workspace?.issuable?.currentUserTodos.nodes[0]?.id;
},
result({ data }) {
if (!data) {
return;
}
const currentUserTodos = data.workspace?.issuable?.currentUserTodos?.nodes ?? [];
this.todoId = currentUserTodos[0]?.id;
this.$emit('todoUpdated', currentUserTodos.length > 0);

View File

@ -1 +0,0 @@
{"__schema":{"types":[{"kind":"UNION","name":"Issuable","possibleTypes":[{"name":"Issue"},{"name":"MergeRequest"}]}, {"kind":"INTERFACE","name":"User","possibleTypes":[{"name":"UserCore"}]}]}}

View File

@ -1,14 +1,8 @@
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import produce from 'immer';
import VueApollo from 'vue-apollo';
import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql';
import { resolvers as workItemResolvers } from '~/work_items/graphql/resolvers';
import createDefaultClient from '~/lib/graphql';
import introspectionQueryResultData from './fragmentTypes.json';
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
});
const resolvers = {
...workItemResolvers,
@ -24,11 +18,7 @@ const resolvers = {
},
};
export const defaultClient = createDefaultClient(resolvers, {
cacheConfig: {
fragmentMatcher,
},
});
export const defaultClient = createDefaultClient(resolvers);
export const apolloProvider = new VueApollo({
defaultClient,

View File

@ -1,5 +1,5 @@
import { defaultDataIdFromObject } from '@apollo/client/core';
import { GlToast } from '@gitlab/ui';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';

View File

@ -3,7 +3,6 @@ query getState($projectPath: ID!, $iid: String!) {
id
archived
onlyAllowMergeIfPipelineSucceeds
mergeRequest(iid: $iid) {
id
autoMergeEnabled

View File

@ -4,6 +4,7 @@ query autoMergeEnabled($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
id
mergeRequest(iid: $iid) {
id
...autoMergeEnabled
}
}

View File

@ -2,6 +2,7 @@
query readyToMerge($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
id
...ReadyToMerge
}
}

View File

@ -1,4 +1,4 @@
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import { defaultDataIdFromObject } from '@apollo/client/core';
import produce from 'immer';
import Vue from 'vue';
import VueApollo from 'vue-apollo';

View File

@ -31,10 +31,6 @@ export default {
type: Object,
required: true,
},
enableLabelPermalinks: {
type: Boolean,
required: true,
},
labelFilterParam: {
type: String,
required: false,
@ -142,11 +138,8 @@ export default {
return label.title || label.name;
},
labelTarget(label) {
if (this.enableLabelPermalinks) {
const value = encodeURIComponent(this.labelTitle(label));
return `?${this.labelFilterParam}[]=${value}`;
}
return '#';
const value = encodeURIComponent(this.labelTitle(label));
return `?${this.labelFilterParam}[]=${value}`;
},
/**
* This is needed as an independent method since

View File

@ -133,11 +133,6 @@ export default {
required: false,
default: 2,
},
enableLabelPermalinks: {
type: Boolean,
required: false,
default: true,
},
labelFilterParam: {
type: String,
required: false,
@ -321,7 +316,6 @@ export default {
:data-qa-issuable-title="issuable.title"
:issuable-symbol="issuableSymbol"
:issuable="issuable"
:enable-label-permalinks="enableLabelPermalinks"
:label-filter-param="labelFilterParam"
:show-checkbox="showBulkEditSidebar"
:checked="issuableChecked(issuable)"

View File

@ -102,8 +102,8 @@ export default {
error(error) {
this.showError(error);
},
result({ loading }) {
if (loading) {
result({ loading, data }) {
if (loading || !data) {
return;
}

View File

@ -1 +0,0 @@
{"__schema":{"types":[{"kind":"INTERFACE","name":"LocalWorkItemWidget","possibleTypes":[{"name":"LocalTitleWidget"}]}]}}

View File

@ -1,23 +1,14 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql';
import workItemQuery from './work_item.query.graphql';
import introspectionQueryResultData from './fragmentTypes.json';
import { resolvers } from './resolvers';
import typeDefs from './typedefs.graphql';
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
});
export function createApolloProvider() {
Vue.use(VueApollo);
const defaultClient = createDefaultClient(resolvers, {
cacheConfig: {
fragmentMatcher,
},
typeDefs,
});

View File

@ -34,7 +34,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
def destroy
@runner.destroy
Ci::UnregisterRunnerService.new(@runner).execute
redirect_to admin_runners_path, status: :found
end

View File

@ -35,7 +35,7 @@ class Groups::RunnersController < Groups::ApplicationController
if @runner.belongs_to_more_than_one_project?
redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), status: :found, alert: _('Runner was not deleted because it is assigned to multiple projects.')
else
@runner.destroy
Ci::UnregisterRunnerService.new(@runner).execute
redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), status: :found
end

View File

@ -23,7 +23,7 @@ class Projects::RunnersController < Projects::ApplicationController
def destroy
if @runner.only_for?(project)
@runner.destroy
Ci::UnregisterRunnerService.new(@runner).execute
end
redirect_to project_runners_path(@project), status: :found

View File

@ -20,7 +20,7 @@ module Mutations
error = authenticate_delete_runner!(runner)
return { errors: [error] } if error
runner.destroy!
::Ci::UnregisterRunnerService.new(runner).execute
{ errors: runner.errors.full_messages }
end

View File

@ -32,13 +32,13 @@ module Mutations
params = global_id_compatibility_params(attributes).merge(author_id: current_user.id)
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
work_item = ::WorkItems::CreateService.new(project: project, current_user: current_user, params: params, spam_params: spam_params).execute
create_result = ::WorkItems::CreateService.new(project: project, current_user: current_user, params: params, spam_params: spam_params).execute
check_spam_action_response!(work_item)
check_spam_action_response!(create_result[:work_item]) if create_result[:work_item]
{
work_item: work_item.valid? ? work_item : nil,
errors: errors_on_object(work_item)
work_item: create_result.success? ? create_result[:work_item] : nil,
errors: create_result.errors
}
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module Ci
class UnregisterRunnerService
attr_reader :runner
# @param [Ci::Runner] runner the runner to unregister/destroy
def initialize(runner)
@runner = runner
end
def execute
@runner&.destroy
end
end
end

View File

@ -2,6 +2,8 @@
module WorkItems
class CreateService
include ::Services::ReturnServiceResponses
def initialize(project:, current_user: nil, params: {}, spam_params:)
@create_service = ::Issues::CreateService.new(
project: project,
@ -10,10 +12,28 @@ module WorkItems
spam_params: spam_params,
build_service: ::WorkItems::BuildService.new(project: project, current_user: current_user, params: params)
)
@current_user = current_user
@project = project
end
def execute
@create_service.execute
unless @current_user.can?(:create_work_item, @project)
return error(_('Operation not allowed'), :forbidden)
end
work_item = @create_service.execute
if work_item.valid?
success(payload(work_item))
else
error(work_item.errors.full_messages, :unprocessable_entity, pass_back: payload(work_item))
end
end
private
def payload(work_item)
{ work_item: work_item }
end
end
end

View File

@ -5,4 +5,4 @@
= form_tag path do
%input{ :name => "_method", :type => "hidden", :value => "delete" }/
= submit_tag _('Revoke'), class: 'gl-button btn btn-danger btn-sm', data: { confirm: _('Are you sure?') }
= submit_tag _('Revoke'), class: 'gl-button btn btn-danger btn-sm', aria: { label: s_('AuthorizedApplication|Revoke application') }, data: { confirm: s_('AuthorizedApplication|Are you sure you want to revoke this application?'), confirm_btn_variant: 'danger' }

View File

@ -2,7 +2,6 @@
- page_title _("Learn GitLab")
- add_page_specific_style 'page_bundles/learn_gitlab'
- data = learn_gitlab_data(@project)
- invite_members_open = session.delete(:confetti_post_signup)
= render 'projects/invite_members_modal', project: @project
@ -10,4 +9,4 @@
- e.control do
#js-learn-gitlab-app{ data: data }
- e.candidate do
#js-learn-gitlab-app{ data: data.merge(invite_members_open: invite_members_open) }
#js-learn-gitlab-app{ data: data.merge(invite_members: 'true') }

View File

@ -150,6 +150,8 @@ function generateEntries() {
}
const alias = {
// Map Apollo client to apollo/client/core to prevent react related imports from being loaded
'@apollo/client$': '@apollo/client/core',
'~': path.join(ROOT_PATH, 'app/assets/javascripts'),
emojis: path.join(ROOT_PATH, 'fixtures/emojis'),
empty_states: path.join(ROOT_PATH, 'app/views/shared/empty_states'),

View File

@ -47,7 +47,7 @@ module.exports = {
'bootstrap/dist/js/bootstrap.js',
'sortablejs/modular/sortable.esm.js',
'popper.js',
'apollo-client',
'@apollo/client/core',
'source-map',
'mousetrap',
],

View File

@ -152,7 +152,7 @@ Root-level queries are defined in
### Multiplex queries
GitLab supports batching queries into a single request using
[apollo-link-batch-http](https://www.apollographql.com/docs/link/links/batch-http/). More
[`@apollo/client/link/batch-http`](https://www.apollographql.com/docs/react/api/link/apollo-link-batch-http/). More
information about multiplexed queries is also available for
[GraphQL Ruby](https://graphql-ruby.org/queries/multiplex.html), the
library GitLab uses on the backend.

View File

@ -171,6 +171,7 @@ POST /projects/:id/ci/lint
| `content` | string | yes | The CI/CD configuration content. |
| `dry_run` | boolean | no | Run [pipeline creation simulation](../ci/lint.md#simulate-a-pipeline), or only do static check. This is false by default. |
| `include_jobs` | boolean | no | If the list of jobs that would exist in a static check or pipeline simulation should be included in the response. This is false by default. |
| `ref` | string | no | When `dry_run` is `true`, sets the branch or tag to use. Defaults to the project's default branch when not set. |
Example request:
@ -220,6 +221,7 @@ GET /projects/:id/ci/lint
| ---------- | ------- | -------- | -------- |
| `dry_run` | boolean | no | Run pipeline creation simulation, or only do static check. |
| `include_jobs` | boolean | no | If the list of jobs that would exist in a static check or pipeline simulation should be included in the response. This is false by default. |
| `ref` | string | no | When `dry_run` is `true`, sets the branch or tag to use. Defaults to the project's default branch when not set. |
Example request:

View File

@ -73,10 +73,8 @@ The chart indicates the project's progress throughout that milestone (for issues
In particular, it shows how many issues were or are still open for a given day in the
milestone's corresponding period.
The burndown chart can also be toggled to display the cumulative open issue
weight for a given day. When using this feature, make sure issue weights have
been properly assigned, since an open issue with no weight adds zero to the
cumulative value.
You can also toggle the burndown chart to display the
[cumulative open issue weight](#switch-between-number-of-issues-and-issue-weight) for a given day.
### Fixed burndown charts
@ -123,12 +121,21 @@ To view a group's burnup chart:
### How burnup charts work
Burnup charts have separate lines for total work and completed work. The total line
shows when scope is reduced or added to a milestone. The completed work is a count
of issues closed.
shows changes to the scope of a milestone. When an open issue is moved to another
milestone, the "total issues" goes down but the "completed issues" stays the same.
The completed work is a count of issues closed. When an issue is closed, the "total
issues" remains the same and "completed issues" goes up.
Burnup charts can show either the total number of issues or total weight for each
day of the milestone. Use the toggle above the charts to switch between total
and weight.
## Switch between number of issues and issue weight
In both burndown or burnup charts you can view them
either by the total number of issues
or the total weight for each day of the milestone.
To switch between the two settings, select either **Issues** or **Issue weight** above the charts.
When sorting by weight, make sure all your issues
have weight assigned, because issues with no weight don't show on the chart.
<!-- ## Troubleshooting

View File

@ -151,10 +151,10 @@ There are also tabs below these that show the following:
### Burndown Charts
The milestone view contains a [burndown chart](burndown_and_burnup_charts.md),
The milestone view contains a [burndown and burnup chart](burndown_and_burnup_charts.md),
showing the progress of completing a milestone.
![burndown chart](img/burndown_chart_v13_6.png)
![burndown chart](img/burndown_and_burnup_charts_v13_6.png)
### Milestone sidebar

View File

@ -57,7 +57,7 @@ module API
delete '/', feature_category: :runner do
authenticate_runner!
destroy_conditionally!(current_runner)
destroy_conditionally!(current_runner) { ::Ci::UnregisterRunnerService.new(current_runner).execute }
end
desc 'Validates authentication credentials' do

View File

@ -110,7 +110,7 @@ module API
authenticate_delete_runner!(runner)
destroy_conditionally!(runner)
destroy_conditionally!(runner) { ::Ci::UnregisterRunnerService.new(runner).execute }
end
desc 'List jobs running on a runner' do

View File

@ -42,6 +42,7 @@ module API
params do
optional :dry_run, type: Boolean, default: false, desc: 'Run pipeline creation simulation, or only do static check.'
optional :include_jobs, type: Boolean, desc: 'Whether or not to include CI jobs in the response'
optional :ref, type: String, desc: 'Branch or tag used to execute a dry run. Defaults to the default branch of the project. Only used when dry_run is true'
end
get ':id/ci/lint', urgency: :low do
authorize! :download_code, user_project
@ -52,7 +53,7 @@ module API
result = Gitlab::Ci::Lint
.new(project: user_project, current_user: current_user)
.validate(content, dry_run: params[:dry_run])
.validate(content, dry_run: params[:dry_run], ref: params[:ref] || user_project.default_branch)
present result, with: Entities::Ci::Lint::Result, current_user: current_user, include_jobs: params[:include_jobs]
end
@ -66,13 +67,14 @@ module API
requires :content, type: String, desc: 'Content of .gitlab-ci.yml'
optional :dry_run, type: Boolean, default: false, desc: 'Run pipeline creation simulation, or only do static check.'
optional :include_jobs, type: Boolean, desc: 'Whether or not to include CI jobs in the response'
optional :ref, type: String, desc: 'Branch or tag used to execute a dry run. Defaults to the default branch of the project. Only used when dry_run is true'
end
post ':id/ci/lint', urgency: :low do
authorize! :create_pipeline, user_project
result = Gitlab::Ci::Lint
.new(project: user_project, current_user: current_user)
.validate(params[:content], dry_run: params[:dry_run])
.validate(params[:content], dry_run: params[:dry_run], ref: params[:ref] || user_project.default_branch)
status 200
present result, with: Entities::Ci::Lint::Result, current_user: current_user, include_jobs: params[:include_jobs]

View File

@ -24,9 +24,9 @@ module Gitlab
@sha = sha || project&.repository&.commit&.sha
end
def validate(content, dry_run: false)
def validate(content, dry_run: false, ref: @project&.default_branch)
if dry_run
simulate_pipeline_creation(content)
simulate_pipeline_creation(content, ref)
else
static_validation(content)
end
@ -34,9 +34,9 @@ module Gitlab
private
def simulate_pipeline_creation(content)
def simulate_pipeline_creation(content, ref)
pipeline = ::Ci::CreatePipelineService
.new(@project, @current_user, ref: @project.default_branch)
.new(@project, @current_user, ref: ref)
.execute(:push, dry_run: true, content: content)
.payload

View File

@ -2091,6 +2091,9 @@ msgstr ""
msgid "Add a numbered list"
msgstr ""
msgid "Add a related epic"
msgstr ""
msgid "Add a related issue"
msgstr ""
@ -5158,6 +5161,12 @@ msgstr ""
msgid "Authorized applications (%{size})"
msgstr ""
msgid "AuthorizedApplication|Are you sure you want to revoke this application?"
msgstr ""
msgid "AuthorizedApplication|Revoke application"
msgstr ""
msgid "Authors: %{authors}"
msgstr ""
@ -21755,6 +21764,9 @@ msgstr ""
msgid "Linked emails (%{email_count})"
msgstr ""
msgid "Linked epics"
msgstr ""
msgid "Linked issues"
msgstr ""
@ -29685,6 +29697,9 @@ msgstr ""
msgid "Read more about project permissions %{help_link_open}here%{help_link_close}"
msgstr ""
msgid "Read more about related epics"
msgstr ""
msgid "Read more about related issues"
msgstr ""
@ -36091,6 +36106,9 @@ msgstr ""
msgid "The contents of this group, its subgroups and projects will be permanently removed after %{deletion_adjourned_period} days on %{date}. After this point, your data cannot be recovered."
msgstr ""
msgid "The current epic"
msgstr ""
msgid "The current issue"
msgstr ""
@ -44006,6 +44024,9 @@ msgstr ""
msgid "the file"
msgstr ""
msgid "the following epic(s)"
msgstr ""
msgid "the following issue(s)"
msgstr ""

View File

@ -47,6 +47,7 @@
"webpack-prod": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
"@apollo/client": "^3.3.11",
"@babel/core": "^7.10.1",
"@babel/plugin-proposal-class-properties": "^7.10.1",
"@babel/plugin-proposal-json-strings": "^7.10.1",
@ -94,13 +95,7 @@
"@tiptap/vue-2": "^2.0.0-beta.77",
"@toast-ui/editor": "^2.5.2",
"@toast-ui/vue-editor": "^2.5.2",
"apollo-cache-inmemory": "^1.6.6",
"apollo-client": "^2.6.10",
"apollo-link": "^1.2.14",
"apollo-link-batch-http": "^1.2.14",
"apollo-link-error": "^1.1.13",
"apollo-link-http": "^1.5.17",
"apollo-upload-client": "^13.0.0",
"apollo-upload-client": "^14.1.3",
"autosize": "^5.0.1",
"aws-sdk": "^2.637.0",
"axios": "^0.24.0",
@ -186,7 +181,7 @@
"uuid": "8.1.0",
"visibilityjs": "^1.2.4",
"vue": "^2.6.12",
"vue-apollo": "^3.0.3",
"vue-apollo": "^3.0.7",
"vue-loader": "^15.9.6",
"vue-observe-visibility": "^1.0.0",
"vue-resize": "^1.0.1",
@ -244,7 +239,7 @@
"markdownlint-cli": "0.31.0",
"md5": "^2.2.1",
"miragejs": "^0.1.40",
"mock-apollo-client": "^0.7.0",
"mock-apollo-client": "1.2.0",
"nodemon": "^2.0.4",
"postcss": "^8.4.5",
"prettier": "2.2.1",
@ -266,6 +261,7 @@
"bootstrap-vue": "https://docs.gitlab.com/ee/development/fe_guide/dependencies.html#bootstrapvue"
},
"resolutions": {
"@apollo/client/subscriptions-transport-ws": "0.11.0",
"chokidar": "^3.4.0",
"@types/node": "14.17.5"
},

View File

@ -4,9 +4,10 @@ require 'spec_helper'
RSpec.describe Admin::RunnersController do
let_it_be(:runner) { create(:ci_runner) }
let_it_be(:user) { create(:admin) }
before do
sign_in(create(:admin))
sign_in(user)
end
describe '#index' do
@ -104,6 +105,10 @@ RSpec.describe Admin::RunnersController do
describe '#destroy' do
it 'destroys the runner' do
expect_next_instance_of(Ci::UnregisterRunnerService, runner) do |service|
expect(service).to receive(:execute).once.and_call_original
end
delete :destroy, params: { id: runner.id }
expect(response).to have_gitlab_http_status(:found)

View File

@ -309,7 +309,8 @@ RSpec.describe Groups::ClustersController do
.to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
allow_any_instance_of(GoogleApi::CloudPlatform::Client)
.to receive(:projects_zones_clusters_create) do
OpenStruct.new(
double(
'instance',
self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123',
status: 'RUNNING'
)

View File

@ -190,6 +190,10 @@ RSpec.describe Groups::RunnersController do
end
it 'destroys the runner and redirects' do
expect_next_instance_of(Ci::UnregisterRunnerService, runner) do |service|
expect(service).to receive(:execute).once.and_call_original
end
delete :destroy, params: params
expect(response).to have_gitlab_http_status(:found)

View File

@ -37,6 +37,10 @@ RSpec.describe Projects::RunnersController do
describe '#destroy' do
it 'destroys the runner' do
expect_next_instance_of(Ci::UnregisterRunnerService, runner) do |service|
expect(service).to receive(:execute).once.and_call_original
end
delete :destroy, params: params
expect(response).to have_gitlab_http_status(:found)

View File

@ -99,7 +99,8 @@ RSpec.describe 'Clusters', :js do
allow_any_instance_of(GoogleApi::CloudPlatform::Client)
.to receive(:projects_zones_clusters_create) do
OpenStruct.new(
double(
'cluster_control',
self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123',
status: 'RUNNING'
)

View File

@ -1,15 +1,14 @@
import { InMemoryCache } from 'apollo-cache-inmemory';
import { InMemoryCache } from '@apollo/client/core';
import { createMockClient as createMockApolloClient } from 'mock-apollo-client';
import VueApollo from 'vue-apollo';
const defaultCacheOptions = {
fragmentMatcher: { match: () => true },
addTypename: false,
};
import possibleTypes from '~/graphql_shared/possibleTypes.json';
import { typePolicies } from '~/lib/graphql';
export function createMockClient(handlers = [], resolvers = {}, cacheOptions = {}) {
const cache = new InMemoryCache({
...defaultCacheOptions,
possibleTypes,
typePolicies,
addTypename: false,
...cacheOptions,
});

View File

@ -1,7 +1,4 @@
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloClient } from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import gql from 'graphql-tag';
import { InMemoryCache, ApolloClient, ApolloLink, gql } from '@apollo/client/core';
const FOO_QUERY = gql`
query {

View File

@ -1,5 +1,5 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { gql } from '@apollo/client/core';
import cable from '~/actioncable_consumer';
import ActionCableLink from '~/actioncable_link';

View File

@ -135,7 +135,6 @@ describe('AdminUsersTable component', () => {
});
it('creates a flash message and captures the error', () => {
expect(createFlash).toHaveBeenCalledTimes(2);
expect(createFlash).toHaveBeenCalledWith({
message: 'Could not load user group counts. Please refresh the page to try again.',
captureError: true,

View File

@ -1,7 +1,7 @@
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql';
import updateHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql';
@ -68,10 +68,7 @@ describe('AlertsSettingsWrapper', () => {
const findAlertsSettingsForm = () => wrapper.findComponent(AlertsSettingsForm);
const findAlert = () => wrapper.findComponent(GlAlert);
async function destroyHttpIntegration(localWrapper) {
await jest.runOnlyPendingTimers();
await nextTick();
function destroyHttpIntegration(localWrapper) {
localWrapper
.find(IntegrationsList)
.vm.$emit('delete-integration', { id: integrationToDestroy.id });
@ -474,11 +471,11 @@ describe('AlertsSettingsWrapper', () => {
it('calls a mutation with correct parameters and destroys a integration', async () => {
createComponentWithApollo();
await waitForPromises();
await destroyHttpIntegration(wrapper);
destroyHttpIntegration(wrapper);
expect(destroyIntegrationHandler).toHaveBeenCalled();
await waitForPromises();
expect(findIntegrations()).toHaveLength(3);

View File

@ -38,6 +38,7 @@ export const getIntegrationsQueryResponse = {
alertManagementIntegrations: {
nodes: [
{
__typename: 'AlertManagementIntegration',
id: '37',
type: 'HTTP',
active: true,
@ -48,6 +49,7 @@ export const getIntegrationsQueryResponse = {
apiUrl: null,
},
{
__typename: 'AlertManagementIntegration',
id: '41',
type: 'HTTP',
active: true,
@ -58,6 +60,7 @@ export const getIntegrationsQueryResponse = {
apiUrl: null,
},
{
__typename: 'AlertManagementIntegration',
id: '40',
type: 'HTTP',
active: true,
@ -68,6 +71,7 @@ export const getIntegrationsQueryResponse = {
apiUrl: null,
},
{
__typename: 'AlertManagementIntegration',
id: '12',
type: 'PROMETHEUS',
active: false,
@ -83,6 +87,7 @@ export const getIntegrationsQueryResponse = {
};
export const integrationToDestroy = {
__typename: 'AlertManagementIntegration',
id: '37',
type: 'HTTP',
active: true,
@ -97,6 +102,7 @@ export const destroyIntegrationResponse = {
httpIntegrationDestroy: {
errors: [],
integration: {
__typename: 'AlertManagementIntegration',
id: '37',
type: 'HTTP',
active: true,

View File

@ -1,4 +1,5 @@
const defaultPageInfo = {
__typename: 'PageInfo',
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,

View File

@ -129,7 +129,7 @@ describe('UsageTrendsCountChart', () => {
const recordedAt = '2020-08-01';
describe('when the fetchMore query returns data', () => {
beforeEach(async () => {
const newData = [{ recordedAt, count: 5 }];
const newData = [{ __typename: 'UsageTrendsMeasurement', recordedAt, count: 5 }];
queryHandler = mockQueryResponse({
key: queryResponseDataKey,
data: mockCountsData1,

View File

@ -4,6 +4,7 @@ import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import UsersChart from '~/analytics/usage_trends/components/users_chart.vue';
import usersQuery from '~/analytics/usage_trends/graphql/queries/users.query.graphql';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
@ -86,7 +87,7 @@ describe('UsersChart', () => {
describe('with data', () => {
beforeEach(async () => {
wrapper = createComponent({ users: mockCountsData2 });
await nextTick();
await waitForPromises();
});
it('hides the skeleton loader', () => {
@ -156,7 +157,7 @@ describe('UsersChart', () => {
jest
.spyOn(wrapper.vm.$apollo.queries.users, 'fetchMore')
.mockImplementation(jest.fn().mockRejectedValue());
await nextTick();
await waitForPromises();
});
it('calls fetchMore', () => {

View File

@ -4,11 +4,11 @@ export const mockUsageCounts = [
];
export const mockCountsData1 = [
{ recordedAt: '2020-07-23', count: 52 },
{ recordedAt: '2020-07-22', count: 40 },
{ recordedAt: '2020-07-21', count: 31 },
{ recordedAt: '2020-06-14', count: 23 },
{ recordedAt: '2020-06-12', count: 20 },
{ __typename: 'UsageTrendsMeasurement', recordedAt: '2020-07-23', count: 52 },
{ __typename: 'UsageTrendsMeasurement', recordedAt: '2020-07-22', count: 40 },
{ __typename: 'UsageTrendsMeasurement', recordedAt: '2020-07-21', count: 31 },
{ __typename: 'UsageTrendsMeasurement', recordedAt: '2020-06-14', count: 23 },
{ __typename: 'UsageTrendsMeasurement', recordedAt: '2020-06-12', count: 20 },
];
export const countsMonthlyChartData1 = [
@ -17,11 +17,11 @@ export const countsMonthlyChartData1 = [
];
export const mockCountsData2 = [
{ recordedAt: '2020-07-28', count: 10 },
{ recordedAt: '2020-07-27', count: 9 },
{ recordedAt: '2020-06-26', count: 14 },
{ recordedAt: '2020-06-25', count: 23 },
{ recordedAt: '2020-06-24', count: 25 },
{ __typename: 'UsageTrendsMeasurement', recordedAt: '2020-07-28', count: 10 },
{ __typename: 'UsageTrendsMeasurement', recordedAt: '2020-07-27', count: 9 },
{ __typename: 'UsageTrendsMeasurement', recordedAt: '2020-06-26', count: 14 },
{ __typename: 'UsageTrendsMeasurement', recordedAt: '2020-06-25', count: 23 },
{ __typename: 'UsageTrendsMeasurement', recordedAt: '2020-06-24', count: 25 },
];
export const countsMonthlyChartData2 = [

View File

@ -106,10 +106,6 @@ describe('Board list component', () => {
fetchItemsForList: jest.fn(),
};
beforeEach(() => {
wrapper = createComponent();
});
it('does not load issues if already loading', () => {
wrapper = createComponent({
actions,
@ -143,6 +139,7 @@ describe('Board list component', () => {
await nextTick();
await waitForPromises();
await nextTick();
await nextTick();
expect(wrapper.find('.board-list-count').text()).toBe('Showing 1 of 20 issues');
});

View File

@ -1,4 +1,4 @@
import { ApolloLink, Observable } from 'apollo-link';
import { ApolloLink, Observable } from '@apollo/client/core';
import { apolloCaptchaLink } from '~/captcha/apollo_captcha_link';
import UnsolvedCaptchaError from '~/captcha/unsolved_captcha_error';

View File

@ -137,7 +137,8 @@ describe('ClusterAgentShow', () => {
await waitForPromises();
});
it('displays agent create information with unknown user', () => {
it('displays agent create information with unknown user', async () => {
await waitForPromises();
expect(findCreatedText()).toMatchInterpolatedText('Created by Unknown user 2 days ago');
});
});
@ -153,19 +154,25 @@ describe('ClusterAgentShow', () => {
await waitForPromises();
});
it('displays token header with no count', () => {
it('displays token header with no count', async () => {
await waitForPromises();
expect(findTokenCount()).toMatchInterpolatedText(`${ClusterAgentShow.i18n.tokens}`);
});
});
describe('when the token list has additional pages', () => {
const pageInfo = {
const pageInfoResponse = {
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'prev',
endCursor: 'next',
};
const pageInfo = {
...pageInfoResponse,
__typename: 'PageInfo',
};
const tokenPagination = {
...defaultClusterAgent,
tokens: {
@ -184,7 +191,7 @@ describe('ClusterAgentShow', () => {
});
it('should pass pageInfo to the pagination component', () => {
expect(findPaginationButtons().props()).toMatchObject(pageInfo);
expect(findPaginationButtons().props()).toMatchObject(pageInfoResponse);
});
});

Some files were not shown because too many files have changed in this diff Show More