diff options
Diffstat (limited to 'app')
79 files changed, 558 insertions, 333 deletions
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue deleted file mode 100644 index 6c256fa6736..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue +++ /dev/null @@ -1,59 +0,0 @@ -<script> -import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; -import iconCommit from '../svg/icon_commit.svg'; -import limitWarning from './limit_warning_component.vue'; -import totalTime from './total_time_component.vue'; - -export default { - components: { - userAvatarImage, - totalTime, - limitWarning, - }, - props: { - items: { - type: Array, - default: () => [], - }, - stage: { - type: Object, - default: () => ({}), - }, - }, - computed: { - iconCommit() { - return iconCommit; - }, - }, -}; -</script> -<template> - <div> - <div class="events-description"> - {{ stage.description }} - <limit-warning :count="items.length" /> - </div> - <ul class="stage-event-list"> - <li v-for="(commit, i) in items" :key="i" class="stage-event-item"> - <div class="item-details item-conmmit-component"> - <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image :img-src="commit.author.avatarUrl" /> - <h5 class="item-title commit-title"> - <a :href="commit.commitUrl"> {{ commit.title }} </a> - </h5> - <span> - {{ s__('FirstPushedBy|First') }} <span class="commit-icon" v-html="iconCommit"> </span> - <a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ - commit.shortSha - }}</a> - {{ s__('FirstPushedBy|pushed by') }} - <a :href="commit.author.webUrl" class="commit-author-link"> - {{ commit.author.name }} - </a> - </span> - </div> - <div class="item-time"><total-time :time="commit.totalTime" /></div> - </li> - </ul> - </div> -</template> diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 3f0a9f2602c..b56e08175cc 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -5,7 +5,6 @@ import Flash from '../flash'; import Translate from '../vue_shared/translate'; import banner from './components/banner.vue'; import stageCodeComponent from './components/stage_code_component.vue'; -import stagePlanComponent from './components/stage_plan_component.vue'; import stageComponent from './components/stage_component.vue'; import stageReviewComponent from './components/stage_review_component.vue'; import stageStagingComponent from './components/stage_staging_component.vue'; @@ -26,7 +25,7 @@ export default () => { components: { banner, 'stage-issue-component': stageComponent, - 'stage-plan-component': stagePlanComponent, + 'stage-plan-component': stageComponent, 'stage-code-component': stageCodeComponent, 'stage-test-component': stageTestComponent, 'stage-review-component': stageReviewComponent, diff --git a/app/assets/javascripts/filtered_search/constants.js b/app/assets/javascripts/filtered_search/constants.js new file mode 100644 index 00000000000..8ca0e94cfeb --- /dev/null +++ b/app/assets/javascripts/filtered_search/constants.js @@ -0,0 +1,2 @@ +/* eslint-disable import/prefer-default-export */ +export const TOKEN_TYPES = ['author', 'assignee']; diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index 315cd6f64da..7f6457242ef 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -1,4 +1,4 @@ -import VisualTokenValue from 'ee_else_ce/filtered_search/visual_token_value'; +import VisualTokenValue from './visual_token_value'; import { objectToQueryString } from '~/lib/utils/common_utils'; import FilteredSearchContainer from './container'; diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js index a54b445fb0a..018207541b3 100644 --- a/app/assets/javascripts/filtered_search/visual_token_value.js +++ b/app/assets/javascripts/filtered_search/visual_token_value.js @@ -6,6 +6,7 @@ import DropdownUtils from '~/filtered_search/dropdown_utils'; import Flash from '~/flash'; import UsersCache from '~/lib/utils/users_cache'; import { __ } from '~/locale'; +import { TOKEN_TYPES } from 'ee_else_ce/filtered_search/constants'; export default class VisualTokenValue { constructor(tokenValue, tokenType) { @@ -22,7 +23,7 @@ export default class VisualTokenValue { if (tokenType === 'label') { this.updateLabelTokenColor(tokenValueContainer); - } else if (tokenType === 'author' || tokenType === 'assignee') { + } else if (TOKEN_TYPES.includes(tokenType)) { this.updateUserTokenAppearance(tokenValueContainer, tokenValueElement); } else if (tokenType === 'my-reaction') { this.updateEmojiTokenAppearance(tokenValueContainer, tokenValueElement); diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index ba33b6826d6..840761f68db 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -56,7 +56,13 @@ export default { return Api.branchSingle(projectId, currentBranchId); }, commit(projectId, payload) { - return Api.commitMultiple(projectId, payload); + // Currently the `commit` endpoint does not support `start_sha` so we + // have to make the request in the FE. This is not ideal and will be + // resolved soon. https://gitlab.com/gitlab-org/gitlab-ce/issues/59023 + const { branch, start_sha: ref } = payload; + const branchPromise = ref ? Api.createBranch(projectId, { ref, branch }) : Promise.resolve(); + + return branchPromise.then(() => Api.commitMultiple(projectId, payload)); }, getFiles(projectUrl, branchId) { const url = `${projectUrl}/files/${branchId}`; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 51062f092ad..ff1255ce749 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -142,6 +142,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo getters, state, rootState, + rootGetters, }); return service.commit(rootState.currentProjectId, payload); diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index bcc9ca60d9b..4e7a8765abe 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -135,7 +135,14 @@ export const getCommitFiles = stagedFiles => }); }, []); -export const createCommitPayload = ({ branch, getters, newBranch, state, rootState }) => ({ +export const createCommitPayload = ({ + branch, + getters, + newBranch, + state, + rootState, + rootGetters, +}) => ({ branch, commit_message: state.commitMessage || getters.preBuiltCommitMessage, actions: getCommitFiles(rootState.stagedFiles).map(f => ({ @@ -146,7 +153,7 @@ export const createCommitPayload = ({ branch, getters, newBranch, state, rootSta encoding: f.base64 ? 'base64' : 'text', last_commit_id: newBranch || f.deleted || f.prevPath ? undefined : f.lastCommitSha, })), - start_branch: newBranch ? rootState.currentBranchId : undefined, + start_sha: newBranch ? rootGetters.lastCommit.short_id : undefined, }); export const createNewMergeRequestUrl = (projectUrl, source, target) => diff --git a/app/assets/javascripts/jobs/components/job_log.vue b/app/assets/javascripts/jobs/components/job_log.vue index 92e20e92d66..d611b370ab9 100644 --- a/app/assets/javascripts/jobs/components/job_log.vue +++ b/app/assets/javascripts/jobs/components/job_log.vue @@ -17,10 +17,19 @@ export default { ...mapState(['isScrolledToBottomBeforeReceivingTrace']), }, updated() { - this.$nextTick(() => this.handleScrollDown()); + this.$nextTick(() => { + this.handleScrollDown(); + this.handleCollapsibleRows(); + }); }, mounted() { - this.$nextTick(() => this.handleScrollDown()); + this.$nextTick(() => { + this.handleScrollDown(); + this.handleCollapsibleRows(); + }); + }, + destroyed() { + this.removeEventListener(); }, methods: { ...mapActions(['scrollBottom']), @@ -38,21 +47,45 @@ export default { }, 0); } }, + removeEventListener() { + this.$el + .querySelectorAll('.js-section-start') + .forEach(el => el.removeEventListener('click', this.handleSectionClick)); + }, + /** + * The collapsible rows are sent in HTML from the backend + * We need tos add a onclick handler for the divs that match `.js-section-start` + * + */ + handleCollapsibleRows() { + this.$el + .querySelectorAll('.js-section-start') + .forEach(el => el.addEventListener('click', this.handleSectionClick)); + }, + /** + * On click, we toggle the hidden class of + * all the rows that match the `data-section` selector + */ + handleSectionClick(evt) { + const clickedArrow = evt.currentTarget; + // toggle the arrow class + clickedArrow.classList.toggle('fa-caret-right'); + clickedArrow.classList.toggle('fa-caret-down'); + + const { section } = clickedArrow.dataset; + const sibilings = this.$el.querySelectorAll(`.js-s-${section}:not(.js-section-header)`); + + sibilings.forEach(row => row.classList.toggle('hidden')); + }, }, }; </script> <template> <pre class="js-build-trace build-trace qa-build-trace"> - <code - class="bash" - v-html="trace" - > + <code class="bash" v-html="trace"> </code> - <div - v-if="!isComplete" - class="js-log-animation build-loader-animation" - > + <div v-if="!isComplete" class="js-log-animation build-loader-animation"> <div class="dot"></div> <div class="dot"></div> <div class="dot"></div> diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue index cb073a9b04d..6e92b599b0a 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -2,7 +2,6 @@ import _ from 'underscore'; import { GlLink } from '@gitlab/ui'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; -import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue'; import Icon from '~/vue_shared/components/icon.vue'; export default { @@ -10,7 +9,6 @@ export default { CiIcon, Icon, GlLink, - PipelineLink, }, props: { pipeline: { @@ -50,12 +48,9 @@ export default { <ci-icon :status="pipeline.details.status" class="vertical-align-middle" /> <span class="font-weight-bold">{{ s__('Job|Pipeline') }}</span> - <pipeline-link - :href="pipeline.path" - :pipeline-id="pipeline.id" - :pipeline-iid="pipeline.iid" - class="js-pipeline-path link-commit qa-pipeline-path" - /> + <gl-link :href="pipeline.path" class="js-pipeline-path link-commit qa-pipeline-path" + >#{{ pipeline.id }}</gl-link + > <template v-if="hasRef"> {{ s__('Job|for') }} diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index e5cf43e8289..b6868e63716 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -147,14 +147,14 @@ export default class MergeRequestTabs { e.stopImmediatePropagation(); e.preventDefault(); - const { action } = e.currentTarget.dataset; + const { action } = e.currentTarget.dataset || {}; - if (action) { - const href = e.currentTarget.getAttribute('href'); - this.tabShown(action, href); - } else if (isMetaClick(e)) { + if (isMetaClick(e)) { const targetLink = e.currentTarget.getAttribute('href'); window.open(targetLink, '_blank'); + } else if (action) { + const href = e.currentTarget.getAttribute('href'); + this.tabShown(action, href); } } } diff --git a/app/assets/javascripts/mr_popover/queries/merge_request.graphql b/app/assets/javascripts/mr_popover/queries/merge_request.graphql index 0bb9bc03bc7..37d4bc88a69 100644 --- a/app/assets/javascripts/mr_popover/queries/merge_request.graphql +++ b/app/assets/javascripts/mr_popover/queries/merge_request.graphql @@ -1,4 +1,4 @@ -query mergeRequest($projectPath: ID!, $mergeRequestIID: ID!) { +query mergeRequest($projectPath: ID!, $mergeRequestIID: String!) { project(fullPath: $projectPath) { mergeRequest(iid: $mergeRequestIID) { createdAt diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index f3a71ee434c..b2e365e5cde 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -83,8 +83,6 @@ export default { v-if="shouldRenderContent" :status="status" :item-id="pipeline.id" - :item-iid="pipeline.iid" - :item-id-tooltip="__('Pipeline ID (IID)')" :time="pipeline.created_at" :user="pipeline.user" :actions="actions" diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 00c02e15562..c41ecab1294 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -2,7 +2,6 @@ import { GlLink, GlTooltipDirective } from '@gitlab/ui'; import _ from 'underscore'; import { __, sprintf } from '~/locale'; -import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import popover from '~/vue_shared/directives/popover'; @@ -20,7 +19,6 @@ export default { components: { UserAvatarLink, GlLink, - PipelineLink, }, directives: { GlTooltip: GlTooltipDirective, @@ -61,13 +59,10 @@ export default { }; </script> <template> - <div class="table-section section-10 d-none d-sm-none d-md-block pipeline-tags section-wrap"> - <pipeline-link - :href="pipeline.path" - :pipeline-id="pipeline.id" - :pipeline-iid="pipeline.iid" - class="js-pipeline-url-link" - /> + <div class="table-section section-10 d-none d-sm-none d-md-block pipeline-tags"> + <gl-link :href="pipeline.path" class="js-pipeline-url-link"> + <span class="pipeline-id">#{{ pipeline.id }}</span> + </gl-link> <div class="label-container"> <span v-if="pipeline.flags.latest" diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index 1e4dfe76b26..f535b2ae9f2 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -64,7 +64,7 @@ export default { <th>{{ s__('ContainerRegistry|Tag') }}</th> <th>{{ s__('ContainerRegistry|Tag ID') }}</th> <th>{{ s__('ContainerRegistry|Size') }}</th> - <th>{{ s__('ContainerRegistry|Created') }}</th> + <th>{{ s__('ContainerRegistry|Last Updated') }}</th> <th></th> </tr> </thead> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index abe5bdd2901..34cdb70ce14 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -49,6 +49,7 @@ export default { required: false, default: () => ({ sourceProjectId: '', + sourceProjectPath: '', mergeRequestId: '', appUrl: '', }), @@ -184,11 +185,6 @@ export default { :link="deploymentExternalUrl" :css-class="`deploy-link js-deploy-url inline ${slotProps.className}`" /> - <visual-review-app-link - v-if="showVisualReviewApp" - :link="deploymentExternalUrl" - :app-metadata="visualReviewAppMeta" - /> </template> <template slot="result" slot-scope="slotProps"> @@ -213,12 +209,12 @@ export default { :link="deploymentExternalUrl" css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inline" /> - <visual-review-app-link - v-if="showVisualReviewApp" - :link="deploymentExternalUrl" - :app-metadata="visualReviewAppMeta" - /> </template> + <visual-review-app-link + v-if="showVisualReviewApp" + :link="deploymentExternalUrl" + :app-metadata="visualReviewAppMeta" + /> </template> <span v-if="deployment.stop_url" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index c377c16fb13..f5fa68308bc 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -5,7 +5,6 @@ import { sprintf, __ } from '~/locale'; import PipelineStage from '~/pipelines/components/stage.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; -import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline'; @@ -17,7 +16,6 @@ export default { Icon, TooltipOnTruncate, GlLink, - PipelineLink, LinkedPipelinesMiniList: () => import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'), }, @@ -114,12 +112,9 @@ export default { <div class="media-body"> <div class="font-weight-bold js-pipeline-info-container"> {{ s__('Pipeline|Pipeline') }} - <pipeline-link - :href="pipeline.path" - :pipeline-id="pipeline.id" - :pipeline-iid="pipeline.iid" - class="pipeline-id pipeline-iid font-weight-normal" - /> + <gl-link :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number" + >#{{ pipeline.id }}</gl-link + > {{ pipeline.details.status.label }} <template v-if="hasCommitInfo"> {{ s__('Pipeline|for') }} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue index 03a15ba81ed..17ac8ada32d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue @@ -1,4 +1,5 @@ <script> +import _ from 'underscore'; import Deployment from './deployment.vue'; import MrWidgetContainer from './mr_widget_container.vue'; import MrWidgetPipeline from './mr_widget_pipeline.vue'; @@ -17,6 +18,8 @@ export default { Deployment, MrWidgetContainer, MrWidgetPipeline, + MergeTrainInfo: () => + import('ee_component/vue_merge_request_widget/components/merge_train_info.vue'), }, props: { mr: { @@ -50,6 +53,7 @@ export default { appUrl: this.mr.appUrl, mergeRequestId: this.mr.iid, sourceProjectId: this.mr.sourceProjectId, + sourceProjectPath: this.mr.sourceProjectFullPath, }; }, pipeline() { @@ -58,6 +62,9 @@ export default { showVisualReviewAppLink() { return Boolean(this.mr.visualReviewFF && this.mr.visualReviewAppAvailable); }, + showMergeTrainInfo() { + return _.isNumber(this.mr.mergeTrainIndex); + }, }, }; </script> @@ -79,10 +86,15 @@ export default { :class="deploymentClass" :deployment="deployment" :show-metrics="hasDeploymentMetrics" - :show-visual-review-app="true" + :show-visual-review-app="showVisualReviewAppLink" :visual-review-app-meta="visualReviewAppMeta" /> </div> + <merge-train-info + v-if="showMergeTrainInfo" + class="mr-widget-extension" + :merge-train-index="mr.mergeTrainIndex" + /> </template> </mr-widget-container> </template> diff --git a/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue b/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue deleted file mode 100644 index eae4c06467c..00000000000 --- a/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue +++ /dev/null @@ -1,32 +0,0 @@ -<script> -import { GlLink, GlTooltipDirective } from '@gitlab/ui'; - -export default { - components: { - GlLink, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - props: { - href: { - type: String, - required: true, - }, - pipelineId: { - type: Number, - required: true, - }, - pipelineIid: { - type: Number, - required: true, - }, - }, -}; -</script> -<template> - <gl-link v-gl-tooltip :href="href" :title="__('Pipeline ID (IID)')"> - <span class="pipeline-id">#{{ pipelineId }}</span> - <span class="pipeline-iid">(#{{ pipelineIid }})</span> - </gl-link> -</template> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 0bac63b1062..3f45dc7853b 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -37,16 +37,6 @@ export default { type: Number, required: true, }, - itemIid: { - type: Number, - required: false, - default: null, - }, - itemIdTooltip: { - type: String, - required: false, - default: '', - }, time: { type: String, required: true, @@ -95,12 +85,7 @@ export default { <section class="header-main-content"> <ci-icon-badge :status="status" /> - <strong v-gl-tooltip :title="itemIdTooltip"> - {{ itemName }} #{{ itemId }} - <template v-if="itemIid" - >(#{{ itemIid }})</template - > - </strong> + <strong> {{ itemName }} #{{ itemId }} </strong> <template v-if="shouldRenderTriggeredLabel"> triggered @@ -111,8 +96,9 @@ export default { <timeago-tooltip :time="time" /> + by + <template v-if="user"> - by <gl-link v-gl-tooltip :href="user.path" diff --git a/app/assets/stylesheets/bootstrap.scss b/app/assets/stylesheets/bootstrap.scss deleted file mode 100644 index 4a09da3d580..00000000000 --- a/app/assets/stylesheets/bootstrap.scss +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Includes specific styles from the bootstrap4 folder in node_modules - */ - -@import "../../../node_modules/bootstrap/scss/functions"; -@import "../../../node_modules/bootstrap/scss/variables"; -@import "../../../node_modules/bootstrap/scss/mixins"; -@import "../../../node_modules/bootstrap/scss/root"; -@import "../../../node_modules/bootstrap/scss/reboot"; -@import "../../../node_modules/bootstrap/scss/type"; -@import "../../../node_modules/bootstrap/scss/images"; -@import "../../../node_modules/bootstrap/scss/code"; -@import "../../../node_modules/bootstrap/scss/grid"; -@import "../../../node_modules/bootstrap/scss/tables"; -@import "../../../node_modules/bootstrap/scss/forms"; -@import "../../../node_modules/bootstrap/scss/buttons"; -@import "../../../node_modules/bootstrap/scss/transitions"; -@import "../../../node_modules/bootstrap/scss/dropdown"; -@import "../../../node_modules/bootstrap/scss/button-group"; -@import "../../../node_modules/bootstrap/scss/input-group"; -@import "../../../node_modules/bootstrap/scss/custom-forms"; -@import "../../../node_modules/bootstrap/scss/nav"; -@import "../../../node_modules/bootstrap/scss/navbar"; -@import "../../../node_modules/bootstrap/scss/card"; -@import "../../../node_modules/bootstrap/scss/breadcrumb"; -@import "../../../node_modules/bootstrap/scss/pagination"; -@import "../../../node_modules/bootstrap/scss/badge"; -@import "../../../node_modules/bootstrap/scss/alert"; -@import "../../../node_modules/bootstrap/scss/progress"; -@import "../../../node_modules/bootstrap/scss/media"; -@import "../../../node_modules/bootstrap/scss/list-group"; -@import "../../../node_modules/bootstrap/scss/close"; -@import "../../../node_modules/bootstrap/scss/modal"; -@import "../../../node_modules/bootstrap/scss/tooltip"; -@import "../../../node_modules/bootstrap/scss/popover"; -@import "../../../node_modules/bootstrap/scss/utilities"; -@import "../../../node_modules/bootstrap/scss/print"; diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index 802d58779d0..29473da21cc 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -1,7 +1,3 @@ -/* - * Scss to help with bootstrap 3 to 4 migration - */ - $text-color: $gl-text-color; $brand-primary: $blue-500; @@ -18,6 +14,9 @@ $input-border: $border-color; $padding-base-vertical: $gl-vert-padding; $padding-base-horizontal: $gl-padding; +/* + * Scss to help with bootstrap 3 to 4 migration + */ body, .form-control, .search form { diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 9b0d19b0ef0..14f4652e847 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -2,7 +2,8 @@ @import 'framework/variables_overrides'; @import 'framework/mixins'; -@import 'bootstrap'; +@import '../../../node_modules/@gitlab/ui/scss/gitlab_ui'; + @import 'bootstrap_migration'; @import 'framework/layout'; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 2a601afff53..821e6691fe4 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -248,14 +248,24 @@ label { .gl-form-checkbox { align-items: baseline; + margin-right: 1rem; + margin-bottom: 0.25rem; + + .form-check-input { + margin-right: 0; + } + + .form-check-label { + padding-left: $gl-padding-8; + } &.form-check-inline .form-check-input { align-self: flex-start; - margin-right: $gl-padding-8; height: 1.5 * $gl-font-size; } - .help-text { - margin-bottom: 0; + .form-check-input:disabled, + .form-check-input:disabled ~ .form-check-label { + cursor: not-allowed; } } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 6fc742871e7..6e98908eeed 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -124,6 +124,10 @@ float: left; padding-left: $gl-padding-8; } + + .section-header ~ .section.line { + margin-left: $gl-padding; + } } .build-header { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 8cb3fab74e0..3917937f4af 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -904,7 +904,8 @@ margin-right: -5px; } -.deploy-heading { +.deploy-heading, +.merge-train-info { @include media-breakpoint-up(md) { padding: $gl-padding-8 $gl-padding; } diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 0a9c56f5625..3b62121eb0d 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -340,6 +340,11 @@ .deprecated-service { cursor: default; + + a { + font-weight: $gl-font-weight-bold; + color: $white-light; + } } .personal-access-tokens-never-expires-label { diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb index 80ee7c35906..ec8077d18e3 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -128,6 +128,7 @@ class Clusters::ClustersController < Clusters::BaseController :enabled, :name, :environment_scope, + :managed, :base_domain, platform_kubernetes_attributes: [ :api_url, @@ -140,6 +141,7 @@ class Clusters::ClustersController < Clusters::BaseController params.require(:cluster).permit( :enabled, :environment_scope, + :managed, :base_domain, platform_kubernetes_attributes: [ :namespace diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 9cf25915e92..88a0690938a 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -104,6 +104,12 @@ module IssuableCollections # Used by view to highlight active option @sort = options[:sort] + # When a user looks for an exact iid, we do not filter by search but only by iid + if params[:search] =~ /^#(?<iid>\d+)\z/ + options[:iids] = Regexp.last_match[:iid] + params[:search] = nil + end + if @project options[:project_id] = @project.id options[:attempt_project_search_optimizations] = true diff --git a/app/controllers/projects/registry/tags_controller.rb b/app/controllers/projects/registry/tags_controller.rb index 567d750caae..bf1d8d8b5fc 100644 --- a/app/controllers/projects/registry/tags_controller.rb +++ b/app/controllers/projects/registry/tags_controller.rb @@ -3,7 +3,7 @@ module Projects module Registry class TagsController < ::Projects::Registry::ApplicationController - before_action :authorize_update_container_image!, only: [:destroy] + before_action :authorize_destroy_container_image!, only: [:destroy] def index respond_to do |format| diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index 2e5bdbd79c8..5615909c4ec 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -7,8 +7,8 @@ class GitlabSchema < GraphQL::Schema AUTHENTICATED_COMPLEXITY = 250 ADMIN_COMPLEXITY = 300 - DEFAULT_MAX_DEPTH = 10 - AUTHENTICATED_MAX_DEPTH = 15 + DEFAULT_MAX_DEPTH = 15 + AUTHENTICATED_MAX_DEPTH = 20 use BatchLoader::GraphQL use Gitlab::Graphql::Authorize diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index dd5133189dc..f2365499eee 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -4,6 +4,8 @@ module Types class IssueType < BaseObject graphql_name 'Issue' + implements(Types::Notes::NoteableType) + authorize :read_issue expose_permissions Types::PermissionTypes::Issue @@ -49,5 +51,7 @@ module Types field :created_at, Types::TimeType, null: false field :updated_at, Types::TimeType, null: false + + field :task_completion_status, Types::TaskCompletionStatus, null: false end end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 85ac3102442..dac4c24cf10 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -4,6 +4,8 @@ module Types class MergeRequestType < BaseObject graphql_name 'MergeRequest' + implements(Types::Notes::NoteableType) + authorize :read_merge_request expose_permissions Types::PermissionTypes::MergeRequest @@ -53,5 +55,7 @@ module Types field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline field :pipelines, Types::Ci::PipelineType.connection_type, resolver: Resolvers::MergeRequestPipelinesResolver + + field :task_completion_status, Types::TaskCompletionStatus, null: false end end diff --git a/app/graphql/types/notes/diff_position_type.rb b/app/graphql/types/notes/diff_position_type.rb new file mode 100644 index 00000000000..104ccb79bbb --- /dev/null +++ b/app/graphql/types/notes/diff_position_type.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Types + module Notes + class DiffPositionType < BaseObject + graphql_name 'DiffPosition' + + field :head_sha, GraphQL::STRING_TYPE, null: false, + description: "The sha of the head at the time the comment was made" + field :base_sha, GraphQL::STRING_TYPE, null: true, + description: "The merge base of the branch the comment was made on" + field :start_sha, GraphQL::STRING_TYPE, null: false, + description: "The sha of the branch being compared against" + + field :file_path, GraphQL::STRING_TYPE, null: false, + description: "The path of the file that was changed" + field :old_path, GraphQL::STRING_TYPE, null: true, + description: "The path of the file on the start sha." + field :new_path, GraphQL::STRING_TYPE, null: true, + description: "The path of the file on the head sha." + field :position_type, Types::Notes::PositionTypeEnum, null: false + + # Fields for text positions + field :old_line, GraphQL::INT_TYPE, null: true, + description: "The line on start sha that was changed", + resolve: -> (position, _args, _ctx) { position.old_line if position.on_text? } + field :new_line, GraphQL::INT_TYPE, null: true, + description: "The line on head sha that was changed", + resolve: -> (position, _args, _ctx) { position.new_line if position.on_text? } + + # Fields for image positions + field :x, GraphQL::INT_TYPE, null: true, + description: "The X postion on which the comment was made", + resolve: -> (position, _args, _ctx) { position.x if position.on_image? } + field :y, GraphQL::INT_TYPE, null: true, + description: "The Y position on which the comment was made", + resolve: -> (position, _args, _ctx) { position.y if position.on_image? } + field :width, GraphQL::INT_TYPE, null: true, + description: "The total width of the image", + resolve: -> (position, _args, _ctx) { position.width if position.on_image? } + field :height, GraphQL::INT_TYPE, null: true, + description: "The total height of the image", + resolve: -> (position, _args, _ctx) { position.height if position.on_image? } + end + end +end diff --git a/app/graphql/types/notes/discussion_type.rb b/app/graphql/types/notes/discussion_type.rb new file mode 100644 index 00000000000..c4691942f2d --- /dev/null +++ b/app/graphql/types/notes/discussion_type.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module Notes + class DiscussionType < BaseObject + graphql_name 'Discussion' + + authorize :read_note + + field :id, GraphQL::ID_TYPE, null: false + field :created_at, Types::TimeType, null: false + field :notes, Types::Notes::NoteType.connection_type, null: false, description: "All notes in the discussion" + end + end +end diff --git a/app/graphql/types/notes/note_type.rb b/app/graphql/types/notes/note_type.rb new file mode 100644 index 00000000000..85c55d16ac2 --- /dev/null +++ b/app/graphql/types/notes/note_type.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Types + module Notes + class NoteType < BaseObject + graphql_name 'Note' + + authorize :read_note + + expose_permissions Types::PermissionTypes::Note + + field :id, GraphQL::ID_TYPE, null: false + + field :project, Types::ProjectType, + null: true, + description: "The project this note is associated to", + resolve: -> (note, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, note.project_id).find } + + field :author, Types::UserType, + null: false, + description: "The user who wrote this note", + resolve: -> (note, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, note.author_id).find } + + field :resolved_by, Types::UserType, + null: true, + description: "The user that resolved the discussion", + resolve: -> (note, _args, _context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, note.resolved_by_id).find } + + field :system, GraphQL::BOOLEAN_TYPE, + null: false, + description: "Whether or not this note was created by the system or by a user" + + field :body, GraphQL::STRING_TYPE, + null: false, + method: :note, + description: "The content note itself" + + field :created_at, Types::TimeType, null: false + field :updated_at, Types::TimeType, null: false + field :discussion, Types::Notes::DiscussionType, null: true, description: "The discussion this note is a part of" + field :resolvable, GraphQL::BOOLEAN_TYPE, null: false, method: :resolvable? + field :resolved_at, Types::TimeType, null: true, description: "The time the discussion was resolved" + field :position, Types::Notes::DiffPositionType, null: true, description: "The position of this note on a diff" + end + end +end diff --git a/app/graphql/types/notes/noteable_type.rb b/app/graphql/types/notes/noteable_type.rb new file mode 100644 index 00000000000..9f126d67b0d --- /dev/null +++ b/app/graphql/types/notes/noteable_type.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Types + module Notes + module NoteableType + include Types::BaseInterface + + field :notes, Types::Notes::NoteType.connection_type, null: false, description: "All notes on this noteable" + field :discussions, Types::Notes::DiscussionType.connection_type, null: false, description: "All discussions on this noteable" + + definition_methods do + def resolve_type(object, context) + case object + when Issue + Types::IssueType + when MergeRequest + Types::MergeRequestType + else + raise "Unknown GraphQL type for #{object}" + end + end + end + end + end +end diff --git a/app/graphql/types/notes/position_type_enum.rb b/app/graphql/types/notes/position_type_enum.rb new file mode 100644 index 00000000000..abdb2cfc804 --- /dev/null +++ b/app/graphql/types/notes/position_type_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + module Notes + class PositionTypeEnum < BaseEnum + graphql_name 'DiffPositionType' + description 'Type of file the position refers to' + + value 'text' + value 'image' + end + end +end diff --git a/app/graphql/types/permission_types/note.rb b/app/graphql/types/permission_types/note.rb new file mode 100644 index 00000000000..a585d3daaa8 --- /dev/null +++ b/app/graphql/types/permission_types/note.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + module PermissionTypes + class Note < BasePermissionType + graphql_name 'NotePermissions' + + abilities :read_note, :create_note, :admin_note, :resolve_note, :award_emoji + end + end +end diff --git a/app/graphql/types/project_statistics_type.rb b/app/graphql/types/project_statistics_type.rb index 62537361918..4000c6db280 100644 --- a/app/graphql/types/project_statistics_type.rb +++ b/app/graphql/types/project_statistics_type.rb @@ -4,6 +4,8 @@ module Types class ProjectStatisticsType < BaseObject graphql_name 'ProjectStatistics' + authorize :read_statistics + field :commit_count, GraphQL::INT_TYPE, null: false field :storage_size, GraphQL::INT_TYPE, null: false diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 2236ffa394d..81914b70c7f 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -70,7 +70,7 @@ module Types field :group, Types::GroupType, null: true field :statistics, Types::ProjectStatisticsType, - null: false, + null: true, resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchProjectStatisticsLoader.new(obj.id).find } field :repository, Types::RepositoryType, null: false diff --git a/app/graphql/types/task_completion_status.rb b/app/graphql/types/task_completion_status.rb new file mode 100644 index 00000000000..c289802509d --- /dev/null +++ b/app/graphql/types/task_completion_status.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + class TaskCompletionStatus < BaseObject + graphql_name 'TaskCompletionStatus' + description 'Completion status of tasks' + + field :count, GraphQL::INT_TYPE, null: false + field :completed_count, GraphQL::INT_TYPE, null: false + end +end diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index d4b50b7ecfb..01ccf163b45 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -39,7 +39,7 @@ module ServicesHelper end def disable_fields_service?(service) - !current_controller?("admin/services") && service.deprecated? + service.is_a?(KubernetesService) || (!current_controller?("admin/services") && service.deprecated?) end extend self diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index ccc877fb924..8c044c86c47 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -193,21 +193,40 @@ module Clusters platform_kubernetes.kubeclient if kubernetes? end + ## + # This is subtly different to #find_or_initialize_kubernetes_namespace_for_project + # below because it will ignore any namespaces that have not got a service account + # token. This provides a guarantee that any namespace selected here can be used + # for cluster operations - a namespace needs to have a service account configured + # before it it can be used. + # + # This is used for selecting a namespace to use when querying a cluster, or + # generating variables to pass to CI. def kubernetes_namespace_for(project) - find_or_initialize_kubernetes_namespace_for_project(project).namespace + find_or_initialize_kubernetes_namespace_for_project( + project, scope: kubernetes_namespaces.has_service_account_token + ).namespace end - def find_or_initialize_kubernetes_namespace_for_project(project) + ## + # This is subtly different to #kubernetes_namespace_for because it will include + # namespaces that have yet to receive a service account token. This allows + # the namespace configuration process to be repeatable - if a namespace has + # already been created without a token we don't need to create another + # record entirely, just set the token on the pre-existing namespace. + # + # This is used for configuring cluster namespaces. + def find_or_initialize_kubernetes_namespace_for_project(project, scope: kubernetes_namespaces) attributes = { project: project } attributes[:cluster_project] = cluster_project if project_type? - kubernetes_namespaces.find_or_initialize_by(attributes).tap do |namespace| + scope.find_or_initialize_by(attributes).tap do |namespace| namespace.set_defaults end end def allow_user_defined_namespace? - project_type? + project_type? || !managed? end def kube_ingress_domain diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 8e06156c73d..272861cacf0 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -80,9 +80,18 @@ module Clusters .append(key: 'KUBE_CA_PEM_FILE', value: ca_pem, file: true) end - if kubernetes_namespace = cluster.kubernetes_namespaces.has_service_account_token.find_by(project: project) + if !cluster.managed? + project_namespace = namespace.presence || "#{project.path}-#{project.id}".downcase + + variables + .append(key: 'KUBE_URL', value: api_url) + .append(key: 'KUBE_TOKEN', value: token, public: false, masked: true) + .append(key: 'KUBE_NAMESPACE', value: project_namespace) + .append(key: 'KUBECONFIG', value: kubeconfig(project_namespace), public: false, file: true) + + elsif kubernetes_namespace = cluster.kubernetes_namespaces.has_service_account_token.find_by(project: project) variables.concat(kubernetes_namespace.predefined_variables) - elsif cluster.project_type? || !cluster.managed? + elsif cluster.project_type? # As of 11.11 a user can create a cluster that they manage themselves, # which replicates the existing project-level cluster behaviour. # Once we have marked all project-level clusters that make use of this diff --git a/app/models/concerns/diff_positionable_note.rb b/app/models/concerns/diff_positionable_note.rb index b61bf29e6ad..2d09eff0111 100644 --- a/app/models/concerns/diff_positionable_note.rb +++ b/app/models/concerns/diff_positionable_note.rb @@ -3,6 +3,7 @@ module DiffPositionableNote extend ActiveSupport::Concern included do + delegate :on_text?, :on_image?, to: :position, allow_nil: true before_validation :set_original_position, on: :create before_validation :update_position, on: :create, if: :on_text? @@ -28,14 +29,6 @@ module DiffPositionableNote end end - def on_text? - position&.position_type == "text" - end - - def on_image? - position&.position_type == "image" - end - def supported? for_commit? || self.noteable.has_complete_diff_refs? end diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 32529ebf71d..ae13cdfd85f 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -4,6 +4,7 @@ # # A discussion of this type can be resolvable. class Discussion + include GlobalID::Identification include ResolvableDiscussion attr_reader :notes, :context_noteable @@ -11,14 +12,19 @@ class Discussion delegate :created_at, :project, :author, - :noteable, :commit_id, :for_commit?, :for_merge_request?, + :to_ability_name, + :editable?, to: :first_note + def declarative_policy_delegate + first_note + end + def project_id project&.id end diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 5245dbc8d15..79a376ff0fd 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -5,7 +5,7 @@ class LfsObject < ApplicationRecord include ObjectStorage::BackgroundMove has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - has_many :projects, through: :lfs_objects_projects + has_many :projects, -> { distinct }, through: :lfs_objects_projects scope :with_files_stored_locally, -> { where(file_store: LfsObjectUploader::Store::LOCAL) } diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb index f9afb18c1d7..e45c56b6394 100644 --- a/app/models/lfs_objects_project.rb +++ b/app/models/lfs_objects_project.rb @@ -5,11 +5,17 @@ class LfsObjectsProject < ApplicationRecord belongs_to :lfs_object validates :lfs_object_id, presence: true - validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" } + validates :lfs_object_id, uniqueness: { scope: [:project_id, :repository_type], message: "already exists in repository" } validates :project_id, presence: true after_commit :update_project_statistics, on: [:create, :destroy] + enum repository_type: { + project: 0, + wiki: 1, + design: 2 ## EE-specific + } + private def update_project_statistics diff --git a/app/models/note.rb b/app/models/note.rb index 081d6f91230..15271c68a9e 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -342,7 +342,7 @@ class Note < ApplicationRecord end def to_ability_name - for_snippet? ? noteable.class.name.underscore : noteable_type.underscore + for_snippet? ? noteable.class.name.underscore : noteable_type.demodulize.underscore end def can_be_discussion_note? diff --git a/app/models/project.rb b/app/models/project.rb index 9d17d68eee2..351d08eaf63 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -72,7 +72,6 @@ class Project < ApplicationRecord delegate :no_import?, to: :import_state, allow_nil: true default_value_for :archived, false - default_value_for(:visibility_level) { Gitlab::CurrentSettings.default_project_visibility } default_value_for :resolve_outdated_diff_discussions, false default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for(:repository_storage) { Gitlab::CurrentSettings.pick_repository_storage } @@ -223,7 +222,7 @@ class Project < ApplicationRecord has_many :starrers, through: :users_star_projects, source: :user has_many :releases has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - has_many :lfs_objects, through: :lfs_objects_projects + has_many :lfs_objects, -> { distinct }, through: :lfs_objects_projects has_many :lfs_file_locks has_many :project_group_links has_many :invited_groups, through: :project_group_links, source: :group @@ -613,6 +612,23 @@ class Project < ApplicationRecord end end + def initialize(attributes = {}) + # We can't use default_value_for because the database has a default + # value of 0 for visibility_level. If someone attempts to create a + # private project, default_value_for will assume that the + # visibility_level hasn't changed and will use the application + # setting default, which could be internal or public. For projects + # inside a private group, those levels are invalid. + # + # To fix the problem, we assign the actual default in the application if + # no explicit visibility has been initialized. + unless visibility_attribute_present?(attributes) + attributes[:visibility_level] = Gitlab::CurrentSettings.default_project_visibility + end + + super + end + def all_pipelines if builds_enabled? super diff --git a/app/models/project_services/data_fields.rb b/app/models/project_services/data_fields.rb new file mode 100644 index 00000000000..438d85098c8 --- /dev/null +++ b/app/models/project_services/data_fields.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module DataFields + extend ActiveSupport::Concern + + included do + has_one :issue_tracker_data + has_one :jira_tracker_data + end +end diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb deleted file mode 100644 index 80aa2101509..00000000000 --- a/app/models/project_services/deployment_service.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -# Base class for deployment services -# -# These services integrate with a deployment solution like Kubernetes/OpenShift, -# Mesosphere, etc, to provide additional features to environments. -class DeploymentService < Service - default_value_for :category, 'deployment' - - def self.supported_events - %w() - end - - def predefined_variables(project:) - [] - end - - # Environments may have a number of terminals. Should return an array of - # hashes describing them, e.g.: - # - # [{ - # :selectors => {"a" => "b", "foo" => "bar"}, - # :url => "wss://external.example.com/exec", - # :headers => {"Authorization" => "Token xxx"}, - # :subprotocols => ["foo"], - # :ca_pem => "----BEGIN CERTIFICATE...", # optional - # :created_at => Time.now.utc - # }] - # - # Selectors should be a set of values that uniquely identify a particular - # terminal - def terminals(environment) - raise NotImplementedError - end - - def can_test? - false - end -end diff --git a/app/models/project_services/issue_tracker_data.rb b/app/models/project_services/issue_tracker_data.rb new file mode 100644 index 00000000000..2c1d28ed421 --- /dev/null +++ b/app/models/project_services/issue_tracker_data.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class IssueTrackerData < ApplicationRecord + belongs_to :service + + delegate :activated?, to: :service, allow_nil: true + + validates :service, presence: true + validates :project_url, presence: true, public_url: { enforce_sanitization: true }, if: :activated? + validates :issues_url, presence: true, public_url: { enforce_sanitization: true }, if: :activated? + validates :new_issue_url, public_url: { enforce_sanitization: true }, if: :activated? + + def self.encryption_options + { + key: Settings.attr_encrypted_db_key_base_32, + encode: true, + mode: :per_attribute_iv, + algorithm: 'aes-256-gcm' + } + end + + attr_encrypted :project_url, encryption_options + attr_encrypted :issues_url, encryption_options + attr_encrypted :new_issue_url, encryption_options +end diff --git a/app/models/project_services/jira_tracker_data.rb b/app/models/project_services/jira_tracker_data.rb new file mode 100644 index 00000000000..4f528e3d81b --- /dev/null +++ b/app/models/project_services/jira_tracker_data.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class JiraTrackerData < ApplicationRecord + belongs_to :service + + delegate :activated?, to: :service, allow_nil: true + + validates :service, presence: true + validates :url, public_url: { enforce_sanitization: true }, presence: true, if: :activated? + validates :api_url, public_url: { enforce_sanitization: true }, allow_blank: true + validates :username, presence: true, if: :activated? + validates :password, presence: true, if: :activated? + validates :jira_issue_transition_id, + format: { with: Gitlab::Regex.jira_transition_id_regex, message: s_("JiraService|transition ids can have only numbers which can be split with , or ;") }, + allow_blank: true + + def self.encryption_options + { + key: Settings.attr_encrypted_db_key_base_32, + encode: true, + mode: :per_attribute_iv, + algorithm: 'aes-256-gcm' + } + end + + attr_encrypted :url, encryption_options + attr_encrypted :api_url, encryption_options + attr_encrypted :username, encryption_options + attr_encrypted :password, encryption_options +end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index aa6b4aa1d5e..edf7e886e77 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -5,10 +5,12 @@ # We'll move this class to Clusters::Platforms::Kubernetes, which contains exactly the same logic. # After we've migrated data, we'll remove KubernetesService. This would happen in a few months. # If you're modyfiyng this class, please note that you should update the same change in Clusters::Platforms::Kubernetes. -class KubernetesService < DeploymentService +class KubernetesService < Service include Gitlab::Kubernetes include ReactiveCaching + default_value_for :category, 'deployment' + self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] } # Namespace defaults to the project path, but can be overridden in case that @@ -32,7 +34,10 @@ class KubernetesService < DeploymentService before_validation :enforce_namespace_to_lower_case - validate :deprecation_validation, unless: :template? + attr_accessor :skip_deprecation_validation + + validate :deprecation_validation, unless: :skip_deprecation_validation + validates :namespace, allow_blank: true, length: 1..63, @@ -44,6 +49,14 @@ class KubernetesService < DeploymentService after_save :clear_reactive_cache! + def self.supported_events + %w() + end + + def can_test? + false + end + def initialize_properties self.properties = {} if properties.nil? end @@ -56,11 +69,6 @@ class KubernetesService < DeploymentService 'Kubernetes / OpenShift integration' end - def help - 'To enable terminal access to Kubernetes environments, label your ' \ - 'deployments with `app=$CI_ENVIRONMENT_SLUG`' - end - def self.to_param 'kubernetes' end @@ -153,14 +161,25 @@ class KubernetesService < DeploymentService end def deprecated? - !active + true + end + + def editable? + false end def deprecation_message - content = _("Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page") % { - deprecated_message_content: deprecated_message_content, - url: Gitlab::Routing.url_helpers.project_clusters_path(project) - } + content = if project + _("Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page") % { + deprecated_message_content: deprecated_message_content, + url: Gitlab::Routing.url_helpers.project_clusters_path(project) + } + else + _("The instance-level Kubernetes service integration is deprecated. Your data has been migrated to an <a href=\"%{url}\"/>instance-level cluster</a>.") % { + url: Gitlab::Routing.url_helpers.admin_clusters_path + } + end + content.html_safe end @@ -243,10 +262,6 @@ class KubernetesService < DeploymentService end def deprecated_message_content - if active? - _("Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure") - else - _("Fields on this page are now uneditable, you can configure") - end + _("Fields on this page are now uneditable, you can configure") end end diff --git a/app/models/project_services/mock_deployment_service.rb b/app/models/project_services/mock_deployment_service.rb index 7ab1687f8ba..1103cb11e73 100644 --- a/app/models/project_services/mock_deployment_service.rb +++ b/app/models/project_services/mock_deployment_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true -class MockDeploymentService < DeploymentService +class MockDeploymentService < Service + default_value_for :category, 'deployment' + def title 'Mock deployment' end @@ -17,4 +19,16 @@ class MockDeploymentService < DeploymentService def terminals(environment) [] end + + def self.supported_events + %w() + end + + def predefined_variables(project:) + [] + end + + def can_test? + false + end end diff --git a/app/models/service.rb b/app/models/service.rb index 9896aa12e90..40033003f3b 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -6,6 +6,7 @@ class Service < ApplicationRecord include Sortable include Importable include ProjectServicesLoggable + include DataFields serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize @@ -119,7 +120,7 @@ class Service < ApplicationRecord end def self.event_names - self.supported_events.map { |event| "#{event}_events" } + self.supported_events.map { |event| ServicesHelper.service_event_field_name(event) } end def event_field(event) @@ -151,7 +152,7 @@ class Service < ApplicationRecord end def self.supported_events - %w(push tag_push issue confidential_issue merge_request wiki_page) + %w(commit push tag_push issue confidential_issue merge_request wiki_page) end def execute(data) diff --git a/app/models/user_callout_enums.rb b/app/models/user_callout_enums.rb index 54b0d2ce831..7b68e5076c7 100644 --- a/app/models/user_callout_enums.rb +++ b/app/models/user_callout_enums.rb @@ -6,12 +6,15 @@ module UserCalloutEnums # # This method is separate from the `UserCallout` model so that it can be # extended by EE. + # + # If you are going to add new items to this hash, check that you're not going + # to conflict with EE-only values: https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/app/models/ee/user_callout_enums.rb def self.feature_names { gke_cluster_integration: 1, gcp_signup_offer: 2, cluster_security_warning: 3, - suggest_popover_dismissed: 4 + suggest_popover_dismissed: 9 } end end diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index 5dd2279ef99..82bf9bf8bf6 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -7,6 +7,10 @@ class BasePolicy < DeclarativePolicy::Base with_options scope: :user, score: 0 condition(:admin) { @user&.admin? } + desc "User is blocked" + with_options scope: :user, score: 0 + condition(:blocked) { @user&.blocked? } + desc "User has access to all private groups & projects" with_options scope: :user, score: 0 condition(:full_private_access) { @user&.full_private_access? } diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index e85397422e6..134de1c9ace 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true class GlobalPolicy < BasePolicy - desc "User is blocked" - with_options scope: :user, score: 0 - condition(:blocked) { @user&.blocked? } - desc "User is an internal user" with_options scope: :user, score: 0 condition(:internal) { @user&.internal? } diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 728a3040227..b3e29e775fc 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -258,6 +258,7 @@ class ProjectPolicy < BasePolicy enable :resolve_note enable :create_container_image enable :update_container_image + enable :destroy_container_image enable :create_environment enable :create_deployment enable :create_release @@ -446,6 +447,10 @@ class ProjectPolicy < BasePolicy prevent :owner_access end + rule { blocked }.policy do + prevent :create_pipeline + end + private def team_member? diff --git a/app/policies/project_statistics_policy.rb b/app/policies/project_statistics_policy.rb new file mode 100644 index 00000000000..c0592f1ea13 --- /dev/null +++ b/app/policies/project_statistics_policy.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ProjectStatisticsPolicy < BasePolicy + delegate { @subject.project } +end diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb index ec2698ecbe3..9ef93b2387f 100644 --- a/app/serializers/pipeline_entity.rb +++ b/app/serializers/pipeline_entity.rb @@ -4,7 +4,6 @@ class PipelineEntity < Grape::Entity include RequestAwareEntity expose :id - expose :iid expose :user, using: UserEntity expose :active?, as: :active diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index fd5442a6c28..f2cd51ef4d0 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -3,7 +3,7 @@ module Files class CreateService < Files::BaseService def create_commit! - transformer = Lfs::FileTransformer.new(project, @branch_name) + transformer = Lfs::FileTransformer.new(project, repository, @branch_name) result = transformer.new_file(@file_path, @file_content) diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index c1bc26c330a..d8c4e5bc5e8 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -5,7 +5,7 @@ module Files UPDATE_FILE_ACTIONS = %w(update move delete chmod).freeze def create_commit! - transformer = Lfs::FileTransformer.new(project, @branch_name) + transformer = Lfs::FileTransformer.new(project, repository, @branch_name) actions = actions_after_lfs_transformation(transformer, params[:actions]) actions = transform_move_actions(actions) diff --git a/app/services/lfs/file_transformer.rb b/app/services/lfs/file_transformer.rb index 5239fe1b6e3..d1746399908 100644 --- a/app/services/lfs/file_transformer.rb +++ b/app/services/lfs/file_transformer.rb @@ -8,17 +8,17 @@ module Lfs # pointer returned. If the file isn't in LFS the untransformed content # is returned to save in the commit. # - # transformer = Lfs::FileTransformer.new(project, @branch_name) + # transformer = Lfs::FileTransformer.new(project, repository, @branch_name) # content_or_lfs_pointer = transformer.new_file(file_path, content).content # create_transformed_commit(content_or_lfs_pointer) # class FileTransformer - attr_reader :project, :branch_name + attr_reader :project, :repository, :repository_type, :branch_name - delegate :repository, to: :project - - def initialize(project, branch_name) + def initialize(project, repository, branch_name) @project = project + @repository = repository + @repository_type = repository.repo_type.name @branch_name = branch_name end @@ -64,7 +64,11 @@ module Lfs # rubocop: enable CodeReuse/ActiveRecord def link_lfs_object!(lfs_object) - project.lfs_objects << lfs_object + LfsObjectsProject.safe_find_or_create_by!( + project: project, + lfs_object: lfs_object, + repository_type: repository_type + ) end def parse_file_content(file_content, encoding: nil) diff --git a/app/views/admin/application_settings/ci_cd.html.haml b/app/views/admin/application_settings/ci_cd.html.haml index db24c9982f7..a2aa1687f80 100644 --- a/app/views/admin/application_settings/ci_cd.html.haml +++ b/app/views/admin/application_settings/ci_cd.html.haml @@ -13,6 +13,8 @@ .settings-content = render 'ci_cd' += render_if_exists 'admin/application_settings/required_instance_ci_setting', expanded: expanded_by_default? + - if Gitlab.config.registry.enabled %section.settings.as-registry.no-animate#js-registry-settings{ class: ('expanded' if expanded_by_default?) } .settings-header diff --git a/app/views/admin/services/_deprecated_message.html.haml b/app/views/admin/services/_deprecated_message.html.haml new file mode 100644 index 00000000000..fea9506a4bb --- /dev/null +++ b/app/views/admin/services/_deprecated_message.html.haml @@ -0,0 +1,3 @@ +.flash-container.flash-container-page + .flash-alert.deprecated-service + %span= @service.deprecation_message diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml index 1798b44bbb7..97373a3c350 100644 --- a/app/views/admin/services/_form.html.haml +++ b/app/views/admin/services/_form.html.haml @@ -6,5 +6,6 @@ = form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form' } do |form| = render 'shared/service_settings', form: form, subject: @service - .footer-block.row-content-block - = form.submit 'Save', class: 'btn btn-success' + - unless @service.is_a?(KubernetesService) + .footer-block.row-content-block + = form.submit 'Save', class: 'btn btn-success' diff --git a/app/views/admin/services/edit.html.haml b/app/views/admin/services/edit.html.haml index 512176649e6..79f5ab0d77d 100644 --- a/app/views/admin/services/edit.html.haml +++ b/app/views/admin/services/edit.html.haml @@ -1,4 +1,7 @@ - add_to_breadcrumbs "Service Templates", admin_application_settings_services_path - breadcrumb_title @service.title - page_title @service.title, "Service Templates" + += render 'deprecated_message' if @service.deprecation_message + = render 'form' diff --git a/app/views/clusters/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml index 70e2eaeaf3b..4d3e3359ea0 100644 --- a/app/views/clusters/clusters/gcp/_form.html.haml +++ b/app/views/clusters/clusters/gcp/_form.html.haml @@ -65,15 +65,6 @@ %p.form-text.text-muted = s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end } - .form-group - = provider_gcp_field.check_box :legacy_abac, { label: s_('ClusterIntegration|RBAC-enabled cluster'), - label_class: 'label-bold' }, false, true - .form-text.text-muted - = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') - = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') - = link_to _('More information'), help_page_path('user/project/clusters/index.md', - anchor: 'role-based-access-control-rbac-core-only'), target: '_blank' - .form-group = field.check_box :managed, { label: s_('ClusterIntegration|GitLab-managed cluster'), label_class: 'label-bold' } diff --git a/app/views/clusters/platforms/kubernetes/_form.html.haml b/app/views/clusters/platforms/kubernetes/_form.html.haml index c1727cf9079..f2e44462226 100644 --- a/app/views/clusters/platforms/kubernetes/_form.html.haml +++ b/app/views/clusters/platforms/kubernetes/_form.html.haml @@ -48,7 +48,7 @@ = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') .form-group - = field.check_box :managed, { disabled: true, label: s_('ClusterIntegration|GitLab-managed cluster'), + = field.check_box :managed, { label: s_('ClusterIntegration|GitLab-managed cluster'), label_class: 'label-bold' } .form-text.text-muted = s_('ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster.') diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index 0a14830c666..0da1f1ba7f5 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -17,6 +17,7 @@ %br %span.descr.text-muted= share_with_group_lock_help_text(@group) + = render_if_exists 'groups/settings/ip_restriction', f: f, group: @group = render 'groups/settings/lfs', f: f = render 'groups/settings/project_creation_level', f: f, group: @group = render 'groups/settings/two_factor_auth', f: f diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index c357207054b..7535aee83a3 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -78,3 +78,4 @@ = render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id') = render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id') = render_if_exists 'layouts/snowplow' + = render_if_exists 'layouts/pendo' if Feature.enabled?(:pendo_tracking) && !Rails.env.test? diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index bdf7b933ab8..f4560404c03 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -53,10 +53,9 @@ %span.badge.badge-info= _('manual') - if pipeline_link - %td.pipeline-link - = link_to pipeline_path(pipeline), class: 'has-tooltip', title: _('Pipeline ID (IID)') do + %td + = link_to pipeline_path(pipeline) do %span.pipeline-id ##{pipeline.id} - %span.pipeline-iid (##{pipeline.iid}) %span by - if pipeline.user = user_avatar(user: pipeline.user, size: 20) diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 77ea2c04b28..a766dd51463 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -81,7 +81,7 @@ = link_to project_pipeline_path(@project, last_pipeline.id), class: "ci-status-icon-#{last_pipeline.status}" do = ci_icon_for_status(last_pipeline.status) #{ _('Pipeline') } - = link_to "##{last_pipeline.id} (##{last_pipeline.iid})", project_pipeline_path(@project, last_pipeline.id), class: "has-tooltip", title: _('Pipeline ID (IID)') + = link_to "##{last_pipeline.id}", project_pipeline_path(@project, last_pipeline.id) = ci_label_for_status(last_pipeline.status) - if last_pipeline.stages_count.nonzero? #{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), last_pipeline.stages_count) } diff --git a/app/views/projects/merge_requests/_mr_box.html.haml b/app/views/projects/merge_requests/_mr_box.html.haml index 7f2c9dcacfd..4f09f47d795 100644 --- a/app/views/projects/merge_requests/_mr_box.html.haml +++ b/app/views/projects/merge_requests/_mr_box.html.haml @@ -1,10 +1,10 @@ .detail-page-description - %h2.title + %h2.title.qa-title = markdown_field(@merge_request, :title) %div - if @merge_request.description.present? - .description{ class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : '' } + .description.qa-description{ class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : '' } .md = markdown_field(@merge_request, :description) %textarea.hidden.js-task-list-field diff --git a/app/views/shared/projects/_search_form.html.haml b/app/views/shared/projects/_search_form.html.haml index 7c7c0a363ac..4365e3f6877 100644 --- a/app/views/shared/projects/_search_form.html.haml +++ b/app/views/shared/projects/_search_form.html.haml @@ -1,7 +1,7 @@ - form_field_classes = local_assigns[:admin_view] || !Feature.enabled?(:project_list_filter_bar) ? 'input-short js-projects-list-filter' : '' - placeholder = local_assigns[:search_form_placeholder] ? search_form_placeholder : 'Filter by name...' -= form_tag filter_projects_path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| += form_tag filter_projects_path, method: :get, class: 'project-filter-form qa-project-filter-form', id: 'project-filter-form' do |f| = search_field_tag :name, params[:name], placeholder: placeholder, class: "project-filter-form-field form-control #{form_field_classes}", |