diff options
Diffstat (limited to 'app')
29 files changed, 259 insertions, 188 deletions
diff --git a/app/assets/javascripts/content_editor/services/upload_file.js b/app/assets/javascripts/content_editor/services/upload_file.js new file mode 100644 index 00000000000..613c53144a1 --- /dev/null +++ b/app/assets/javascripts/content_editor/services/upload_file.js @@ -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); +}; diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js index 8a66d4d11b7..a7d44322eb1 100644 --- a/app/assets/javascripts/groups/groups_filterable_list.js +++ b/app/assets/javascripts/groups/groups_filterable_list.js @@ -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) { diff --git a/app/assets/javascripts/issuable/components/issuable_by_email.vue b/app/assets/javascripts/issuable/components/issuable_by_email.vue index 48eadec1d5c..6e300831e00 100644 --- a/app/assets/javascripts/issuable/components/issuable_by_email.vue +++ b/app/assets/javascripts/issuable/components/issuable_by_email.vue @@ -36,7 +36,7 @@ export default { default: null, }, issuableType: { - default: '', + default: 'issue', }, emailsHelpPagePath: { default: '', diff --git a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue index 70d73aca925..07492b0046c 100644 --- a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue +++ b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue @@ -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 diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue index fcd31013fe5..e3cc43d2679 100644 --- a/app/assets/javascripts/issues_list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue @@ -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" /> diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js index e4b9136343e..d94d4b9a19a 100644 --- a/app/assets/javascripts/issues_list/constants.js +++ b/app/assets/javascripts/issues_list/constants.js @@ -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'), }; diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js index dc73d8c7cc8..71ceb9bef55 100644 --- a/app/assets/javascripts/issues_list/index.js +++ b/app/assets/javascripts/issues_list/index.js @@ -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, diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue index 35b16d73cc7..e31c13f40b0 100644 --- a/app/assets/javascripts/jobs/components/empty_state.vue +++ b/app/assets/javascripts/jobs/components/empty_state.vue @@ -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 diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index be95001a396..fa9ee56c049 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -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 --> diff --git a/app/assets/javascripts/jobs/components/manual_variables_form.vue b/app/assets/javascripts/jobs/components/manual_variables_form.vue index d45012d2023..269551ff9aa 100644 --- a/app/assets/javascripts/jobs/components/manual_variables_form.vue +++ b/app/assets/javascripts/jobs/components/manual_variables_form.vue @@ -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 diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js index 260190f5043..1fb6a6f9850 100644 --- a/app/assets/javascripts/jobs/index.js +++ b/app/assets/javascripts/jobs/index.js @@ -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, diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js index cec689a44ca..d91fc61ba21 100644 --- a/app/assets/javascripts/lib/graphql.js +++ b/app/assets/javascripts/lib/graphql.js @@ -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, ]), ); diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 8b02d2567a4..7922ff22a70 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -108,25 +108,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. * @@ -514,6 +495,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 * * @param {Object?} params that needs to be converted diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue index 05b87abecd5..e01f9cc79c0 100644 --- a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue +++ b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue @@ -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) { diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue index 368a026bdaa..6af3361e7e6 100644 --- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue +++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue @@ -66,6 +66,7 @@ export default { }, data() { return { + commitSha: '', hasError: false, }; }, diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/update_commit_sha.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/update_commit_sha.mutation.graphql new file mode 100644 index 00000000000..dce17cad808 --- /dev/null +++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/update_commit_sha.mutation.graphql @@ -0,0 +1,3 @@ +mutation updateCommitSha($commitSha: String) { + updateCommitSha(commitSha: $commitSha) @client +} diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql new file mode 100644 index 00000000000..219c23bb22b --- /dev/null +++ b/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql @@ -0,0 +1,12 @@ +query getLatestCommitSha($projectPath: ID!, $ref: String) { + project(fullPath: $projectPath) { + pipelines(ref: $ref) { + nodes { + id + sha + path + commitPath + } + } + } +} diff --git a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js index ad333f6d42a..2bec2006e95 100644 --- a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js +++ b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js @@ -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) => { diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue index c7de8516c86..758c8c51a5b 100644 --- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue +++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue @@ -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> diff --git a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue index 07d8f3cc5f1..a0129dd536b 100644 --- a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue +++ b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue @@ -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" diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue index 794a8a85cc5..cf1cff9023e 100644 --- a/app/assets/javascripts/repository/components/tree_content.vue +++ b/app/assets/javascripts/repository/components/tree_content.vue @@ -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(); }, }, diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js index 62d5d3db445..22349261d3c 100644 --- a/app/assets/javascripts/repository/constants.js +++ b/app/assets/javascripts/repository/constants.js @@ -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 diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index 6668af33a1c..82d768c2351 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -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; +} diff --git a/app/graphql/resolvers/ci/template_resolver.rb b/app/graphql/resolvers/ci/template_resolver.rb index dd910116544..7f5a1a486d7 100644 --- a/app/graphql/resolvers/ci/template_resolver.rb +++ b/app/graphql/resolvers/ci/template_resolver.rb @@ -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 diff --git a/app/helpers/ci/jobs_helper.rb b/app/helpers/ci/jobs_helper.rb index 23f2a082a68..882302f05ad 100644 --- a/app/helpers/ci/jobs_helper.rb +++ b/app/helpers/ci/jobs_helper.rb @@ -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, diff --git a/app/helpers/ci/pipeline_editor_helper.rb b/app/helpers/ci/pipeline_editor_helper.rb index 1a30b6bed08..d441ffbb853 100644 --- a/app/helpers/ci/pipeline_editor_helper.rb +++ b/app/helpers/ci/pipeline_editor_helper.rb @@ -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, diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index 7679f6fce72..dc37d73df85 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -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') diff --git a/app/models/error_tracking/error_event.rb b/app/models/error_tracking/error_event.rb index 90f63f82a66..ed14a1bce41 100644 --- a/app/models/error_tracking/error_event.rb +++ b/app/models/error_tracking/error_event.rb @@ -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' } diff --git a/app/validators/json_schemas/error_tracking_event_payload.json b/app/validators/json_schemas/error_tracking_event_payload.json index 79e81672d0e..19abde7de08 100644 --- a/app/validators/json_schemas/error_tracking_event_payload.json +++ b/app/validators/json_schemas/error_tracking_event_payload.json @@ -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" } } } |