Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
12de063de4
commit
f81141c25d
|
|
@ -1 +1 @@
|
|||
5fdd1ba64d79df3a46c74f29d17faf7927650887
|
||||
c8a29dc9fd507cab8835b2e1152b94a6ac96de35
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
const extractAttachmentLinkUrl = (html) => {
|
||||
const parser = new DOMParser();
|
||||
const { body } = parser.parseFromString(html, 'text/html');
|
||||
const link = body.querySelector('a');
|
||||
const src = link.getAttribute('href');
|
||||
const { canonicalSrc } = link.dataset;
|
||||
|
||||
return { src, canonicalSrc };
|
||||
};
|
||||
|
||||
/**
|
||||
* Uploads a file with a post request to the URL indicated
|
||||
* in the uploadsPath parameter. The expected response of the
|
||||
* uploads service is a JSON object that contains, at least, a
|
||||
* link property. The link property should contain markdown link
|
||||
* definition (i.e. [GitLab](https://gitlab.com)).
|
||||
*
|
||||
* This Markdown will be rendered to extract its canonical and full
|
||||
* URLs using GitLab Flavored Markdown renderer in the backend.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {String} params.uploadsPath An absolute URL that points to a service
|
||||
* that allows sending a file for uploading via POST request.
|
||||
* @param {String} params.renderMarkdown A function that accepts a markdown string
|
||||
* and returns a rendered version in HTML format.
|
||||
* @param {File} params.file The file to upload
|
||||
*
|
||||
* @returns Returns an object with two properties:
|
||||
*
|
||||
* canonicalSrc: The URL as defined in the Markdown
|
||||
* src: The absolute URL that points to the resource in the server
|
||||
*/
|
||||
export const uploadFile = async ({ uploadsPath, renderMarkdown, file }) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file, file.name);
|
||||
|
||||
const { data } = await axios.post(uploadsPath, formData);
|
||||
const { markdown } = data.link;
|
||||
const rendered = await renderMarkdown(markdown);
|
||||
|
||||
return extractAttachmentLinkUrl(rendered);
|
||||
};
|
||||
|
|
@ -86,11 +86,11 @@ export default class GroupFilterableList extends FilterableList {
|
|||
// Get option query param, also preserve currently applied query param
|
||||
const sortParam = getParameterByName(
|
||||
'sort',
|
||||
isOptionFilterBySort ? e.currentTarget.href : window.location.href,
|
||||
isOptionFilterBySort ? e.currentTarget.search : window.location.search,
|
||||
);
|
||||
const archivedParam = getParameterByName(
|
||||
'archived',
|
||||
isOptionFilterByArchivedProjects ? e.currentTarget.href : window.location.href,
|
||||
isOptionFilterByArchivedProjects ? e.currentTarget.search : window.location.search,
|
||||
);
|
||||
|
||||
if (sortParam) {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export default {
|
|||
default: null,
|
||||
},
|
||||
issuableType: {
|
||||
default: '',
|
||||
default: 'issue',
|
||||
},
|
||||
emailsHelpPagePath: {
|
||||
default: '',
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export default {
|
|||
{{ timeEstimate }}
|
||||
</span>
|
||||
<weight-count
|
||||
class="gl-display-none gl-sm-display-inline-block gl-mr-3"
|
||||
class="issuable-weight gl-display-none gl-sm-display-inline-block gl-mr-3"
|
||||
:weight="issue.weight"
|
||||
/>
|
||||
<issue-health-status
|
||||
|
|
|
|||
|
|
@ -664,7 +664,7 @@ export default {
|
|||
v-gl-tooltip
|
||||
class="gl-display-none gl-sm-display-block"
|
||||
:title="$options.i18n.relatedMergeRequests"
|
||||
data-testid="issuable-mr"
|
||||
data-testid="merge-requests"
|
||||
>
|
||||
<gl-icon name="merge-request" />
|
||||
{{ issuable.mergeRequestsCount }}
|
||||
|
|
@ -672,7 +672,7 @@ export default {
|
|||
<li
|
||||
v-if="issuable.upvotes"
|
||||
v-gl-tooltip
|
||||
class="gl-display-none gl-sm-display-block"
|
||||
class="issuable-upvotes gl-display-none gl-sm-display-block"
|
||||
:title="$options.i18n.upvotes"
|
||||
data-testid="issuable-upvotes"
|
||||
>
|
||||
|
|
@ -682,7 +682,7 @@ export default {
|
|||
<li
|
||||
v-if="issuable.downvotes"
|
||||
v-gl-tooltip
|
||||
class="gl-display-none gl-sm-display-block"
|
||||
class="issuable-downvotes gl-display-none gl-sm-display-block"
|
||||
:title="$options.i18n.downvotes"
|
||||
data-testid="issuable-downvotes"
|
||||
>
|
||||
|
|
@ -690,7 +690,7 @@ export default {
|
|||
{{ issuable.downvotes }}
|
||||
</li>
|
||||
<blocking-issues-count
|
||||
class="gl-display-none gl-sm-display-block"
|
||||
class="blocking-issues gl-display-none gl-sm-display-block"
|
||||
:blocking-issues-count="issuable.blockedByCount"
|
||||
:is-list-item="true"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ export const i18n = {
|
|||
relatedMergeRequests: __('Related merge requests'),
|
||||
reorderError: __('An error occurred while reordering issues.'),
|
||||
rssLabel: __('Subscribe to RSS feed'),
|
||||
searchPlaceholder: __('Search or filter results…'),
|
||||
searchPlaceholder: __('Search or filter results...'),
|
||||
upvotes: __('Upvotes'),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { IssuableType } from '~/issue_show/constants';
|
||||
import IssuesListApp from '~/issues_list/components/issues_list_app.vue';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
|
||||
|
|
@ -150,7 +149,6 @@ export function mountIssuesListApp() {
|
|||
// For IssuableByEmail component
|
||||
emailsHelpPagePath,
|
||||
initialEmail,
|
||||
issuableType: IssuableType.Issue,
|
||||
markdownHelpPath,
|
||||
quickActionsHelpPath,
|
||||
resetPath,
|
||||
|
|
|
|||
|
|
@ -35,11 +35,6 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
variablesSettingsUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
action: {
|
||||
type: Object,
|
||||
required: false,
|
||||
|
|
@ -75,11 +70,7 @@ export default {
|
|||
|
||||
<p v-if="content" data-testid="job-empty-state-content">{{ content }}</p>
|
||||
</div>
|
||||
<manual-variables-form
|
||||
v-if="shouldRenderManualVariables"
|
||||
:action="action"
|
||||
:variables-settings-url="variablesSettingsUrl"
|
||||
/>
|
||||
<manual-variables-form v-if="shouldRenderManualVariables" :action="action" />
|
||||
<div class="text-content">
|
||||
<div v-if="action && !shouldRenderManualVariables" class="text-center">
|
||||
<gl-link
|
||||
|
|
|
|||
|
|
@ -50,11 +50,6 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
variablesSettingsUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
deploymentHelpUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
|
@ -315,7 +310,6 @@ export default {
|
|||
:action="emptyStateAction"
|
||||
:playable="job.playable"
|
||||
:scheduled="job.scheduled"
|
||||
:variables-settings-url="variablesSettingsUrl"
|
||||
/>
|
||||
<!-- EO empty state -->
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
<script>
|
||||
/* eslint-disable vue/no-v-html */
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { GlButton, GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { mapActions } from 'vuex';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'ManualVariablesForm',
|
||||
components: {
|
||||
GlButton,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
},
|
||||
props: {
|
||||
action: {
|
||||
|
|
@ -24,11 +26,6 @@ export default {
|
|||
);
|
||||
},
|
||||
},
|
||||
variablesSettingsUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
inputTypes: {
|
||||
key: 'key',
|
||||
|
|
@ -37,6 +34,9 @@ export default {
|
|||
i18n: {
|
||||
keyPlaceholder: s__('CiVariables|Input variable key'),
|
||||
valuePlaceholder: s__('CiVariables|Input variable value'),
|
||||
formHelpText: s__(
|
||||
'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default',
|
||||
),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -47,17 +47,8 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
helpText() {
|
||||
return sprintf(
|
||||
s__(
|
||||
'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default',
|
||||
),
|
||||
{
|
||||
linkStart: `<a href="${this.variablesSettingsUrl}">`,
|
||||
linkEnd: '</a>',
|
||||
},
|
||||
false,
|
||||
);
|
||||
variableSettings() {
|
||||
return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' });
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -188,8 +179,14 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gl-mt-3 justify-content-center">
|
||||
<p class="text-muted" data-testid="form-help-text" v-html="helpText"></p>
|
||||
<div class="gl-text-center gl-mt-3">
|
||||
<gl-sprintf :message="$options.i18n.formHelpText">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="variableSettings" target="_blank">
|
||||
{{ content }}
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
<div class="d-flex justify-content-center">
|
||||
<gl-button
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ export default () => {
|
|||
deploymentHelpUrl,
|
||||
codeQualityHelpUrl,
|
||||
runnerSettingsUrl,
|
||||
variablesSettingsUrl,
|
||||
subscriptionsMoreMinutesUrl,
|
||||
endpoint,
|
||||
pagePath,
|
||||
|
|
@ -41,7 +40,6 @@ export default () => {
|
|||
deploymentHelpUrl,
|
||||
codeQualityHelpUrl,
|
||||
runnerSettingsUrl,
|
||||
variablesSettingsUrl,
|
||||
subscriptionsMoreMinutesUrl,
|
||||
endpoint,
|
||||
pagePath,
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ import { InMemoryCache } from 'apollo-cache-inmemory';
|
|||
import { ApolloClient } from 'apollo-client';
|
||||
import { ApolloLink } from 'apollo-link';
|
||||
import { BatchHttpLink } from 'apollo-link-batch-http';
|
||||
import { createHttpLink } from 'apollo-link-http';
|
||||
import { HttpLink } from 'apollo-link-http';
|
||||
import { createUploadLink } from 'apollo-upload-client';
|
||||
import ActionCableLink from '~/actioncable_link';
|
||||
import { apolloCaptchaLink } from '~/captcha/apollo_captcha_link';
|
||||
import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
|
||||
import csrf from '~/lib/utils/csrf';
|
||||
import { objectToQuery, queryToObject } from '~/lib/utils/url_utility';
|
||||
import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
|
||||
|
||||
export const fetchPolicies = {
|
||||
|
|
@ -18,6 +19,31 @@ export const fetchPolicies = {
|
|||
CACHE_ONLY: 'cache-only',
|
||||
};
|
||||
|
||||
export const stripWhitespaceFromQuery = (url, path) => {
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
const [_, params] = url.split(path);
|
||||
|
||||
if (!params) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const decoded = decodeURIComponent(params);
|
||||
const paramsObj = queryToObject(decoded);
|
||||
|
||||
if (!paramsObj.query) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const stripped = paramsObj.query
|
||||
.split(/\s+|\n/)
|
||||
.join(' ')
|
||||
.trim();
|
||||
paramsObj.query = stripped;
|
||||
|
||||
const reassembled = objectToQuery(paramsObj);
|
||||
return `${path}?${reassembled}`;
|
||||
};
|
||||
|
||||
export default (resolvers = {}, config = {}) => {
|
||||
const {
|
||||
assumeImmutableResults,
|
||||
|
|
@ -58,10 +84,31 @@ export default (resolvers = {}, config = {}) => {
|
|||
});
|
||||
});
|
||||
|
||||
/*
|
||||
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
|
||||
request to fail on certain self-hosted stacks. When we can move
|
||||
to subscriptions entirely or can land an upstream PR, this can be removed.
|
||||
|
||||
Related links
|
||||
Bug report: https://gitlab.com/gitlab-org/gitlab/-/issues/329895
|
||||
Moving to subscriptions: https://gitlab.com/gitlab-org/gitlab/-/issues/332485
|
||||
Apollo Client issue: https://github.com/apollographql/apollo-feature-requests/issues/182
|
||||
*/
|
||||
|
||||
const fetchIntervention = (url, options) => {
|
||||
return fetch(stripWhitespaceFromQuery(url, path), options);
|
||||
};
|
||||
|
||||
const requestLink = ApolloLink.split(
|
||||
() => useGet,
|
||||
new HttpLink({ ...httpOptions, fetch: fetchIntervention }),
|
||||
new BatchHttpLink(httpOptions),
|
||||
);
|
||||
|
||||
const uploadsLink = ApolloLink.split(
|
||||
(operation) => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
|
||||
createUploadLink(httpOptions),
|
||||
useGet ? createHttpLink(httpOptions) : new BatchHttpLink(httpOptions),
|
||||
);
|
||||
|
||||
const performanceBarLink = new ApolloLink((operation, forward) => {
|
||||
|
|
@ -99,6 +146,7 @@ export default (resolvers = {}, config = {}) => {
|
|||
new StartupJSLink(),
|
||||
apolloCaptchaLink,
|
||||
uploadsLink,
|
||||
requestLink,
|
||||
]),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -107,25 +107,6 @@ export function getParameterValues(sParam, url = window.location) {
|
|||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function accepts the `name` of the param to parse in the url
|
||||
* if the name does not exist this function will return `null`
|
||||
* otherwise it will return the value of the param key provided
|
||||
*
|
||||
* @param {String} name
|
||||
* @param {String?} urlToParse
|
||||
* @returns value of the parameter as string
|
||||
*/
|
||||
export const getParameterByName = (name, urlToParse) => {
|
||||
const url = urlToParse || window.location.href;
|
||||
const parsedName = name.replace(/[[\]]/g, '\\$&');
|
||||
const regex = new RegExp(`[?&]${parsedName}(=([^&#]*)|&|#|$)`);
|
||||
const results = regex.exec(url);
|
||||
if (!results) return null;
|
||||
if (!results[2]) return '';
|
||||
return decodeUrlParameter(results[2]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Merges a URL to a set of params replacing value for
|
||||
* those already present.
|
||||
|
|
@ -513,6 +494,19 @@ export function queryToObject(query, { gatherArrays = false, legacySpacesDecode
|
|||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function accepts the `name` of the param to parse in the url
|
||||
* if the name does not exist this function will return `null`
|
||||
* otherwise it will return the value of the param key provided
|
||||
*
|
||||
* @param {String} name
|
||||
* @param {String?} urlToParse
|
||||
* @returns value of the parameter as string
|
||||
*/
|
||||
export const getParameterByName = (name, query = window.location.search) => {
|
||||
return queryToObject(query)[name] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert search query object back into a search query
|
||||
*
|
||||
|
|
|
|||
|
|
@ -158,6 +158,12 @@ export default {
|
|||
const updatedPath = setUrlParams({ branch_name: newBranch });
|
||||
historyPushState(updatedPath);
|
||||
|
||||
this.$emit('updateCommitSha', { newBranch });
|
||||
|
||||
// refetching the content will cause a lot of components to re-render,
|
||||
// including the text editor which uses the commit sha to register the CI schema
|
||||
// so we need to make sure the commit sha is updated first
|
||||
await this.$nextTick();
|
||||
this.$emit('refetchContent');
|
||||
},
|
||||
async setSearchTerm(newSearchTerm) {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
commitSha: '',
|
||||
hasError: false,
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
mutation updateCommitSha($commitSha: String) {
|
||||
updateCommitSha(commitSha: $commitSha) @client
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
query getLatestCommitSha($projectPath: ID!, $ref: String) {
|
||||
project(fullPath: $projectPath) {
|
||||
pipelines(ref: $ref) {
|
||||
nodes {
|
||||
id
|
||||
sha
|
||||
path
|
||||
commitPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import produce from 'immer';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import getCommitShaQuery from './queries/client/commit_sha.graphql';
|
||||
import getCurrentBranchQuery from './queries/client/current_branch.graphql';
|
||||
import getLastCommitBranchQuery from './queries/client/last_commit_branch.query.graphql';
|
||||
|
||||
|
|
@ -31,7 +32,15 @@ export const resolvers = {
|
|||
__typename: 'CiLintContent',
|
||||
}));
|
||||
},
|
||||
updateCurrentBranch: (_, { currentBranch = undefined }, { cache }) => {
|
||||
updateCommitSha: (_, { commitSha }, { cache }) => {
|
||||
cache.writeQuery({
|
||||
query: getCommitShaQuery,
|
||||
data: produce(cache.readQuery({ query: getCommitShaQuery }), (draftData) => {
|
||||
draftData.commitSha = commitSha;
|
||||
}),
|
||||
});
|
||||
},
|
||||
updateCurrentBranch: (_, { currentBranch }, { cache }) => {
|
||||
cache.writeQuery({
|
||||
query: getCurrentBranchQuery,
|
||||
data: produce(cache.readQuery({ query: getCurrentBranchQuery }), (draftData) => {
|
||||
|
|
@ -39,7 +48,7 @@ export const resolvers = {
|
|||
}),
|
||||
});
|
||||
},
|
||||
updateLastCommitBranch: (_, { lastCommitBranch = undefined }, { cache }) => {
|
||||
updateLastCommitBranch: (_, { lastCommitBranch }, { cache }) => {
|
||||
cache.writeQuery({
|
||||
query: getLastCommitBranchQuery,
|
||||
data: produce(cache.readQuery({ query: getLastCommitBranchQuery }), (draftData) => {
|
||||
|
|
|
|||
|
|
@ -16,12 +16,14 @@ import {
|
|||
LOAD_FAILURE_UNKNOWN,
|
||||
STARTER_TEMPLATE_NAME,
|
||||
} from './constants';
|
||||
import updateCommitShaMutation from './graphql/mutations/update_commit_sha.mutation.graphql';
|
||||
import getBlobContent from './graphql/queries/blob_content.graphql';
|
||||
import getCiConfigData from './graphql/queries/ci_config.graphql';
|
||||
import getAppStatus from './graphql/queries/client/app_status.graphql';
|
||||
import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
|
||||
import getIsNewCiConfigFile from './graphql/queries/client/is_new_ci_config_file.graphql';
|
||||
import getTemplate from './graphql/queries/get_starter_template.query.graphql';
|
||||
import getLatestCommitShaQuery from './graphql/queries/latest_commit_sha.query.graphql';
|
||||
import PipelineEditorHome from './pipeline_editor_home.vue';
|
||||
|
||||
export default {
|
||||
|
|
@ -250,6 +252,38 @@ export default {
|
|||
updateCiConfig(ciFileContent) {
|
||||
this.currentCiFileContent = ciFileContent;
|
||||
},
|
||||
async updateCommitSha({ newBranch }) {
|
||||
let fetchResults;
|
||||
|
||||
try {
|
||||
fetchResults = await this.$apollo.query({
|
||||
query: getLatestCommitShaQuery,
|
||||
variables: {
|
||||
projectPath: this.projectFullPath,
|
||||
ref: newBranch,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
this.showFetchError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (fetchResults.errors?.length > 0) {
|
||||
this.showFetchError();
|
||||
return;
|
||||
}
|
||||
|
||||
const pipelineNodes = fetchResults?.data?.project?.pipelines?.nodes ?? [];
|
||||
if (pipelineNodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commitSha = pipelineNodes[0].sha;
|
||||
this.$apollo.mutate({
|
||||
mutation: updateCommitShaMutation,
|
||||
variables: { commitSha },
|
||||
});
|
||||
},
|
||||
updateOnCommit({ type }) {
|
||||
this.reportSuccess(type);
|
||||
|
||||
|
|
@ -302,6 +336,7 @@ export default {
|
|||
@showError="showErrorAlert"
|
||||
@refetchContent="refetchContent"
|
||||
@updateCiConfig="updateCiConfig"
|
||||
@updateCommitSha="updateCommitSha"
|
||||
/>
|
||||
<confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@ export default {
|
|||
<div class="col-lg-8">
|
||||
<div class="form-group">
|
||||
<gl-button
|
||||
variant="success"
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
name="commit"
|
||||
type="submit"
|
||||
:disabled="!isSubmitEnabled"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import filesQuery from 'shared_queries/repository/files.query.graphql';
|
||||
import createFlash from '~/flash';
|
||||
import { __ } from '../../locale';
|
||||
import { TREE_PAGE_SIZE, TREE_INITIAL_FETCH_COUNT } from '../constants';
|
||||
import { TREE_PAGE_SIZE, TREE_INITIAL_FETCH_COUNT, TREE_PAGE_LIMIT } from '../constants';
|
||||
import getRefMixin from '../mixins/get_ref';
|
||||
import projectPathQuery from '../queries/project_path.query.graphql';
|
||||
import { readmeFile } from '../utils/readme';
|
||||
|
|
@ -36,6 +36,7 @@ export default {
|
|||
return {
|
||||
projectPath: '',
|
||||
nextPageCursor: '',
|
||||
pagesLoaded: 1,
|
||||
entries: {
|
||||
trees: [],
|
||||
submodules: [],
|
||||
|
|
@ -44,16 +45,26 @@ export default {
|
|||
isLoadingFiles: false,
|
||||
isOverLimit: false,
|
||||
clickedShowMore: false,
|
||||
pageSize: TREE_PAGE_SIZE,
|
||||
fetchCounter: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
pageSize() {
|
||||
// we want to exponentially increase the page size to reduce the load on the frontend
|
||||
const exponentialSize = (TREE_PAGE_SIZE / TREE_INITIAL_FETCH_COUNT) * (this.fetchCounter + 1);
|
||||
return exponentialSize < TREE_PAGE_SIZE ? exponentialSize : TREE_PAGE_SIZE;
|
||||
},
|
||||
totalEntries() {
|
||||
return Object.values(this.entries).flat().length;
|
||||
},
|
||||
readme() {
|
||||
return readmeFile(this.entries.blobs);
|
||||
},
|
||||
pageLimitReached() {
|
||||
return this.totalEntries / this.pagesLoaded >= TREE_PAGE_LIMIT;
|
||||
},
|
||||
hasShowMore() {
|
||||
return !this.clickedShowMore && this.fetchCounter === TREE_INITIAL_FETCH_COUNT;
|
||||
return !this.clickedShowMore && this.pageLimitReached;
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -104,7 +115,7 @@ export default {
|
|||
if (pageInfo?.hasNextPage) {
|
||||
this.nextPageCursor = pageInfo.endCursor;
|
||||
this.fetchCounter += 1;
|
||||
if (this.fetchCounter < TREE_INITIAL_FETCH_COUNT || this.clickedShowMore) {
|
||||
if (!this.pageLimitReached || this.clickedShowMore) {
|
||||
this.fetchFiles();
|
||||
this.clickedShowMore = false;
|
||||
}
|
||||
|
|
@ -127,6 +138,7 @@ export default {
|
|||
},
|
||||
handleShowMore() {
|
||||
this.clickedShowMore = true;
|
||||
this.pagesLoaded += 1;
|
||||
this.fetchFiles();
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
const TREE_PAGE_LIMIT = 1000; // the maximum amount of items per page
|
||||
|
||||
export const TREE_PAGE_LIMIT = 1000; // the maximum amount of items per page
|
||||
export const TREE_PAGE_SIZE = 100; // the amount of items to be fetched per (batch) request
|
||||
export const TREE_INITIAL_FETCH_COUNT = TREE_PAGE_LIMIT / TREE_PAGE_SIZE; // the amount of (batch) requests to make
|
||||
|
|
|
|||
|
|
@ -240,3 +240,13 @@ $gl-line-height-42: px-to-rem(42px);
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1490
|
||||
.gl-w-grid-size-28 {
|
||||
width: $grid-size * 28;
|
||||
}
|
||||
|
||||
// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1491
|
||||
.gl-min-w-8 {
|
||||
min-width: $gl-spacing-scale-8;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ module Resolvers
|
|||
type Types::Ci::TemplateType, null: true
|
||||
|
||||
argument :name, GraphQL::STRING_TYPE, required: true,
|
||||
description: 'Name of the CI/CD template to search for.'
|
||||
description: 'Name of the CI/CD template to search for. Template must be formatted as `Name.gitlab-ci.yml`.'
|
||||
|
||||
alias_method :project, :object
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ module Ci
|
|||
"artifact_help_url" => help_page_path('user/gitlab_com/index.html', anchor: 'gitlab-cicd'),
|
||||
"deployment_help_url" => help_page_path('user/project/clusters/index.html', anchor: 'troubleshooting'),
|
||||
"runner_settings_url" => project_runners_path(@build.project, anchor: 'js-runners-settings'),
|
||||
"variables_settings_url" => project_variables_path(@build.project, anchor: 'js-cicd-variables-settings'),
|
||||
"page_path" => project_job_path(@project, @build),
|
||||
"build_status" => @build.status,
|
||||
"build_stage" => @build.stage,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ module Ci
|
|||
end
|
||||
|
||||
def js_pipeline_editor_data(project)
|
||||
commit_sha = project.commit ? project.commit.sha : ''
|
||||
initial_branch = params[:branch_name]
|
||||
latest_commit = project.repository.commit(initial_branch) || project.commit
|
||||
commit_sha = latest_commit ? latest_commit.sha : ''
|
||||
{
|
||||
"ci-config-path": project.ci_config_path_or_default,
|
||||
"ci-examples-help-page-path" => help_page_path('ci/examples/index'),
|
||||
|
|
@ -17,11 +19,11 @@ module Ci
|
|||
"commit-sha" => commit_sha,
|
||||
"default-branch" => project.default_branch_or_main,
|
||||
"empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'),
|
||||
"initial-branch-name": params[:branch_name],
|
||||
"initial-branch-name" => initial_branch,
|
||||
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
|
||||
"needs-help-page-path" => help_page_path('ci/yaml/README', anchor: 'needs'),
|
||||
"new-merge-request-path" => namespace_project_new_merge_request_path,
|
||||
"pipeline_etag" => project.commit ? graphql_etag_pipeline_sha_path(commit_sha) : '',
|
||||
"pipeline_etag" => latest_commit ? graphql_etag_pipeline_sha_path(commit_sha) : '',
|
||||
"pipeline-page-path" => project_pipelines_path(project),
|
||||
"project-path" => project.path,
|
||||
"project-full-path" => project.full_path,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ class AwardEmoji < ApplicationRecord
|
|||
after_save :expire_cache
|
||||
after_destroy :expire_cache
|
||||
|
||||
after_save :update_awardable_upvotes_count
|
||||
after_destroy :update_awardable_upvotes_count
|
||||
|
||||
class << self
|
||||
def votes_for_collection(ids, type)
|
||||
select('name', 'awardable_id', 'COUNT(*) as count')
|
||||
|
|
@ -64,6 +67,14 @@ class AwardEmoji < ApplicationRecord
|
|||
awardable.try(:bump_updated_at)
|
||||
awardable.try(:expire_etag_cache)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_awardable_upvotes_count
|
||||
return unless upvote? && awardable.has_attribute?(:upvotes_count)
|
||||
|
||||
awardable.update_column(:upvotes_count, awardable.upvotes)
|
||||
end
|
||||
end
|
||||
|
||||
AwardEmoji.prepend_mod_with('AwardEmoji')
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ErrorTracking::ErrorEvent < ApplicationRecord
|
||||
belongs_to :error
|
||||
belongs_to :error, counter_cache: :events_count
|
||||
|
||||
validates :payload, json_schema: { filename: 'error_tracking_event_payload' }
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
"description": "Error tracking event payload",
|
||||
"type": "object",
|
||||
"required": [],
|
||||
"modules": {
|
||||
"type": "object"
|
||||
},
|
||||
"properties": {
|
||||
"event_id": {
|
||||
"type": "string"
|
||||
|
|
@ -73,28 +76,7 @@
|
|||
}
|
||||
},
|
||||
"trace": {
|
||||
"type": "object",
|
||||
"required": [],
|
||||
"properties": {
|
||||
"trace_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"span_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"parent_span_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"op": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -118,52 +100,13 @@
|
|||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": [],
|
||||
"properties": {
|
||||
"controller": {
|
||||
"type": "string"
|
||||
},
|
||||
"action": {
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"type": "object",
|
||||
"required": [],
|
||||
"properties": {
|
||||
"controller": {
|
||||
"type": "string"
|
||||
},
|
||||
"action": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"format": {
|
||||
"type": "string"
|
||||
},
|
||||
"method": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"start_timestamp": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"level": {
|
||||
"type": "string"
|
||||
"type": "object"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -199,37 +142,7 @@
|
|||
"type": "string"
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"required": [],
|
||||
"properties": {
|
||||
"Host": {
|
||||
"type": "string"
|
||||
},
|
||||
"User-Agent": {
|
||||
"type": "string"
|
||||
},
|
||||
"Accept": {
|
||||
"type": "string"
|
||||
},
|
||||
"Accept-Language": {
|
||||
"type": "string"
|
||||
},
|
||||
"Accept-Encoding": {
|
||||
"type": "string"
|
||||
},
|
||||
"Referer": {
|
||||
"type": "string"
|
||||
},
|
||||
"Turbolinks-Referrer": {
|
||||
"type": "string"
|
||||
},
|
||||
"Connection": {
|
||||
"type": "string"
|
||||
},
|
||||
"X-Request-Id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
"type": "object"
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
|
|
@ -290,25 +203,19 @@
|
|||
"type": "number"
|
||||
},
|
||||
"in_app": {
|
||||
"type": "string"
|
||||
"type": "boolean"
|
||||
},
|
||||
"filename": {
|
||||
"type": "string"
|
||||
},
|
||||
"pre_context": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
"type": "array"
|
||||
},
|
||||
"context_line": {
|
||||
"type": "string"
|
||||
},
|
||||
"post_context": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334264
|
|||
milestone: '14.1'
|
||||
type: development
|
||||
group: group::source code
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: api_caching_repository_compare
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64418
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334264
|
||||
milestone: '14.1'
|
||||
type: development
|
||||
group: group::source code
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
TEMPLATE_MESSAGE = <<~MSG
|
||||
This merge request requires coordination with gitaly deployments.
|
||||
Before merging this merge request we should verify that gitaly
|
||||
running in production already implements the new gRPC interface
|
||||
included here.
|
||||
|
||||
Failing to do so will introduce a [non backward compatible
|
||||
change](https://docs.gitlab.com/ee/development/multi_version_compatibility.html)
|
||||
during canary depoyment that can cause an incident.
|
||||
|
||||
1. Identify the gitaly MR introducing the new interface
|
||||
1. Verify that the environment widget contains a `gprd` deployment
|
||||
MSG
|
||||
|
||||
changed_lines = helper.changed_lines('Gemfile.lock')
|
||||
if changed_lines.any? { |line| line =~ /^\+\s+gitaly \(/ }
|
||||
warn 'Changing gitaly gem can cause a multi-version incompatibility incident'
|
||||
|
||||
markdown(TEMPLATE_MESSAGE)
|
||||
end
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddUpvotesCountToIssues < ActiveRecord::Migration[6.1]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
add_column :issues, :upvotes_count, :integer, default: 0, null: false
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :issues, :upvotes_count
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddErrorTrackingCounterCache < ActiveRecord::Migration[6.1]
|
||||
def up
|
||||
add_column :error_tracking_errors, :events_count, :bigint, null: false, default: 0
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :error_tracking_errors, :events_count
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BackfillIssuesUpvotesCount < ActiveRecord::Migration[6.1]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
MIGRATION = 'BackfillUpvotesCountOnIssues'
|
||||
DELAY_INTERVAL = 2.minutes
|
||||
BATCH_SIZE = 5_000
|
||||
|
||||
def up
|
||||
scope = Issue.joins("INNER JOIN award_emoji e ON e.awardable_id = issues.id AND e.awardable_type = 'Issue' AND e.name = 'thumbsup'")
|
||||
|
||||
queue_background_migration_jobs_by_range_at_intervals(
|
||||
scope,
|
||||
MIGRATION,
|
||||
DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
c2efdad12c3d0ec5371259baa91466137b827f513250e901842ab28e56c3de0a
|
||||
|
|
@ -0,0 +1 @@
|
|||
fdd7509fc88e563b65b487706cae1a64066a7ba7d4bd13d0414b8431c3ddfb68
|
||||
|
|
@ -0,0 +1 @@
|
|||
ed0c0dc015e7c3457248303b8b478c8d259d6a800a2bfed8b05b1f976b6794a7
|
||||
|
|
@ -12790,6 +12790,7 @@ CREATE TABLE error_tracking_errors (
|
|||
platform text,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
events_count bigint DEFAULT 0 NOT NULL,
|
||||
CONSTRAINT check_18a758e537 CHECK ((char_length(name) <= 255)),
|
||||
CONSTRAINT check_b5cb4d3888 CHECK ((char_length(actor) <= 255)),
|
||||
CONSTRAINT check_c739788b12 CHECK ((char_length(description) <= 1024)),
|
||||
|
|
@ -14267,6 +14268,7 @@ CREATE TABLE issues (
|
|||
sprint_id bigint,
|
||||
issue_type smallint DEFAULT 0 NOT NULL,
|
||||
blocking_issues_count integer DEFAULT 0 NOT NULL,
|
||||
upvotes_count integer DEFAULT 0 NOT NULL,
|
||||
CONSTRAINT check_fba63f706d CHECK ((lock_version IS NOT NULL))
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -11596,7 +11596,7 @@ Returns [`CiTemplate`](#citemplate).
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="projectcitemplatename"></a>`name` | [`String!`](#string) | Name of the CI/CD template to search for. |
|
||||
| <a id="projectcitemplatename"></a>`name` | [`String!`](#string) | Name of the CI/CD template to search for. Template must be formatted as `Name.gitlab-ci.yml`. |
|
||||
|
||||
##### `Project.clusterAgent`
|
||||
|
||||
|
|
|
|||
|
|
@ -49,8 +49,10 @@ The following table lists project permissions available for each role:
|
|||
| View allowed and denied licenses **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View License Compliance reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Security reports **(ULTIMATE)** | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Dependency list **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View License list **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View Dependency list **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
|
||||
| View License list **(ULTIMATE)** | | ✓ | ✓ | ✓ | ✓ |
|
||||
| View [Threats list](application_security/threat_monitoring/#threat-monitoring) **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
|
||||
| Create and run [on-demand DAST scans](application_security/dast/#on-demand-scans) | | | ✓ | ✓ | ✓ |
|
||||
| View licenses in Dependency list **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
| View [Design Management](project/issues/design_management.md) pages | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| View project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ module API
|
|||
get ':id/repository/compare' do
|
||||
ff_enabled = Feature.enabled?(:api_caching_rate_limit_repository_compare, user_project, default_enabled: :yaml)
|
||||
|
||||
cache_action_if(ff_enabled, [user_project, :repository_compare, current_user, declared_params], expires_in: 30.seconds) do
|
||||
cache_action_if(ff_enabled, [user_project, :repository_compare, current_user, declared_params], expires_in: 1.minute) do
|
||||
if params[:from_project_id].present?
|
||||
target_project = MergeRequestTargetProjectFinder
|
||||
.new(current_user: current_user, source_project: user_project, project_feature: :repository)
|
||||
|
|
@ -138,11 +138,7 @@ module API
|
|||
compare = CompareService.new(user_project, params[:to]).execute(target_project, params[:from], straight: params[:straight])
|
||||
|
||||
if compare
|
||||
if Feature.enabled?(:api_caching_repository_compare, user_project, default_enabled: :yaml)
|
||||
present_cached compare, with: Entities::Compare, expires_in: 1.day, cache_context: nil
|
||||
else
|
||||
present compare, with: Entities::Compare
|
||||
end
|
||||
present compare, with: Entities::Compare
|
||||
else
|
||||
not_found!("Ref")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
# Class that will populate the upvotes_count field
|
||||
# for each issue
|
||||
class BackfillUpvotesCountOnIssues
|
||||
BATCH_SIZE = 1_000
|
||||
|
||||
def perform(start_id, stop_id)
|
||||
(start_id..stop_id).step(BATCH_SIZE).each do |offset|
|
||||
update_issue_upvotes_count(offset, offset + BATCH_SIZE)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def execute(sql)
|
||||
@connection ||= ::ActiveRecord::Base.connection
|
||||
@connection.execute(sql)
|
||||
end
|
||||
|
||||
def update_issue_upvotes_count(batch_start, batch_stop)
|
||||
execute(<<~SQL)
|
||||
UPDATE issues
|
||||
SET upvotes_count = sub_q.count_all
|
||||
FROM (
|
||||
SELECT COUNT(*) AS count_all, e.awardable_id AS issue_id
|
||||
FROM award_emoji AS e
|
||||
WHERE e.name = 'thumbsup' AND
|
||||
e.awardable_type = 'Issue' AND
|
||||
e.awardable_id BETWEEN #{batch_start} AND #{batch_stop}
|
||||
GROUP BY issue_id
|
||||
) AS sub_q
|
||||
WHERE sub_q.issue_id = issues.id;
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -229,6 +229,7 @@ excluded_attributes:
|
|||
- :promoted_to_epic_id
|
||||
- :blocking_issues_count
|
||||
- :service_desk_reply_to
|
||||
- :upvotes_count
|
||||
merge_request:
|
||||
- :milestone_id
|
||||
- :sprint_id
|
||||
|
|
|
|||
|
|
@ -16865,6 +16865,9 @@ msgstr ""
|
|||
msgid "InProductMarketing|Create your first project!"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Deliver Better Products Faster"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Did you know teams that use GitLab are far more efficient?"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -16904,6 +16907,9 @@ msgstr ""
|
|||
msgid "InProductMarketing|Follow our steps"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Free 30-day trial"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Get going with CI/CD quickly using our %{quick_start_link}. Start with an available runner and then create a CI .yml file – it's really that easy."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -16982,6 +16988,9 @@ msgstr ""
|
|||
msgid "InProductMarketing|Improve code quality and streamline reviews"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Increase Operational Efficiencies"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Invite your colleagues and start shipping code faster."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -17024,12 +17033,18 @@ msgstr ""
|
|||
msgid "InProductMarketing|Neutral"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|No credit card required."
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Our tool brings all the things together"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Rapid development, simplified"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Reduce Security & Compliance Risk"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Security that's integrated into your development lifecycle"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -17120,6 +17135,9 @@ msgstr ""
|
|||
msgid "InProductMarketing|Use GitLab CI/CD"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Used by more than 100,000 organizations from around the globe:"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Very difficult"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -29087,6 +29105,9 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|Security policy project"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityPolicies|All policies"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityPolicies|Description"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@
|
|||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/svgs": "1.202.0",
|
||||
"@gitlab/tributejs": "1.0.0",
|
||||
"@gitlab/ui": "31.0.1",
|
||||
"@gitlab/ui": "31.2.0",
|
||||
"@gitlab/visual-review-tools": "1.6.1",
|
||||
"@rails/actioncable": "6.1.3-2",
|
||||
"@rails/ujs": "6.1.3-2",
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ RSpec.describe 'User views an empty project' do
|
|||
let_it_be(:user) { create(:user) }
|
||||
|
||||
shared_examples 'allowing push to default branch' do
|
||||
it 'shows push-to-master instructions' do
|
||||
let(:default_branch) { project.default_branch_or_main }
|
||||
|
||||
it 'shows push-to-default-branch instructions' do
|
||||
visit project_path(project)
|
||||
|
||||
expect(page).to have_content('git push -u origin master')
|
||||
expect(page).to have_content("git push -u origin #{default_branch}")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -47,7 +49,7 @@ RSpec.describe 'User views an empty project' do
|
|||
it 'does not show push-to-master instructions' do
|
||||
visit project_path(project)
|
||||
|
||||
expect(page).not_to have_content('git push -u origin master')
|
||||
expect(page).not_to have_content('git push -u origin')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -61,7 +63,7 @@ RSpec.describe 'User views an empty project' do
|
|||
it 'does not show push-to-master instructions nor invite members link', :aggregate_failures, :js do
|
||||
visit project_path(project)
|
||||
|
||||
expect(page).not_to have_content('git push -u origin master')
|
||||
expect(page).not_to have_content('git push -u origin')
|
||||
expect(page).not_to have_button(text: 'Invite members')
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
import axios from 'axios';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { uploadFile } from '~/content_editor/services/upload_file';
|
||||
import httpStatus from '~/lib/utils/http_status';
|
||||
|
||||
describe('content_editor/services/upload_file', () => {
|
||||
const uploadsPath = '/uploads';
|
||||
const file = new File(['content'], 'file.txt');
|
||||
// TODO: Replace with automated fixture
|
||||
const renderedAttachmentLinkFixture =
|
||||
'<a href="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png"><img data-src="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png"></a></p>';
|
||||
const successResponse = {
|
||||
link: {
|
||||
markdown: '[GitLab](https://gitlab.com)',
|
||||
},
|
||||
};
|
||||
const parseHTML = (html) => new DOMParser().parseFromString(html, 'text/html');
|
||||
let mock;
|
||||
let renderMarkdown;
|
||||
let renderedMarkdown;
|
||||
|
||||
beforeEach(() => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
renderedMarkdown = parseHTML(renderedAttachmentLinkFixture);
|
||||
|
||||
mock = new MockAdapter(axios);
|
||||
mock.onPost(uploadsPath, formData).reply(httpStatus.OK, successResponse);
|
||||
renderMarkdown = jest.fn().mockResolvedValue(renderedAttachmentLinkFixture);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('returns src and canonicalSrc of uploaded file', async () => {
|
||||
const response = await uploadFile({ uploadsPath, renderMarkdown, file });
|
||||
|
||||
expect(renderMarkdown).toHaveBeenCalledWith(successResponse.link.markdown);
|
||||
expect(response).toEqual({
|
||||
src: renderedMarkdown.querySelector('a').getAttribute('href'),
|
||||
canonicalSrc: renderedMarkdown.querySelector('a').dataset.canonicalSrc,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -54,7 +54,7 @@ describe('Compare diff version dropdowns', () => {
|
|||
|
||||
Object.defineProperty(window, 'location', {
|
||||
writable: true,
|
||||
value: { href: `https://example.gitlab.com${diffHeadParam}` },
|
||||
value: { search: diffHeadParam },
|
||||
});
|
||||
|
||||
expectedFirstVersion = {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ describe('Empty State', () => {
|
|||
illustrationSizeClass: 'svg-430',
|
||||
title: 'This job has not started yet',
|
||||
playable: false,
|
||||
variablesSettingsUrl: '',
|
||||
};
|
||||
|
||||
const createWrapper = (props) => {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ describe('Job App', () => {
|
|||
deploymentHelpUrl: 'help/deployment',
|
||||
codeQualityHelpPath: '/help/code_quality',
|
||||
runnerSettingsUrl: 'settings/ci-cd/runners',
|
||||
variablesSettingsUrl: 'settings/ci-cd/variables',
|
||||
terminalPath: 'jobs/123/terminal',
|
||||
projectPath: 'user-name/project-name',
|
||||
subscriptionsMoreMinutesUrl: 'https://customers.gitlab.com/buy_pipeline_minutes',
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
|
|
@ -18,7 +19,6 @@ describe('Manual Variables Form', () => {
|
|||
method: 'post',
|
||||
button_title: 'Trigger this manual action',
|
||||
},
|
||||
variablesSettingsUrl: '/settings',
|
||||
};
|
||||
|
||||
const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
|
||||
|
|
@ -33,15 +33,19 @@ describe('Manual Variables Form', () => {
|
|||
propsData: { ...requiredProps, ...props },
|
||||
localVue,
|
||||
store,
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const findInputKey = () => wrapper.findComponent({ ref: 'inputKey' });
|
||||
const findInputValue = () => wrapper.findComponent({ ref: 'inputSecretValue' });
|
||||
const findHelpText = () => wrapper.findComponent(GlSprintf);
|
||||
const findHelpLink = () => wrapper.findComponent(GlLink);
|
||||
|
||||
const findTriggerBtn = () => wrapper.findByTestId('trigger-manual-job-btn');
|
||||
const findHelpText = () => wrapper.findByTestId('form-help-text');
|
||||
const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn');
|
||||
const findCiVariableKey = () => wrapper.findByTestId('ci-variable-key');
|
||||
const findCiVariableValue = () => wrapper.findByTestId('ci-variable-value');
|
||||
|
|
@ -62,11 +66,10 @@ describe('Manual Variables Form', () => {
|
|||
});
|
||||
|
||||
it('renders help text with provided link', () => {
|
||||
expect(findHelpText().text()).toBe(
|
||||
'Specify variable values to be used in this run. The values specified in CI/CD settings will be used as default',
|
||||
expect(findHelpText().exists()).toBe(true);
|
||||
expect(findHelpLink().attributes('href')).toBe(
|
||||
'/help/ci/variables/index#add-a-cicd-variable-to-a-project',
|
||||
);
|
||||
|
||||
expect(wrapper.find('a').attributes('href')).toBe(requiredProps.variablesSettingsUrl);
|
||||
});
|
||||
|
||||
describe('when adding a new variable', () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
|
||||
import { stripWhitespaceFromQuery } from '~/lib/graphql';
|
||||
import { queryToObject } from '~/lib/utils/url_utility';
|
||||
|
||||
describe('stripWhitespaceFromQuery', () => {
|
||||
const operationName = 'getPipelineDetails';
|
||||
const variables = `{
|
||||
projectPath: 'root/abcd-dag',
|
||||
iid: '44'
|
||||
}`;
|
||||
|
||||
const testQuery = getPipelineDetails.loc.source.body;
|
||||
const defaultPath = '/api/graphql';
|
||||
const encodedVariables = encodeURIComponent(variables);
|
||||
|
||||
it('shortens the query argument by replacing multiple spaces and newlines with a single space', () => {
|
||||
const testString = `${defaultPath}?query=${encodeURIComponent(testQuery)}`;
|
||||
expect(testString.length > stripWhitespaceFromQuery(testString, defaultPath).length).toBe(true);
|
||||
});
|
||||
|
||||
it('does not contract a single space', () => {
|
||||
const simpleSingleString = `${defaultPath}?query=${encodeURIComponent('fragment Nonsense')}`;
|
||||
expect(stripWhitespaceFromQuery(simpleSingleString, defaultPath)).toEqual(simpleSingleString);
|
||||
});
|
||||
|
||||
it('works with a non-default path', () => {
|
||||
const newPath = 'another/graphql/path';
|
||||
const newPathSingleString = `${newPath}?query=${encodeURIComponent('fragment Nonsense')}`;
|
||||
expect(stripWhitespaceFromQuery(newPathSingleString, newPath)).toEqual(newPathSingleString);
|
||||
});
|
||||
|
||||
it('does not alter other arguments', () => {
|
||||
const bareParams = `?query=${encodeURIComponent(
|
||||
testQuery,
|
||||
)}&operationName=${operationName}&variables=${encodedVariables}`;
|
||||
const testLongString = `${defaultPath}${bareParams}`;
|
||||
|
||||
const processed = stripWhitespaceFromQuery(testLongString, defaultPath);
|
||||
const decoded = decodeURIComponent(processed);
|
||||
const params = queryToObject(decoded);
|
||||
|
||||
expect(params.operationName).toBe(operationName);
|
||||
expect(params.variables).toBe(variables);
|
||||
});
|
||||
|
||||
it('works when there are no query params', () => {
|
||||
expect(stripWhitespaceFromQuery(defaultPath, defaultPath)).toEqual(defaultPath);
|
||||
});
|
||||
|
||||
it('works when the params do not include a query', () => {
|
||||
const paramsWithoutQuery = `${defaultPath}&variables=${encodedVariables}`;
|
||||
expect(stripWhitespaceFromQuery(paramsWithoutQuery, defaultPath)).toEqual(paramsWithoutQuery);
|
||||
});
|
||||
});
|
||||
|
|
@ -101,48 +101,6 @@ describe('URL utility', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getParameterByName', () => {
|
||||
const { getParameterByName } = urlUtils;
|
||||
|
||||
it('should return valid parameter', () => {
|
||||
setWindowLocation({ href: 'https://gitlab.com?scope=all&p=2' });
|
||||
|
||||
expect(getParameterByName('p')).toEqual('2');
|
||||
expect(getParameterByName('scope')).toBe('all');
|
||||
});
|
||||
|
||||
it('should return invalid parameter', () => {
|
||||
setWindowLocation({ href: 'https://gitlab.com?scope=all&p=2' });
|
||||
|
||||
expect(getParameterByName('fakeParameter')).toBe(null);
|
||||
});
|
||||
|
||||
it('should return a parameter with spaces', () => {
|
||||
setWindowLocation({ href: 'https://gitlab.com?search=my terms' });
|
||||
|
||||
expect(getParameterByName('search')).toBe('my terms');
|
||||
});
|
||||
|
||||
it('should return a parameter with encoded spaces', () => {
|
||||
setWindowLocation({ href: 'https://gitlab.com?search=my%20terms' });
|
||||
|
||||
expect(getParameterByName('search')).toBe('my terms');
|
||||
});
|
||||
|
||||
it('should return a parameter with plus signs as spaces', () => {
|
||||
setWindowLocation({ href: 'https://gitlab.com?search=my+terms' });
|
||||
|
||||
expect(getParameterByName('search')).toBe('my terms');
|
||||
});
|
||||
|
||||
it('should return valid parameters if URL is provided', () => {
|
||||
expect(getParameterByName('foo', 'http://cocteau.twins?foo=bar')).toBe('bar');
|
||||
expect(getParameterByName('manan', 'http://cocteau.twins?foo=bar&manan=canchu')).toBe(
|
||||
'canchu',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeUrlParams', () => {
|
||||
const { mergeUrlParams } = urlUtils;
|
||||
|
||||
|
|
@ -762,6 +720,49 @@ describe('URL utility', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getParameterByName', () => {
|
||||
const { getParameterByName } = urlUtils;
|
||||
|
||||
it('should return valid parameter', () => {
|
||||
setWindowLocation({ search: '?scope=all&p=2' });
|
||||
|
||||
expect(getParameterByName('p')).toEqual('2');
|
||||
expect(getParameterByName('scope')).toBe('all');
|
||||
});
|
||||
|
||||
it('should return invalid parameter', () => {
|
||||
setWindowLocation({ search: '?scope=all&p=2' });
|
||||
|
||||
expect(getParameterByName('fakeParameter')).toBe(null);
|
||||
});
|
||||
|
||||
it('should return a parameter with spaces', () => {
|
||||
setWindowLocation({ search: '?search=my terms' });
|
||||
|
||||
expect(getParameterByName('search')).toBe('my terms');
|
||||
});
|
||||
|
||||
it('should return a parameter with encoded spaces', () => {
|
||||
setWindowLocation({ search: '?search=my%20terms' });
|
||||
|
||||
expect(getParameterByName('search')).toBe('my terms');
|
||||
});
|
||||
|
||||
it('should return a parameter with plus signs as spaces', () => {
|
||||
setWindowLocation({ search: '?search=my+terms' });
|
||||
|
||||
expect(getParameterByName('search')).toBe('my terms');
|
||||
});
|
||||
|
||||
it('should return valid parameters if search is provided', () => {
|
||||
expect(getParameterByName('foo', 'foo=bar')).toBe('bar');
|
||||
expect(getParameterByName('foo', '?foo=bar')).toBe('bar');
|
||||
|
||||
expect(getParameterByName('manan', 'foo=bar&manan=canchu')).toBe('canchu');
|
||||
expect(getParameterByName('manan', '?foo=bar&manan=canchu')).toBe('canchu');
|
||||
});
|
||||
});
|
||||
|
||||
describe('objectToQuery', () => {
|
||||
it('converts search query object back into a search query', () => {
|
||||
const searchQueryObject = { one: '1', two: '2' };
|
||||
|
|
|
|||
|
|
@ -207,7 +207,8 @@ describe('Pipeline editor branch switcher', () => {
|
|||
|
||||
it('updates session history when selecting a different branch', async () => {
|
||||
const branch = findDropdownItems().at(1);
|
||||
await branch.vm.$emit('click');
|
||||
branch.vm.$emit('click');
|
||||
await waitForPromises();
|
||||
|
||||
expect(window.history.pushState).toHaveBeenCalled();
|
||||
expect(window.history.pushState.mock.calls[0][2]).toContain(`?branch_name=${branch.text()}`);
|
||||
|
|
@ -215,7 +216,8 @@ describe('Pipeline editor branch switcher', () => {
|
|||
|
||||
it('does not update session history when selecting current branch', async () => {
|
||||
const branch = findDropdownItems().at(0);
|
||||
await branch.vm.$emit('click');
|
||||
branch.vm.$emit('click');
|
||||
await waitForPromises();
|
||||
|
||||
expect(branch.text()).toBe(mockDefaultBranch);
|
||||
expect(window.history.pushState).not.toHaveBeenCalled();
|
||||
|
|
@ -227,7 +229,8 @@ describe('Pipeline editor branch switcher', () => {
|
|||
expect(branch.text()).not.toBe(mockDefaultBranch);
|
||||
expect(wrapper.emitted('refetchContent')).toBeUndefined();
|
||||
|
||||
await branch.vm.$emit('click');
|
||||
branch.vm.$emit('click');
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('refetchContent')).toBeDefined();
|
||||
expect(wrapper.emitted('refetchContent')).toHaveLength(1);
|
||||
|
|
@ -239,10 +242,20 @@ describe('Pipeline editor branch switcher', () => {
|
|||
expect(branch.text()).toBe(mockDefaultBranch);
|
||||
expect(wrapper.emitted('refetchContent')).toBeUndefined();
|
||||
|
||||
await branch.vm.$emit('click');
|
||||
branch.vm.$emit('click');
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('refetchContent')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('emits the updateCommitSha event when selecting a different branch', async () => {
|
||||
expect(wrapper.emitted('updateCommitSha')).toBeUndefined();
|
||||
|
||||
const branch = findDropdownItems().at(1);
|
||||
branch.vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('updateCommitSha')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when searching', () => {
|
||||
|
|
|
|||
|
|
@ -156,6 +156,35 @@ export const mergeUnwrappedCiConfig = (mergedConfig) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const mockNewCommitShaResults = {
|
||||
data: {
|
||||
project: {
|
||||
pipelines: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/Ci::Pipeline/1',
|
||||
sha: 'd0d56d363d8a3f67a8ab9fc00207d468f30032ca',
|
||||
path: `/${mockProjectFullPath}/-/pipelines/488`,
|
||||
commitPath: `/${mockProjectFullPath}/-/commit/d0d56d363d8a3f67a8ab9fc00207d468f30032ca`,
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/Ci::Pipeline/2',
|
||||
sha: 'fcab2ece40b26f428dfa3aa288b12c3c5bdb06aa',
|
||||
path: `/${mockProjectFullPath}/-/pipelines/487`,
|
||||
commitPath: `/${mockProjectFullPath}/-/commit/fcab2ece40b26f428dfa3aa288b12c3c5bdb06aa`,
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/Ci::Pipeline/3',
|
||||
sha: '6c16b17c7f94a438ae19a96c285bb49e3c632cf4',
|
||||
path: `/${mockProjectFullPath}/-/pipelines/433`,
|
||||
commitPath: `/${mockProjectFullPath}/-/commit/6c16b17c7f94a438ae19a96c285bb49e3c632cf4`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockProjectBranches = {
|
||||
data: {
|
||||
project: {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_edi
|
|||
import { COMMIT_SUCCESS, COMMIT_FAILURE } from '~/pipeline_editor/constants';
|
||||
import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.graphql';
|
||||
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
|
||||
import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql';
|
||||
import getTemplate from '~/pipeline_editor/graphql/queries/get_starter_template.query.graphql';
|
||||
import getLatestCommitShaQuery from '~/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql';
|
||||
import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
|
||||
import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
|
||||
import {
|
||||
|
|
@ -24,6 +26,7 @@ import {
|
|||
mockDefaultBranch,
|
||||
mockProjectFullPath,
|
||||
mockCiYml,
|
||||
mockNewCommitShaResults,
|
||||
} from './mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
|
@ -49,6 +52,9 @@ describe('Pipeline editor app component', () => {
|
|||
let mockBlobContentData;
|
||||
let mockCiConfigData;
|
||||
let mockGetTemplate;
|
||||
let mockUpdateCommitSha;
|
||||
let mockLatestCommitShaQuery;
|
||||
let mockPipelineQuery;
|
||||
|
||||
const createComponent = ({ blobLoading = false, options = {}, provide = {} } = {}) => {
|
||||
wrapper = shallowMount(PipelineEditorApp, {
|
||||
|
|
@ -84,9 +90,16 @@ describe('Pipeline editor app component', () => {
|
|||
[getBlobContent, mockBlobContentData],
|
||||
[getCiConfigData, mockCiConfigData],
|
||||
[getTemplate, mockGetTemplate],
|
||||
[getLatestCommitShaQuery, mockLatestCommitShaQuery],
|
||||
[getPipelineQuery, mockPipelineQuery],
|
||||
];
|
||||
|
||||
mockApollo = createMockApollo(handlers);
|
||||
const resolvers = {
|
||||
Mutation: {
|
||||
updateCommitSha: mockUpdateCommitSha,
|
||||
},
|
||||
};
|
||||
mockApollo = createMockApollo(handlers, resolvers);
|
||||
|
||||
const options = {
|
||||
localVue,
|
||||
|
|
@ -116,6 +129,9 @@ describe('Pipeline editor app component', () => {
|
|||
mockBlobContentData = jest.fn();
|
||||
mockCiConfigData = jest.fn();
|
||||
mockGetTemplate = jest.fn();
|
||||
mockUpdateCommitSha = jest.fn();
|
||||
mockLatestCommitShaQuery = jest.fn();
|
||||
mockPipelineQuery = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -347,4 +363,45 @@ describe('Pipeline editor app component', () => {
|
|||
expect(findTextEditor().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updating commit sha', () => {
|
||||
const newCommitSha = mockNewCommitShaResults.data.project.pipelines.nodes[0].sha;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockUpdateCommitSha.mockResolvedValue(newCommitSha);
|
||||
mockLatestCommitShaQuery.mockResolvedValue(mockNewCommitShaResults);
|
||||
await createComponentWithApollo();
|
||||
});
|
||||
|
||||
it('fetches updated commit sha for the new branch', async () => {
|
||||
expect(mockLatestCommitShaQuery).not.toHaveBeenCalled();
|
||||
|
||||
wrapper
|
||||
.findComponent(PipelineEditorHome)
|
||||
.vm.$emit('updateCommitSha', { newBranch: 'new-branch' });
|
||||
await waitForPromises();
|
||||
|
||||
expect(mockLatestCommitShaQuery).toHaveBeenCalledWith({
|
||||
projectPath: mockProjectFullPath,
|
||||
ref: 'new-branch',
|
||||
});
|
||||
});
|
||||
|
||||
it('updates commit sha with the newly fetched commit sha', async () => {
|
||||
expect(mockUpdateCommitSha).not.toHaveBeenCalled();
|
||||
|
||||
wrapper
|
||||
.findComponent(PipelineEditorHome)
|
||||
.vm.$emit('updateCommitSha', { newBranch: 'new-branch' });
|
||||
await waitForPromises();
|
||||
|
||||
expect(mockUpdateCommitSha).toHaveBeenCalled();
|
||||
expect(mockUpdateCommitSha).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
{ commitSha: mockNewCommitShaResults.data.project.pipelines.nodes[0].sha },
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import filesQuery from 'shared_queries/repository/files.query.graphql';
|
||||
import FilePreview from '~/repository/components/preview/index.vue';
|
||||
import FileTable from '~/repository/components/table/index.vue';
|
||||
import TreeContent from '~/repository/components/tree_content.vue';
|
||||
import { TREE_INITIAL_FETCH_COUNT } from '~/repository/constants';
|
||||
|
||||
let vm;
|
||||
let $apollo;
|
||||
|
|
@ -23,6 +23,8 @@ function factory(path, data = () => ({})) {
|
|||
}
|
||||
|
||||
describe('Repository table component', () => {
|
||||
const findFileTable = () => vm.find(FileTable);
|
||||
|
||||
afterEach(() => {
|
||||
vm.destroy();
|
||||
});
|
||||
|
|
@ -85,14 +87,12 @@ describe('Repository table component', () => {
|
|||
|
||||
describe('FileTable showMore', () => {
|
||||
describe('when is present', () => {
|
||||
const fileTable = () => vm.find(FileTable);
|
||||
|
||||
beforeEach(async () => {
|
||||
factory('/');
|
||||
});
|
||||
|
||||
it('is changes hasShowMore to false when "showMore" event is emitted', async () => {
|
||||
fileTable().vm.$emit('showMore');
|
||||
findFileTable().vm.$emit('showMore');
|
||||
|
||||
await vm.vm.$nextTick();
|
||||
|
||||
|
|
@ -100,7 +100,7 @@ describe('Repository table component', () => {
|
|||
});
|
||||
|
||||
it('changes clickedShowMore when "showMore" event is emitted', async () => {
|
||||
fileTable().vm.$emit('showMore');
|
||||
findFileTable().vm.$emit('showMore');
|
||||
|
||||
await vm.vm.$nextTick();
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ describe('Repository table component', () => {
|
|||
it('triggers fetchFiles when "showMore" event is emitted', () => {
|
||||
jest.spyOn(vm.vm, 'fetchFiles');
|
||||
|
||||
fileTable().vm.$emit('showMore');
|
||||
findFileTable().vm.$emit('showMore');
|
||||
|
||||
expect(vm.vm.fetchFiles).toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -126,10 +126,52 @@ describe('Repository table component', () => {
|
|||
expect(vm.vm.hasShowMore).toBe(false);
|
||||
});
|
||||
|
||||
it('has limit of 1000 files on initial load', () => {
|
||||
it.each`
|
||||
totalBlobs | pagesLoaded | limitReached
|
||||
${900} | ${1} | ${false}
|
||||
${1000} | ${1} | ${true}
|
||||
${1002} | ${1} | ${true}
|
||||
${1002} | ${2} | ${false}
|
||||
${1900} | ${2} | ${false}
|
||||
${2000} | ${2} | ${true}
|
||||
`('has limit of 1000 entries per page', async ({ totalBlobs, pagesLoaded, limitReached }) => {
|
||||
factory('/');
|
||||
|
||||
expect(TREE_INITIAL_FETCH_COUNT * vm.vm.pageSize).toBe(1000);
|
||||
const blobs = new Array(totalBlobs).fill('fakeBlob');
|
||||
vm.setData({ entries: { blobs }, pagesLoaded });
|
||||
|
||||
await vm.vm.$nextTick();
|
||||
|
||||
expect(findFileTable().props('hasMore')).toBe(limitReached);
|
||||
});
|
||||
|
||||
it.each`
|
||||
fetchCounter | pageSize
|
||||
${0} | ${10}
|
||||
${2} | ${30}
|
||||
${4} | ${50}
|
||||
${6} | ${70}
|
||||
${8} | ${90}
|
||||
${10} | ${100}
|
||||
${20} | ${100}
|
||||
${100} | ${100}
|
||||
${200} | ${100}
|
||||
`('exponentially increases page size, to a maximum of 100', ({ fetchCounter, pageSize }) => {
|
||||
factory('/');
|
||||
vm.setData({ fetchCounter });
|
||||
|
||||
vm.vm.fetchFiles();
|
||||
|
||||
expect($apollo.query).toHaveBeenCalledWith({
|
||||
query: filesQuery,
|
||||
variables: {
|
||||
pageSize,
|
||||
nextPageCursor: '',
|
||||
path: '/',
|
||||
projectPath: '',
|
||||
ref: '',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ RSpec.describe Ci::PipelineEditorHelper do
|
|||
"commit-sha" => project.commit.sha,
|
||||
"default-branch" => project.default_branch_or_main,
|
||||
"empty-state-illustration-path" => 'foo',
|
||||
"initial-branch-name": nil,
|
||||
"initial-branch-name" => nil,
|
||||
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
|
||||
"needs-help-page-path" => help_page_path('ci/yaml/README', anchor: 'needs'),
|
||||
"new-merge-request-path" => '/mock/project/-/merge_requests/new',
|
||||
|
|
@ -72,7 +72,7 @@ RSpec.describe Ci::PipelineEditorHelper do
|
|||
"commit-sha" => '',
|
||||
"default-branch" => project.default_branch_or_main,
|
||||
"empty-state-illustration-path" => 'foo',
|
||||
"initial-branch-name": nil,
|
||||
"initial-branch-name" => nil,
|
||||
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
|
||||
"needs-help-page-path" => help_page_path('ci/yaml/README', anchor: 'needs'),
|
||||
"new-merge-request-path" => '/mock/project/-/merge_requests/new',
|
||||
|
|
@ -87,5 +87,21 @@ RSpec.describe Ci::PipelineEditorHelper do
|
|||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a non-default branch name' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
create_commit('Message', project, user, 'feature')
|
||||
controller.params[:branch_name] = 'feature'
|
||||
end
|
||||
|
||||
it 'returns correct values' do
|
||||
latest_feature_sha = project.repository.commit('feature').sha
|
||||
|
||||
expect(pipeline_editor_data['initial-branch-name']).to eq('feature')
|
||||
expect(pipeline_editor_data['commit-sha']).to eq(latest_feature_sha)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::BackfillUpvotesCountOnIssues, schema: 20210701111909 do
|
||||
let(:award_emoji) { table(:award_emoji) }
|
||||
|
||||
let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
|
||||
let!(:project1) { table(:projects).create!(namespace_id: namespace.id) }
|
||||
let!(:project2) { table(:projects).create!(namespace_id: namespace.id) }
|
||||
let!(:issue1) { table(:issues).create!(project_id: project1.id) }
|
||||
let!(:issue2) { table(:issues).create!(project_id: project2.id) }
|
||||
let!(:issue3) { table(:issues).create!(project_id: project2.id) }
|
||||
let!(:issue4) { table(:issues).create!(project_id: project2.id) }
|
||||
|
||||
describe '#perform' do
|
||||
before do
|
||||
add_upvotes(issue1, :thumbsdown, 1)
|
||||
add_upvotes(issue2, :thumbsup, 2)
|
||||
add_upvotes(issue2, :thumbsdown, 1)
|
||||
add_upvotes(issue3, :thumbsup, 3)
|
||||
add_upvotes(issue4, :thumbsup, 4)
|
||||
end
|
||||
|
||||
it 'updates upvotes_count' do
|
||||
subject.perform(issue1.id, issue4.id)
|
||||
|
||||
expect(issue1.reload.upvotes_count).to eq(0)
|
||||
expect(issue2.reload.upvotes_count).to eq(2)
|
||||
expect(issue3.reload.upvotes_count).to eq(3)
|
||||
expect(issue4.reload.upvotes_count).to eq(4)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_upvotes(issue, name, count)
|
||||
count.times do
|
||||
award_emoji.create!(
|
||||
name: name.to_s,
|
||||
awardable_type: 'Issue',
|
||||
awardable_id: issue.id
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe BackfillIssuesUpvotesCount do
|
||||
let(:migration) { described_class.new }
|
||||
let(:issues) { table(:issues) }
|
||||
let(:award_emoji) { table(:award_emoji) }
|
||||
|
||||
let!(:issue1) { issues.create! }
|
||||
let!(:issue2) { issues.create! }
|
||||
let!(:issue3) { issues.create! }
|
||||
let!(:issue4) { issues.create! }
|
||||
let!(:issue4_without_thumbsup) { issues.create! }
|
||||
|
||||
let!(:award_emoji1) { award_emoji.create!( name: 'thumbsup', awardable_type: 'Issue', awardable_id: issue1.id) }
|
||||
let!(:award_emoji2) { award_emoji.create!( name: 'thumbsup', awardable_type: 'Issue', awardable_id: issue2.id) }
|
||||
let!(:award_emoji3) { award_emoji.create!( name: 'thumbsup', awardable_type: 'Issue', awardable_id: issue3.id) }
|
||||
let!(:award_emoji4) { award_emoji.create!( name: 'thumbsup', awardable_type: 'Issue', awardable_id: issue4.id) }
|
||||
|
||||
it 'correctly schedules background migrations' do
|
||||
stub_const("#{described_class.name}::BATCH_SIZE", 2)
|
||||
|
||||
Sidekiq::Testing.fake! do
|
||||
freeze_time do
|
||||
migrate!
|
||||
|
||||
expect(described_class::MIGRATION).to be_scheduled_migration(issue1.id, issue2.id)
|
||||
expect(described_class::MIGRATION).to be_scheduled_migration(issue3.id, issue4.id)
|
||||
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -171,4 +171,43 @@ RSpec.describe AwardEmoji do
|
|||
expect(awards).to eq('thumbsup' => 2)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'updating upvotes_count' do
|
||||
context 'on an issue' do
|
||||
let(:issue) { create(:issue) }
|
||||
let(:upvote) { build(:award_emoji, :upvote, user: build(:user), awardable: issue) }
|
||||
let(:downvote) { build(:award_emoji, :downvote, user: build(:user), awardable: issue) }
|
||||
|
||||
it 'updates upvotes_count on the issue when saved' do
|
||||
expect(issue).to receive(:update_column).with(:upvotes_count, 1).once
|
||||
|
||||
upvote.save!
|
||||
downvote.save!
|
||||
end
|
||||
|
||||
it 'updates upvotes_count on the issue when destroyed' do
|
||||
expect(issue).to receive(:update_column).with(:upvotes_count, 0).once
|
||||
|
||||
upvote.destroy!
|
||||
downvote.destroy!
|
||||
end
|
||||
end
|
||||
|
||||
context 'on another awardable' do
|
||||
let(:merge_request) { create(:merge_request) }
|
||||
let(:award_emoji) { build(:award_emoji, user: build(:user), awardable: merge_request) }
|
||||
|
||||
it 'does not update upvotes_count on the merge_request when saved' do
|
||||
expect(merge_request).not_to receive(:update_column)
|
||||
|
||||
award_emoji.save!
|
||||
end
|
||||
|
||||
it 'does not update upvotes_count on the merge_request when destroyed' do
|
||||
expect(merge_request).not_to receive(:update_column)
|
||||
|
||||
award_emoji.destroy!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -488,17 +488,6 @@ RSpec.describe API::Repositories do
|
|||
let(:current_user) { nil }
|
||||
end
|
||||
end
|
||||
|
||||
context 'api_caching_repository_compare is disabled' do
|
||||
before do
|
||||
stub_feature_flags(api_caching_repository_compare: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'repository compare' do
|
||||
let(:project) { create(:project, :public, :repository) }
|
||||
let(:current_user) { nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /projects/:id/repository/contributors' do
|
||||
|
|
|
|||
|
|
@ -611,11 +611,12 @@ RSpec.describe API::Wikis do
|
|||
let(:payload) { { file: fixture_file_upload('spec/fixtures/dk.png') } }
|
||||
let(:url) { "/projects/#{project.id}/wikis/attachments" }
|
||||
let(:file_path) { "#{Wikis::CreateAttachmentService::ATTACHMENT_PATH}/fixed_hex/dk.png" }
|
||||
let(:branch) { wiki.default_branch }
|
||||
let(:result_hash) do
|
||||
{
|
||||
file_name: 'dk.png',
|
||||
file_path: file_path,
|
||||
branch: 'master',
|
||||
branch: branch,
|
||||
link: {
|
||||
url: file_path,
|
||||
markdown: ""
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ RSpec.describe Tooling::Danger::ProjectHelper do
|
|||
|
||||
describe '.local_warning_message' do
|
||||
it 'returns an informational message with rules that can run' do
|
||||
expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changelog, database, datateam, documentation, duplicate_yarn_dependencies, eslint, karma, pajamas, pipeline, prettier, product_intelligence, utility_css')
|
||||
expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changelog, database, datateam, documentation, duplicate_yarn_dependencies, eslint, gitaly, karma, pajamas, pipeline, prettier, product_intelligence, utility_css')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ module Tooling
|
|||
documentation
|
||||
duplicate_yarn_dependencies
|
||||
eslint
|
||||
gitaly
|
||||
karma
|
||||
pajamas
|
||||
pipeline
|
||||
|
|
|
|||
|
|
@ -908,10 +908,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
|
||||
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
|
||||
|
||||
"@gitlab/ui@31.0.1":
|
||||
version "31.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-31.0.1.tgz#55c481f2e2fa777ff34237a8229f39553428d107"
|
||||
integrity sha512-Sw7Hm9VZ4ZE6knZNkd9L7vs1DGmeTFC1d0xzDytOKBw+1kK1+CpCLae2ehT+Kkkwho9GLwUFtHDdATEDLbFaBg==
|
||||
"@gitlab/ui@31.2.0":
|
||||
version "31.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-31.2.0.tgz#7716500c9e811560d6e450d8553bf71bdcba79ec"
|
||||
integrity sha512-hbW3Zd/gIN4C/AKx27ChZy4lf9yW8TBTJwG85dqQKSYvqWG3LuLx7o0kvc+UJqVFK3lk1iUC3pUSN2UrQ+isqg==
|
||||
dependencies:
|
||||
"@babel/standalone" "^7.0.0"
|
||||
bootstrap-vue "2.18.1"
|
||||
|
|
|
|||
Loading…
Reference in New Issue