Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-18 18:10:26 +00:00
parent 80d252c8e2
commit 96c78a921f
62 changed files with 1093 additions and 599 deletions

View file

@ -174,7 +174,7 @@ review-performance:
- .default-retry
- .review:rules:review-performance
image:
name: sitespeedio/sitespeed.io:6.3.1
name: sitespeedio/sitespeed.io
entrypoint: [""]
stage: qa
# This is needed so that manual jobs with needs don't block the pipeline.

View file

@ -1 +1 @@
b66774f8cfbdde61d45589d7ac4cc030a086cfc6
8d14377ab5b3914033f85ec3572e0ba65749a6e0

View file

@ -0,0 +1,14 @@
<script>
import InstanceCounts from './instance_counts.vue';
export default {
name: 'InstanceStatisticsApp',
components: {
InstanceCounts,
},
};
</script>
<template>
<instance-counts />
</template>

View file

@ -0,0 +1,64 @@
<script>
import * as Sentry from '@sentry/browser';
import { s__ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
import MetricCard from '~/analytics/shared/components/metric_card.vue';
import instanceStatisticsCountQuery from '../graphql/queries/instance_statistics_count.query.graphql';
const defaultPrecision = 0;
export default {
name: 'InstanceCounts',
components: {
MetricCard,
},
data() {
return {
counts: [],
};
},
apollo: {
counts: {
query: instanceStatisticsCountQuery,
update(data) {
return Object.entries(data).map(([key, obj]) => {
const label = this.$options.i18n.labels[key];
const formatter = getFormatter(SUPPORTED_FORMATS.number);
const value = obj.nodes?.length ? formatter(obj.nodes[0].count, defaultPrecision) : null;
return {
key,
value,
label,
};
});
},
error(error) {
createFlash(this.$options.i18n.loadCountsError);
Sentry.captureException(error);
},
},
},
i18n: {
labels: {
users: s__('InstanceStatistics|Users'),
projects: s__('InstanceStatistics|Projects'),
groups: s__('InstanceStatistics|Groups'),
issues: s__('InstanceStatistics|Issues'),
mergeRequests: s__('InstanceStatistics|Merge Requests'),
pipelines: s__('InstanceStatistics|Pipelines'),
},
loadCountsError: s__('Could not load instance counts. Please refresh the page to try again.'),
},
};
</script>
<template>
<metric-card
:title="__('Instance Statistics')"
:metrics="counts"
:is-loading="$apollo.queries.counts.loading"
class="gl-mt-4"
/>
</template>

View file

@ -0,0 +1,32 @@
query getInstanceCounts {
projects: instanceStatisticsMeasurements(identifier: PROJECTS, first: 1) {
nodes {
count
}
}
groups: instanceStatisticsMeasurements(identifier: GROUPS, first: 1) {
nodes {
count
}
}
users: instanceStatisticsMeasurements(identifier: USERS, first: 1) {
nodes {
count
}
}
issues: instanceStatisticsMeasurements(identifier: ISSUES, first: 1) {
nodes {
count
}
}
mergeRequests: instanceStatisticsMeasurements(identifier: MERGE_REQUESTS, first: 1) {
nodes {
count
}
}
pipelines: instanceStatisticsMeasurements(identifier: PIPELINES, first: 1) {
nodes {
count
}
}
}

View file

@ -0,0 +1,24 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import InstanceStatisticsApp from './components/app.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default () => {
const el = document.getElementById('js-instance-statistics-app');
if (!el) return false;
return new Vue({
el,
apolloProvider,
render(h) {
return h(InstanceStatisticsApp);
},
});
};

View file

@ -0,0 +1,80 @@
<script>
import {
GlCard,
GlDeprecatedSkeletonLoading as GlSkeletonLoading,
GlLink,
GlIcon,
GlTooltipDirective,
} from '@gitlab/ui';
export default {
name: 'MetricCard',
components: {
GlCard,
GlSkeletonLoading,
GlLink,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
title: {
type: String,
required: true,
},
metrics: {
type: Array,
required: true,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
valueText(metric) {
const { value = null, unit = null } = metric;
if (!value || value === '-') return '-';
return unit && value ? `${value} ${unit}` : value;
},
},
};
</script>
<template>
<gl-card>
<template #header>
<strong ref="title">{{ title }}</strong>
</template>
<template #default>
<gl-skeleton-loading v-if="isLoading" class="gl-h-auto gl-py-3" />
<div v-else ref="metricsWrapper" class="gl-display-flex">
<div
v-for="metric in metrics"
:key="metric.key"
ref="metricItem"
class="js-metric-card-item gl-flex-grow-1 gl-text-center"
>
<gl-link v-if="metric.link" :href="metric.link">
<h3 class="gl-my-2 gl-text-blue-700">{{ valueText(metric) }}</h3>
</gl-link>
<h3 v-else class="gl-my-2">{{ valueText(metric) }}</h3>
<p class="text-secondary gl-font-sm gl-mb-2">
{{ metric.label }}
<span v-if="metric.tooltipText">
&nbsp;
<gl-icon
v-gl-tooltip="{ title: metric.tooltipText }"
:size="14"
class="gl-vertical-align-middle"
name="question"
data-testid="tooltip"
/>
</span>
</p>
</div>
</div>
</template>
</gl-card>
</template>

View file

@ -1,19 +1,6 @@
<script>
import CommitItem from './commit_item.vue';
/**
* CommitWidget
*
* -----------------------------------------------------------------
* WARNING: Please keep changes up-to-date with the following files:
* - `views/projects/merge_requests/diffs/_commit_widget.html.haml`
* -----------------------------------------------------------------
*
* This Component was cloned from a HAML view. For the time being,
* they coexist, but there is an issue to remove the duplication.
* https://gitlab.com/gitlab-org/gitlab-foss/issues/51613
*
*/
export default {
components: {
CommitItem,

View file

@ -49,7 +49,7 @@ export default {
variant="info"
category="secondary"
type="button"
class="js-enable-review-app-button"
class="gl-w-full js-enable-review-app-button"
>
{{ s__('Environments|Enable review app') }}
</gl-button>

View file

@ -1,5 +1,5 @@
<script>
import { GlDeprecatedButton } from '@gitlab/ui';
import { GlBadge, GlButton, GlTab, GlTabs } from '@gitlab/ui';
import { deprecatedCreateFlash as Flash } from '~/flash';
import { s__ } from '~/locale';
import emptyState from './empty_state.vue';
@ -16,7 +16,10 @@ export default {
ConfirmRollbackModal,
emptyState,
EnableReviewAppButton,
GlDeprecatedButton,
GlBadge,
GlButton,
GlTab,
GlTabs,
StopEnvironmentModal,
DeleteEnvironmentModal,
},
@ -124,43 +127,87 @@ export default {
};
</script>
<template>
<div>
<div class="environments-section">
<stop-environment-modal :environment="environmentInStopModal" />
<delete-environment-modal :environment="environmentInDeleteModal" />
<confirm-rollback-modal :environment="environmentInRollbackModal" />
<div class="top-area">
<tabs :tabs="tabs" scope="environments" @onChangeTab="onChangeTab" />
<div class="nav-controls">
<enable-review-app-button v-if="state.reviewAppDetails.can_setup_review_app" class="mr-2" />
<gl-deprecated-button
<div class="gl-w-full">
<div
class="
gl-display-flex
gl-flex-direction-column
gl-mt-3
gl-display-md-none!"
>
<enable-review-app-button
v-if="state.reviewAppDetails.can_setup_review_app"
class="gl-mb-3 gl-flex-fill-1"
/>
<gl-button
v-if="canCreateEnvironment && !isLoading"
:href="newEnvironmentPath"
category="primary"
variant="success"
>
{{ s__('Environments|New environment') }}
</gl-deprecated-button>
</gl-button>
</div>
<gl-tabs content-class="gl-display-none">
<gl-tab
v-for="(tab, idx) in tabs"
:key="idx"
:title-item-class="`js-environments-tab-${tab.scope}`"
@click="onChangeTab(tab.scope)"
>
<template #title>
<span>{{ tab.name }}</span>
<gl-badge size="sm" class="gl-tab-counter-badge">{{ tab.count }}</gl-badge>
</template>
</gl-tab>
<template #tabs-end>
<div
class="
gl-display-none
gl-display-md-flex
gl-lg-align-items-center
gl-lg-flex-direction-row
gl-lg-flex-fill-1
gl-lg-justify-content-end
gl-lg-mt-0"
>
<enable-review-app-button
v-if="state.reviewAppDetails.can_setup_review_app"
class="gl-mb-3 gl-lg-mr-3 gl-lg-mb-0"
/>
<gl-button
v-if="canCreateEnvironment && !isLoading"
:href="newEnvironmentPath"
category="primary"
variant="success"
>
{{ s__('Environments|New environment') }}
</gl-button>
</div>
</template>
</gl-tabs>
<container
:is-loading="isLoading"
:environments="state.environments"
:pagination="state.paginationInformation"
:can-read-environment="canReadEnvironment"
:canary-deployment-feature-id="canaryDeploymentFeatureId"
:show-canary-deployment-callout="showCanaryDeploymentCallout"
:user-callouts-path="userCalloutsPath"
:lock-promotion-svg-path="lockPromotionSvgPath"
:help-canary-deployments-path="helpCanaryDeploymentsPath"
:deploy-boards-help-path="deployBoardsHelpPath"
@onChangePage="onChangePage"
>
<template v-if="!isLoading && state.environments.length === 0" #emptyState>
<empty-state :help-path="helpPagePath" />
</template>
</container>
</div>
<container
:is-loading="isLoading"
:environments="state.environments"
:pagination="state.paginationInformation"
:can-read-environment="canReadEnvironment"
:canary-deployment-feature-id="canaryDeploymentFeatureId"
:show-canary-deployment-callout="showCanaryDeploymentCallout"
:user-callouts-path="userCalloutsPath"
:lock-promotion-svg-path="lockPromotionSvgPath"
:help-canary-deployments-path="helpCanaryDeploymentsPath"
:deploy-boards-help-path="deployBoardsHelpPath"
@onChangePage="onChangePage"
>
<template v-if="!isLoading && state.environments.length === 0" #emptyState>
<empty-state :help-path="helpPagePath" />
</template>
</container>
</div>
</template>

View file

@ -7,11 +7,6 @@ import { __ } from './locale';
export default class Milestone {
constructor() {
this.bindTabsSwitching();
// Load merge request tab if it is active
// merge request tab is active based on different conditions in the backend
this.loadTab($('.js-milestone-tabs .active a'));
this.loadInitialTab();
}
@ -23,12 +18,14 @@ export default class Milestone {
this.loadTab($target);
});
}
// eslint-disable-next-line class-methods-use-this
loadInitialTab() {
const $target = $(`.js-milestone-tabs a[href="${window.location.hash}"]`);
const $target = $(`.js-milestone-tabs a:not(.active)[href="${window.location.hash}"]`);
if ($target.length) {
$target.tab('show');
} else {
this.loadTab($('.js-milestone-tabs a.active'));
}
}
// eslint-disable-next-line class-methods-use-this

View file

@ -0,0 +1,3 @@
import initInstanceStatisticsApp from '~/analytics/instance_statistics';
document.addEventListener('DOMContentLoaded', () => initInstanceStatisticsApp());

View file

@ -1,5 +1,4 @@
<script>
import { GlSprintf, GlLink } from '@gitlab/ui';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import { n__, sprintf } from '~/locale';
@ -15,8 +14,6 @@ import {
export default {
components: {
GlSprintf,
GlLink,
TitleArea,
MetadataItem,
},
@ -54,8 +51,6 @@ export default {
},
i18n: {
CONTAINER_REGISTRY_TITLE,
LIST_INTRO_TEXT,
EXPIRATION_POLICY_DISABLED_MESSAGE,
},
computed: {
imagesCountText() {
@ -83,52 +78,40 @@ export default {
!this.expirationPolicyEnabled && this.imagesCount > 0 && !this.hideExpirationPolicyData
);
},
infoMessages() {
const base = [{ text: LIST_INTRO_TEXT, link: this.helpPagePath }];
return this.showExpirationPolicyTip
? [
...base,
{ text: EXPIRATION_POLICY_DISABLED_MESSAGE, link: this.expirationPolicyHelpPagePath },
]
: base;
},
},
};
</script>
<template>
<div>
<title-area :title="$options.i18n.CONTAINER_REGISTRY_TITLE">
<template #right-actions>
<slot name="commands"></slot>
</template>
<template #metadata_count>
<metadata-item
v-if="imagesCount"
data-testid="images-count"
icon="container-image"
:text="imagesCountText"
/>
</template>
<template #metadata_exp_policies>
<metadata-item
v-if="!hideExpirationPolicyData"
data-testid="expiration-policy"
icon="expire"
:text="expirationPolicyText"
size="xl"
/>
</template>
</title-area>
<div data-testid="info-area">
<p>
<span data-testid="default-intro">
<gl-sprintf :message="$options.i18n.LIST_INTRO_TEXT">
<template #docLink="{content}">
<gl-link :href="helpPagePath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
<span v-if="showExpirationPolicyTip" data-testid="expiration-disabled-message">
<gl-sprintf :message="$options.i18n.EXPIRATION_POLICY_DISABLED_MESSAGE">
<template #docLink="{content}">
<gl-link :href="expirationPolicyHelpPagePath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
</p>
</div>
</div>
<title-area :title="$options.i18n.CONTAINER_REGISTRY_TITLE" :info-messages="infoMessages">
<template #right-actions>
<slot name="commands"></slot>
</template>
<template #metadata_count>
<metadata-item
v-if="imagesCount"
data-testid="images-count"
icon="container-image"
:text="imagesCountText"
/>
</template>
<template #metadata_exp_policies>
<metadata-item
v-if="!hideExpirationPolicyData"
data-testid="expiration-policy"
icon="expire"
:text="expirationPolicyText"
size="xl"
/>
</template>
</title-area>
</template>

View file

@ -1,10 +1,12 @@
<script>
import { GlAvatar } from '@gitlab/ui';
import { GlAvatar, GlSprintf, GlLink } from '@gitlab/ui';
export default {
name: 'TitleArea',
components: {
GlAvatar,
GlSprintf,
GlLink,
},
props: {
avatar: {
@ -17,6 +19,11 @@ export default {
default: null,
required: false,
},
infoMessages: {
type: Array,
default: () => [],
required: false,
},
},
data() {
return {
@ -30,37 +37,58 @@ export default {
</script>
<template>
<div class="gl-display-flex gl-justify-content-space-between gl-py-3">
<div class="gl-flex-direction-column">
<div class="gl-display-flex">
<gl-avatar v-if="avatar" :src="avatar" shape="rect" class="gl-align-self-center gl-mr-4" />
<div class="gl-display-flex gl-flex-direction-column">
<div class="gl-display-flex gl-justify-content-space-between gl-py-3">
<div class="gl-flex-direction-column">
<div class="gl-display-flex">
<gl-avatar
v-if="avatar"
:src="avatar"
shape="rect"
class="gl-align-self-center gl-mr-4"
/>
<div class="gl-display-flex gl-flex-direction-column">
<h1 class="gl-font-size-h1 gl-mt-3 gl-mb-2" data-testid="title">
<slot name="title">{{ title }}</slot>
</h1>
<div class="gl-display-flex gl-flex-direction-column">
<h1 class="gl-font-size-h1 gl-mt-3 gl-mb-2" data-testid="title">
<slot name="title">{{ title }}</slot>
</h1>
<div
v-if="$slots['sub-header']"
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
>
<slot name="sub-header"></slot>
</div>
</div>
</div>
<div class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3">
<div
v-if="$slots['sub-header']"
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
v-for="(row, metadataIndex) in metadataSlots"
:key="metadataIndex"
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<slot name="sub-header"></slot>
<slot :name="row"></slot>
</div>
</div>
</div>
<div class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3">
<div
v-for="(row, metadataIndex) in metadataSlots"
:key="metadataIndex"
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<slot :name="row"></slot>
</div>
<div v-if="$slots['right-actions']" class="gl-mt-3">
<slot name="right-actions"></slot>
</div>
</div>
<div v-if="$slots['right-actions']" class="gl-mt-3">
<slot name="right-actions"></slot>
</div>
<p>
<span
v-for="(message, index) in infoMessages"
:key="index"
class="gl-mr-2"
data-testid="info-message"
>
<gl-sprintf :message="message.text">
<template #docLink="{content}">
<gl-link :href="message.link" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
</p>
</div>
</template>

View file

@ -10,6 +10,10 @@ import { validateParams } from '~/pipelines/utils';
export default {
methods: {
onChangeTab(scope) {
if (this.scope === scope) {
return;
}
let params = {
scope,
page: '1',

View file

@ -88,11 +88,6 @@
content: '\f078';
}
.fa-remove::before,
.fa-times::before {
content: '\f00d';
}
.fa-caret-down::before {
content: '\f0d7';
}

View file

@ -3,13 +3,25 @@
module MilestoneActions
extend ActiveSupport::Concern
def issues
respond_to do |format|
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_issues_tab", {
issues: @milestone.sorted_issues(current_user), # rubocop:disable Gitlab/ModuleWithInstanceVariables
show_project_name: Gitlab::Utils.to_boolean(params[:show_project_name])
})
end
end
end
def merge_requests
respond_to do |format|
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_merge_requests_tab", {
merge_requests: @milestone.sorted_merge_requests(current_user), # rubocop:disable Gitlab/ModuleWithInstanceVariables
show_project_name: true
show_project_name: Gitlab::Utils.to_boolean(params[:show_project_name])
})
end
end

View file

@ -3,7 +3,7 @@
class Groups::MilestonesController < Groups::ApplicationController
include MilestoneActions
before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels, :destroy]
before_action :milestone, only: [:edit, :show, :update, :issues, :merge_requests, :participants, :labels, :destroy]
before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update, :destroy]
before_action do
push_frontend_feature_flag(:burnup_charts, @group)

View file

@ -5,7 +5,7 @@ class Projects::MilestonesController < Projects::ApplicationController
include MilestoneActions
before_action :check_issuables_available!
before_action :milestone, only: [:edit, :update, :destroy, :show, :merge_requests, :participants, :labels, :promote]
before_action :milestone, only: [:edit, :update, :destroy, :show, :issues, :merge_requests, :participants, :labels, :promote]
before_action do
push_frontend_feature_flag(:burnup_charts, @project)
end
@ -14,7 +14,7 @@ class Projects::MilestonesController < Projects::ApplicationController
before_action :authorize_read_milestone!
# Allow admin milestone
before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels]
before_action :authorize_admin_milestone!, except: [:index, :show, :issues, :merge_requests, :participants, :labels]
# Allow to promote milestone
before_action :authorize_promote_milestone!, only: :promote

View file

@ -228,8 +228,8 @@ module TimeboxesHelper
end
alias_method :milestone_date_range, :timebox_date_range
def milestone_tab_path(milestone, tab)
url_for(action: tab, format: :json)
def milestone_tab_path(milestone, tab, params = {})
url_for(params.merge(action: tab, format: :json))
end
def update_milestone_path(milestone, params = {})

View file

@ -4,6 +4,6 @@
%hr
= link_to _("Resend Request"), retry_admin_hook_hook_log_path(@hook, @hook_log), method: :post, class: "btn btn-default float-right gl-ml-3"
= link_to _("Resend Request"), retry_admin_hook_hook_log_path(@hook, @hook_log), method: :post, class: "btn gl-button btn-default float-right gl-ml-3"
= render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }

View file

@ -1,11 +0,0 @@
-#-----------------------------------------------------------------
WARNING: Please keep changes up-to-date with the following files:
- `assets/javascripts/diffs/components/commit_widget.vue`
-#-----------------------------------------------------------------
- collapsible = local_assigns.fetch(:collapsible, true)
- if @commit
.info-well.mw-100.mx-0
.well-segment
%ul.blob-commit-info
= render 'projects/commits/commit', commit: @commit, merge_request: @merge_request, view_details: true, collapsible: collapsible

View file

@ -1,11 +0,0 @@
- if @merge_request_diff && different_base?(@start_version, @merge_request_diff)
.mr-version-controls
.content-block
= sprite_icon('information-o')
Selected versions have different base commits.
Changes will include
= link_to project_compare_path(@project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do
new commits
from
= succeed '.' do
%code.ref-name= @merge_request.target_branch

View file

@ -1,21 +0,0 @@
= render 'projects/merge_requests/diffs/version_controls'
= render 'projects/merge_requests/diffs/different_base'
= render 'projects/merge_requests/diffs/not_all_comments_displayed'
= render 'projects/merge_requests/diffs/commit_widget'
- if @merge_request_diff&.empty?
.row.empty-state.nothing-here-block
.col-12
.svg-content= image_tag 'illustrations/merge_request_changes_empty.svg'
.col-12
.text-content.text-center
%p
No changes between
%span.ref-name= @merge_request.source_branch
and
%span.ref-name= @merge_request.target_branch
.text-center= link_to 'Create commit', project_new_blob_path(@project, @merge_request.source_branch), class: 'btn btn-success'
- else
- diff_viewable = @merge_request_diff ? @merge_request_diff.viewable? : true
- if diff_viewable
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, merge_request: true

View file

@ -1,17 +0,0 @@
- if @commit || @start_version || (@merge_request_diff && !@merge_request_diff.latest?)
.mr-version-controls
.content-block.comments-disabled-notif.clearfix
= sprite_icon('information-o')
= succeed '.' do
- if @commit
Only comments from the following commit are shown below
- else
Not all comments are displayed because you're
- if @start_version
comparing two versions of the diff
- else
viewing an old version of the diff
.float-right
= link_to diffs_project_merge_request_path(@merge_request.project, @merge_request), class: 'btn btn-sm' do
Show latest version
= "of the diff" if @commit

View file

@ -1,73 +0,0 @@
- if @merge_request_diff && @merge_request_diffs.size > 1
.mr-version-controls
.mr-version-menus-container.content-block
Changes between
%span.dropdown.inline.mr-version-dropdown
%a.dropdown-toggle.btn.btn-default{ data: { toggle: :dropdown, display: 'static' } }
%span
- if @merge_request_diff.latest?
latest version
- else
version #{version_index(@merge_request_diff)}
= icon('caret-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Version:
%button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times', class: 'dropdown-menu-close-icon')
.dropdown-content
%ul
- @merge_request_diffs.each do |merge_request_diff|
%li
= link_to merge_request_version_path(@project, @merge_request, merge_request_diff, @start_sha), class: ('is-active' if merge_request_diff == @merge_request_diff) do
%div
%strong
- if merge_request_diff.latest?
latest version
- else
version #{version_index(merge_request_diff)}
%div
%small.commit-sha= short_sha(merge_request_diff.head_commit_sha)
%div
%small
#{number_with_delimiter(merge_request_diff.commits_count)} #{'commit'.pluralize(merge_request_diff.commits_count)},
= time_ago_with_tooltip(merge_request_diff.created_at)
- if @merge_request_diff.base_commit_sha
and
%span.dropdown.inline.mr-version-compare-dropdown
%a.btn.btn-default.dropdown-toggle{ data: { toggle: :dropdown, display: 'static' } }
- if @start_version
version #{version_index(@start_version)}
- else
%span.ref-name= @merge_request.target_branch
= icon('caret-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Compared with:
%button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
= icon('times', class: 'dropdown-menu-close-icon')
.dropdown-content
%ul
- @comparable_diffs.each do |merge_request_diff|
%li
= link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do
%div
%strong
- if merge_request_diff.latest?
latest version
- else
version #{version_index(merge_request_diff)}
%div
%small.commit-sha= short_sha(merge_request_diff.head_commit_sha)
%div
%small
= time_ago_with_tooltip(merge_request_diff.created_at)
%li
= link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_version) do
%div
%strong
%span.ref-name= @merge_request.target_branch
(base)
%div
%strong.commit-sha= short_sha(@merge_request_diff.base_commit_sha)

View file

@ -10,8 +10,6 @@
%span
- if show_project_name
%strong #{project.name} &middot;
- elsif show_full_project_name
%strong #{project.full_name} &middot;
- if issuable.is_a?(Issue)
= confidential_icon(issuable)
= link_to issuable.title, issuable_url_args, title: issuable.title

View file

@ -15,4 +15,4 @@
= render partial: 'shared/milestones/issuable',
collection: issuables,
as: :issuable,
locals: { show_project_name: show_project_name, show_full_project_name: show_full_project_name }
locals: { show_project_name: show_project_name }

View file

@ -1,5 +1,4 @@
- args = { show_project_name: local_assigns.fetch(:show_project_name, false),
show_full_project_name: local_assigns.fetch(:show_full_project_name, false) }
- args = { show_project_name: local_assigns.fetch(:show_project_name, false) }
- if display_issues_count_warning?(@milestone)
.flash-container

View file

@ -1,5 +1,4 @@
- args = { show_project_name: local_assigns.fetch(:show_project_name, false),
show_full_project_name: local_assigns.fetch(:show_full_project_name, false) }
- args = { show_project_name: local_assigns.fetch(:show_project_name, false) }
.row.gl-mt-3
.col-md-3

View file

@ -1,14 +1,16 @@
- show_project_name = local_assigns.fetch(:show_project_name, false)
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
%ul.nav-links.scrolling-tabs.js-milestone-tabs.nav.nav-tabs
%li.nav-item
= link_to '#tab-issues', class: 'nav-link active', data: { toggle: 'tab', show: '.tab-issues-buttons' } do
= link_to '#tab-issues', class: 'nav-link active', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'issues', show_project_name: show_project_name) } do
= _('Issues')
%span.badge.badge-pill= milestone.issues_visible_to_user(current_user).size
- if milestone.merge_requests_enabled?
%li.nav-item
= link_to '#tab-merge-requests', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'merge_requests') } do
= link_to '#tab-merge-requests', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'merge_requests', show_project_name: show_project_name) } do
= _('Merge Requests')
%span.badge.badge-pill= milestone.merge_requests_visible_to_user(current_user).size
%li.nav-item
@ -20,20 +22,13 @@
= _('Labels')
%span.badge.badge-pill= milestone.issue_labels_visible_by_user(current_user).count
- issues = milestone.sorted_issues(current_user)
- show_project_name = local_assigns.fetch(:show_project_name, false)
- show_full_project_name = local_assigns.fetch(:show_full_project_name, false)
.tab-content.milestone-content
.tab-pane.active#tab-issues{ data: { sort_endpoint: (sort_issues_project_milestone_path(@project, @milestone) if @project && current_user) } }
= render 'shared/milestones/issues_tab', issues: issues, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane.active#tab-issues
= render "shared/milestones/tab_loading"
- if milestone.merge_requests_enabled?
.tab-pane#tab-merge-requests
-# loaded async
= render "shared/milestones/tab_loading"
.tab-pane#tab-participants
-# loaded async
= render "shared/milestones/tab_loading"
.tab-pane#tab-labels
-# loaded async
= render "shared/milestones/tab_loading"

View file

@ -0,0 +1,5 @@
---
title: Load issues tab in the milestone page asynchronously
merge_request: 42473
author:
type: performance

View file

@ -0,0 +1,5 @@
---
title: Migrate custom Tabs to GlTabs
merge_request: 42236
author:
type: changed

View file

@ -61,6 +61,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resources :milestones, constraints: { id: %r{[^/]+} } do
member do
get :issues
get :merge_requests
get :participants
get :labels

View file

@ -161,8 +161,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :milestones, constraints: { id: /\d+/ } do
member do
post :promote
put :sort_issues
put :sort_merge_requests
get :issues
get :merge_requests
get :participants
get :labels

View file

@ -298,6 +298,7 @@ OmniAuth
onboarding
OpenID
OpenShift
Opsgenie
Packagist
parallelization
parallelizations
@ -382,6 +383,7 @@ reusability
reverified
reverifies
reverify
RHEL
rollout
rollouts
rsync

View file

@ -547,6 +547,7 @@ runtime.
at least version **1.8** if you want to use private registries.
- Available for [Kubernetes executor](https://docs.gitlab.com/runner/executors/kubernetes.html)
in GitLab Runner 13.1 and later.
- [Credentials Store](#using-credentials-store) and [Credential Helpers](#using-credential-helpers) require binaries to be added to the GitLab Runner's `$PATH`, and require access to do so. Therefore, these features are not available on shared runners or any other runner where the user does not have access to the environment where the runner is installed.
### Using statically-defined credentials

View file

@ -43,6 +43,7 @@ For information on how to install, configure, update, and upgrade your own GitLa
**Must-reads:**
- [Guide on adapting existing and introducing new components](architecture.md#adapting-existing-and-introducing-new-components)
- [Code review guidelines](code_review.md) for reviewing code and having code reviewed
- [Database review guidelines](database_review.md) for reviewing database-related changes and complex SQL queries, and having them reviewed
- [Secure coding guidelines](secure_coding_guidelines.md)

View file

@ -1,32 +1,91 @@
# GitLab Architecture Overview
# GitLab architecture overview
## Software delivery
There are two software distributions of GitLab: the open source [Community Edition](https://gitlab.com/gitlab-org/gitlab-foss/) (CE), and the open core [Enterprise Edition](https://gitlab.com/gitlab-org/gitlab/) (EE). GitLab is available under [different subscriptions](https://about.gitlab.com/pricing/).
There are two software distributions of GitLab:
New versions of GitLab are released in stable branches and the master branch is for bleeding edge development.
- The open source [Community Edition](https://gitlab.com/gitlab-org/gitlab-foss/) (CE).
- The open core [Enterprise Edition](https://gitlab.com/gitlab-org/gitlab/) (EE).
For information, see the [GitLab Release Process](https://gitlab.com/gitlab-org/release/docs/-/tree/master#gitlab-release-process).
GitLab is available under [different subscriptions](https://about.gitlab.com/pricing/).
Both EE and CE require some add-on components called GitLab Shell and Gitaly. These components are available from the [GitLab Shell](https://gitlab.com/gitlab-org/gitlab-shell/-/tree/master) and [Gitaly](https://gitlab.com/gitlab-org/gitaly/-/tree/master) repositories respectively. New versions are usually tags but staying on the master branch will give you the latest stable version. New releases are generally around the same time as GitLab CE releases with the exception of informal security updates deemed critical.
New versions of GitLab are released from stable branches, and the `master` branch is used for
bleeding-edge development.
For more information, visit the [GitLab Release Process](https://about.gitlab.com/handbook/engineering/releases/).
Both distributions require additional components. These components are described in the
[Component details](#components) section, and all have their own repositories.
New versions of each dependent component are usually tags, but staying on the `master` branch of the
GitLab codebase gives you the latest stable version of those components. New versions are
generally released around the same time as GitLab releases, with the exception of informal security
updates deemed critical.
## Components
A typical install of GitLab will be on GNU/Linux. It uses NGINX or Apache as a web front end to proxypass the Unicorn web server. By default, communication between Unicorn and the front end is via a Unix domain socket but forwarding requests via TCP is also supported. The web front end accesses `/home/git/gitlab/public` bypassing the Unicorn server to serve static pages, uploads (e.g. avatar images or attachments), and pre-compiled assets. GitLab serves web pages and the [GitLab API](../api/README.md) using the Unicorn web server. It uses Sidekiq as a job queue which, in turn, uses Redis as a non-persistent database backend for job information, meta data, and incoming jobs.
A typical install of GitLab is on GNU/Linux, but growing number of deployments also use the
Kubernetes platform. The largest known GitLab instance is on GitLab.com, which is deployed using our
[official GitLab Helm chart](https://docs.gitlab.com/charts/) and the [official Linux package](https://about.gitlab.com/install/).
We also support deploying GitLab on Kubernetes using our [GitLab Helm chart](https://docs.gitlab.com/charts/).
A typical installation uses NGINX or Apache as a web server to proxy through
[GitLab Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse) and into the [Puma](https://puma.io)
application server. GitLab serves web pages and the [GitLab API](../api/README.md) using the Puma
application server. It uses Sidekiq as a job queue which, in turn, uses Redis as a non-persistent
database backend for job information, metadata, and incoming jobs.
The GitLab web app uses PostgreSQL for persistent database information (e.g. users, permissions, issues, other meta data). GitLab stores the bare Git repositories it serves in `/home/git/repositories` by default. It also keeps default branch and hook information with the bare repository.
By default, communication between Puma and Workhorse is via a Unix domain socket, but forwarding
requests via TCP is also supported. Workhorse accesses the `gitlab/public` directory, bypassing the
Puma application server to serve static pages, uploads (for example, avatar images or attachments),
and pre-compiled assets.
When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to resolve authorization and access as well as serving Git objects.
The GitLab application uses PostgreSQL for persistent database information (for example, users,
permissions, issues, or other metadata). GitLab stores the bare Git repositories in the location
defined in [the configuration file, `repositories:` section](https://gitlab.com/gitlab-org/gitlab/blob/master/config/gitlab.yml.example).
It also keeps default branch and hook information with the bare repository.
The add-on component GitLab Shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. GitLab Shell accesses the bare repositories through Gitaly to serve Git objects and communicates with Redis to submit jobs to Sidekiq for GitLab to process. GitLab Shell queries the GitLab API to determine authorization and access.
When serving repositories over HTTP/HTTPS GitLab uses the GitLab API to resolve authorization and
access and to serve Git objects.
Gitaly executes Git operations from GitLab Shell and the GitLab web app, and provides an API to the GitLab web app to get attributes from Git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files).
The add-on component GitLab Shell serves repositories over SSH. It manages the SSH keys within the
location defined in [the configuration file, `GitLab Shell` section](https://gitlab.com/gitlab-org/gitlab/blob/master/config/gitlab.yml.example).
The file in that location should never be manually edited. GitLab Shell accesses the bare
repositories through Gitaly to serve Git objects, and communicates with Redis to submit jobs to
Sidekiq for GitLab to process. GitLab Shell queries the GitLab API to determine authorization and access.
Gitaly executes Git operations from GitLab Shell and the GitLab web app, and provides an API to the
GitLab web app to get attributes from Git (for example, title, branches, tags, or other metadata),
and to get blobs (for example, diffs, commits, or files).
You may also be interested in the [production architecture of GitLab.com](https://about.gitlab.com/handbook/engineering/infrastructure/production/architecture/).
### Simplified Component Overview
## Adapting existing and introducing new components
There are fundamental differences in how the application behaves when it is installed on a
traditional Linux machine compared to a containerized platform, such as Kubernetes.
Compared to [our official installation methods](https://about.gitlab.com/install/), some of the
notable differences are:
- Official Linux packages can access files on the same file system with different services.
[Shared files](shared_files.md) are not an option for the application running on the Kubernetes
platform.
- Official Linux packages by default have services that have access to the shared configuration and
network. This is not the case for services running in Kubernetes, where services might be running
in complete isolation, or only accessible through specific ports.
In other words, the shared state between services needs to be carefully considered when
architecting new features and adding new components. Services that need to have access to the same
files, need to be able to exchange information through the appropriate APIs. Whenever possible,
this should not be done with files.
Since components written with the API-first philosophy in mind are compatible with both methods, all
new features and services must be written to consider Kubernetes compatibility **first**.
The simplest way to ensure this, is to add support for your feature or service to
[the official GitLab Helm chart](https://docs.gitlab.com/charts/) or reach out to
[the Distribution team](https://about.gitlab.com/handbook/engineering/development/enablement/distribution/#how-to-work-with-distribution).
### Simplified component overview
This is a simplified architecture diagram that can be used to
understand GitLab's architecture.
@ -411,7 +470,8 @@ For monitoring deployed apps, see [Jaeger tracing documentation](../operations/t
- Layer: Core Service
- Process: `logrotate`
GitLab is comprised of a large number of services that all log. We started bundling our own logrotate as of 7.4 to make sure we were logging responsibly. This is just a packaged version of the common open source offering.
GitLab is comprised of a large number of services that all log. We started bundling our own Logrotate
as of GitLab 7.4 to make sure we were logging responsibly. This is just a packaged version of the common open source offering.
#### Mattermost
@ -669,7 +729,7 @@ You can install them after you create a cluster. This includes:
- [JupyterHub](https://jupyter.org)
- [Knative](https://cloud.google.com/knative/)
## GitLab by Request Type
## GitLab by request type
GitLab provides two "interfaces" for end users to access the service:
@ -678,7 +738,7 @@ GitLab provides two "interfaces" for end users to access the service:
It's important to understand the distinction as some processes are used in both and others are exclusive to a specific request type.
### GitLab Web HTTP Request Cycle
### GitLab Web HTTP request cycle
When making a request to an HTTP Endpoint (think `/users/sign_in`) the request will take the following path through the GitLab Service:
@ -687,11 +747,11 @@ When making a request to an HTTP Endpoint (think `/users/sign_in`) the request w
- Unicorn - Since this is a web request, and it needs to access the application it will go to Unicorn.
- PostgreSQL/Gitaly/Redis - Depending on the type of request, it may hit these services to store or retrieve data.
### GitLab Git Request Cycle
### GitLab Git request cycle
Below we describe the different paths that HTTP vs. SSH Git requests will take. There is some overlap with the Web Request Cycle but also some differences.
### Web Request (80/443)
### Web request (80/443)
Git operations over HTTP use the stateless "smart" protocol described in the
[Git documentation](https://git-scm.com/docs/http-protocol), but responsibility
@ -736,7 +796,7 @@ sequenceDiagram
The sequence is similar for `git push`, except `git-receive-pack` is used
instead of `git-upload-pack`.
### SSH Request (22)
### SSH request (22)
Git operations over SSH can use the stateful protocol described in the
[Git documentation](https://git-scm.com/docs/pack-protocol#_ssh_transport), but
@ -801,7 +861,7 @@ except there is no round-trip into Gitaly - Rails performs the action as part
of the [internal API](internal_api.md) call, and GitLab Shell streams the
response back to the user directly.
## System Layout
## System layout
When referring to `~git` in the pictures it means the home directory of the Git user which is typically `/home/git`.
@ -811,7 +871,7 @@ The bare repositories are located in `/home/git/repositories`. GitLab is a Ruby
To serve repositories over SSH there's an add-on application called GitLab Shell which is installed in `/home/git/gitlab-shell`.
### Installation Folder Summary
### Installation folder summary
To summarize here's the [directory structure of the `git` user home directory](../install/structure.md).
@ -824,7 +884,7 @@ ps aux | grep '^git'
GitLab has several components to operate. It requires a persistent database
(PostgreSQL) and Redis database, and uses Apache `httpd` or NGINX to proxypass
Unicorn. All these components should run as different system users to GitLab
(e.g., `postgres`, `redis` and `www-data`, instead of `git`).
(for example, `postgres`, `redis`, and `www-data`, instead of `git`).
As the `git` user it starts Sidekiq and Unicorn (a simple Ruby HTTP server
running on port `8080` by default). Under the GitLab user there are normally 4
@ -914,15 +974,16 @@ PostgreSQL:
### GitLab specific configuration files
GitLab has configuration files located in `/home/git/gitlab/config/*`. Commonly referenced config files include:
GitLab has configuration files located in `/home/git/gitlab/config/*`. Commonly referenced
configuration files include:
- `gitlab.yml` - GitLab configuration.
- `unicorn.rb` - Unicorn web server settings.
- `database.yml` - Database connection settings.
- `gitlab.yml` - GitLab configuration
- `unicorn.rb` - Unicorn web server settings
- `database.yml` - Database connection settings
GitLab Shell has a configuration file at `/home/git/gitlab-shell/config.yml`.
### Maintenance Tasks
### Maintenance tasks
[GitLab](https://gitlab.com/gitlab-org/gitlab/tree/master) provides Rake tasks with which you see version information and run a quick check on your configuration to ensure it is configured properly within the application. See [maintenance Rake tasks](../raketasks/maintenance.md).
In a nutshell, do the following:
@ -934,7 +995,8 @@ bundle exec rake gitlab:env:info RAILS_ENV=production
bundle exec rake gitlab:check RAILS_ENV=production
```
Note: It is recommended to log into the `git` user using `sudo -i -u git` or `sudo su - git`. While the sudo commands provided by GitLab work in Ubuntu they do not always work in RHEL.
Note: It is recommended to log into the `git` user using `sudo -i -u git` or `sudo su - git`. While
the `sudo` commands provided by GitLab work in Ubuntu they do not always work in RHEL.
## GitLab.com

View file

@ -86,8 +86,9 @@ You can also dismiss vulnerabilities in the table:
The group Security Dashboard gives an overview of the vulnerabilities in the default branches of the
projects in a group and its subgroups. Access it by navigating to **Security > Security Dashboard**
for your group. By default, the Security Dashboard displays all detected and confirmed
vulnerabilities.
after selecting your group. By default, the Security Dashboard displays all detected and confirmed
vulnerabilities. If you don't see the vulnerabilities over time graph, the likely cause is that you
have not selected a group.
NOTE: **Note:**
The Security Dashboard only shows projects with [security reports](#supported-reports) enabled in a
@ -96,20 +97,20 @@ group.
![Dashboard with action buttons and metrics](img/group_security_dashboard_v13_3.png)
There is a timeline chart that shows how many open
vulnerabilities your projects had at various points in time. You can filter among 30, 60, and
90 days, with the default being 90. Hover over the chart to get more details about
the open vulnerabilities at a specific time.
vulnerabilities your projects had at various points in time. You can display the vulnerability
trends over a 30, 60, or 90-day time frame (the default is 90 days). Hover over the chart to get
more details about the open vulnerabilities at a specific time.
Next to the timeline chart is a list of projects, grouped and sorted by the severity of the vulnerability found:
- F: 1 or more "critical"
- D: 1 or more "high" or "unknown"
- C: 1 or more "medium"
- B: 1 or more "low"
- A: 0 vulnerabilities
- F: One or more "critical"
- D: One or more "high" or "unknown"
- C: One or more "medium"
- B: One or more "low"
- A: Zero vulnerabilities
Projects with no vulnerability tests configured will not appear in the list. Additionally, dismissed
vulnerabilities are not included either.
vulnerabilities are excluded.
Navigate to the group's [Vulnerability Report](#vulnerability-list) to view the vulnerabilities found.

View file

@ -512,6 +512,11 @@ Cleanup policies can be run on all projects, with these exceptions:
for all projects (even those created before 12.8) in
[GitLab application settings](../../../api/settings.md#change-application-settings)
by setting `container_expiration_policies_enable_historic_entries` to true.
Alternatively, you can execute the following command in the [Rails console](../../../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session):
```ruby
ApplicationSetting.last.update(container_expiration_policies_enable_historic_entries: true)
```
There are performance risks with enabling it for all projects, especially if you
are using an [external registry](./index.md#use-with-external-container-registries).

View file

@ -32,7 +32,7 @@ file path fragments to start seeing results.
## Syntax highlighting
As expected from an IDE, syntax highlighting for many languages within
the Web IDE will make your direct editing even easier.
the Web IDE makes your direct editing even easier.
The Web IDE currently provides:
@ -143,7 +143,7 @@ The Web IDE supports configuration of certain editor settings by using
[`.editorconfig` files](https://editorconfig.org/). When opening a file, the
Web IDE looks for a file named `.editorconfig` in the current directory
and all parent directories. If a configuration file is found and has settings
that match the file's path, these settings will be enforced on the opened file.
that match the file's path, these settings are enforced on the opened file.
The Web IDE currently supports the following `.editorconfig` settings:
@ -166,7 +166,7 @@ review the list of changed files.
Once you have finalized your changes, you can add a commit message, commit the
changes and directly create a merge request. In case you don't have write
access to the selected branch, you will see a warning, but still be able to create
access to the selected branch, you see a warning, but can still create
a new branch and start a merge request.
To discard a change in a particular file, click the **Discard changes** button on that
@ -201,8 +201,7 @@ left.
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/19318) in [GitLab Core](https://about.gitlab.com/pricing/) 11.0.
To switch between your authored and assigned merge requests, click the
dropdown in the top of the sidebar to open a list of merge requests. You will
need to commit or discard all your changes before switching to a different merge
dropdown in the top of the sidebar to open a list of merge requests. You need to commit or discard all your changes before switching to a different merge
request.
## Switching branches
@ -211,7 +210,7 @@ request.
To switch between branches of the current project repository, click the dropdown
in the top of the sidebar to open a list of branches.
You will need to commit or discard all your changes before switching to a
You need to commit or discard all your changes before switching to a
different branch.
## Markdown editing
@ -226,7 +225,7 @@ supports [GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown-g
You can also upload any local images by pasting them directly in the Markdown file.
The image is uploaded to the same directory and is named `image.png` by default.
If another file already exists with the same name, a numeric suffix is automatically
added to the file name.
added to the filename.
## Live Preview
@ -249,7 +248,7 @@ The Live Preview feature needs to be enabled in the GitLab instances
admin settings. Live Preview is enabled for all projects on
GitLab.com
![Admin Live Preview setting](img/admin_live_preview_v13_0.png)
![Administrator Live Preview setting](img/admin_live_preview_v13_0.png)
Once you have done that, you can preview projects with a `package.json` file and
a `main` entry point inside the Web IDE. An example `package.json` is shown
@ -292,7 +291,7 @@ to work:
[enabled](../../../administration/integration/terminal.md#enabling-and-disabling-terminal-support). **(ULTIMATE ONLY)**
If you have the terminal open and the job has finished with its tasks, the
terminal will block the job from finishing for the duration configured in
terminal blocks the job from finishing for the duration configured in
[`[session_server].session_timeout`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-session_server-section)
until you close the terminal window.
@ -308,15 +307,15 @@ In order to enable the Web IDE terminals you need to create the file
file is fairly similar to the [CI configuration file](../../../ci/yaml/README.md)
syntax but with some restrictions:
- No global blocks can be defined (ie: `before_script` or `after_script`)
- No global blocks can be defined (i.e., `before_script` or `after_script`)
- Only one job named `terminal` can be added to this file.
- Only the keywords `image`, `services`, `tags`, `before_script`, `script`, and
`variables` are allowed to be used to configure the job.
- To connect to the interactive terminal, the `terminal` job must be still alive
and running, otherwise the terminal won't be able to connect to the job's session.
and running, otherwise the terminal cannot connect to the job's session.
By default the `script` keyword has the value `sleep 60` to prevent
the job from ending and giving the Web IDE enough time to connect. This means
that, if you override the default `script` value, you'll have to add a command
that, if you override the default `script` value, you have to add a command
which would keep the job running, like `sleep`.
In the code below there is an example of this configuration file:
@ -333,40 +332,39 @@ terminal:
NODE_ENV: "test"
```
Once the terminal has started, the console will be displayed and we could access
Once the terminal has started, the console is displayed and we could access
the project repository files.
**Important**. The terminal job is branch dependent. This means that the
configuration file used to trigger and configure the terminal will be the one in
configuration file used to trigger and configure the terminal is the one in
the selected branch of the Web IDE.
If there is no configuration file in a branch, an error message will be shown.
If there is no configuration file in a branch, an error message is shown.
### Running interactive terminals in the Web IDE
If Interactive Terminals are available for the current user, the **Terminal** button
will be visible in the right sidebar of the Web IDE. Click this button to open
If Interactive Terminals are available for the current user, the **Terminal** button is visible in the right sidebar of the Web IDE. Click this button to open
or close the terminal tab.
Once open, the tab will show the **Start Web Terminal** button. This button may
Once open, the tab shows the **Start Web Terminal** button. This button may
be disabled if the environment is not configured correctly. If so, a status
message will describe the issue. Here are some reasons why **Start Web Terminal**
message describes the issue. Here are some reasons why **Start Web Terminal**
may be disabled:
- `.gitlab/.gitlab-webide.yml` does not exist or is set up incorrectly.
- No active private runners are available for the project.
If active, clicking the **Start Web Terminal** button will load the terminal view
If active, clicking the **Start Web Terminal** button loads the terminal view
and start connecting to the runner's terminal. At any time, the **Terminal** tab
can be closed and reopened and the state of the terminal will not be affected.
can be closed and reopened and the state of the terminal is not affected.
When the terminal is started and is successfully connected to the runner, then the
runner's shell prompt will appear in the terminal. From here, you can enter
commands that will be executed within the runner's environment. This is similar
runner's shell prompt appears in the terminal. From here, you can enter
commands executed within the runner's environment. This is similar
to running commands in a local terminal or through SSH.
While the terminal is running, it can be stopped by clicking **Stop Terminal**.
This will disconnect the terminal and stop the runner's terminal job. From here,
This disconnects the terminal and stops the runner's terminal job. From here,
click **Restart Terminal** to start a new terminal session.
### File syncing to web terminal
@ -408,14 +406,14 @@ terminal:
more information.
- `$CI_PROJECT_DIR` is a
[predefined environment variable](../../../ci/variables/predefined_variables.md)
for GitLab Runner. This is where your project's repository will be.
for GitLab Runners. This is where your project's repository resides.
Once you have configured the web terminal for file syncing, then when the web
terminal is started, a **Terminal** status will be visible in the status bar.
terminal is started, a **Terminal** status is visible in the status bar.
![Web IDE Client Side Evaluation](img/terminal_status.png)
Changes made to your files via the Web IDE will sync to the running terminal
Changes made to your files via the Web IDE sync to the running terminal
when:
- <kbd>Ctrl</kbd> + <kbd>S</kbd> (or <kbd>Cmd</kbd> + <kbd>S</kbd> on Mac)
@ -425,7 +423,7 @@ when:
### Limitations
Interactive Terminals is in a beta phase and will continue to be improved upon in upcoming
Interactive Terminals is in a beta phase and continues to be improved in upcoming
releases. In the meantime, please note that the user is limited to having only one
active terminal at a time.

View file

@ -8,8 +8,6 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-09-17 11:26-0400\n"
"PO-Revision-Date: 2020-09-17 11:26-0400\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@ -7184,6 +7182,9 @@ msgstr ""
msgid "Could not find iteration"
msgstr ""
msgid "Could not load instance counts. Please refresh the page to try again."
msgstr ""
msgid "Could not remove the trigger."
msgstr ""
@ -13586,6 +13587,24 @@ msgstr ""
msgid "Instance administrators group already exists"
msgstr ""
msgid "InstanceStatistics|Groups"
msgstr ""
msgid "InstanceStatistics|Issues"
msgstr ""
msgid "InstanceStatistics|Merge Requests"
msgstr ""
msgid "InstanceStatistics|Pipelines"
msgstr ""
msgid "InstanceStatistics|Projects"
msgstr ""
msgid "InstanceStatistics|Users"
msgstr ""
msgid "Integration"
msgstr ""
@ -26166,6 +26185,9 @@ msgstr ""
msgid "Threat Monitoring"
msgstr ""
msgid "ThreatMonitoring|All Environments"
msgstr ""
msgid "ThreatMonitoring|Anomalous Requests"
msgstr ""

View file

@ -9,7 +9,6 @@ RSpec.describe Groups::MilestonesController do
let(:user) { create(:user) }
let(:title) { '肯定不是中文的问题' }
let(:milestone) { create(:milestone, project: project) }
let(:milestone_path) { group_milestone_path(group, milestone.safe_title, title: milestone.title) }
let(:milestone_params) do
{
@ -25,6 +24,12 @@ RSpec.describe Groups::MilestonesController do
project.add_maintainer(user)
end
it_behaves_like 'milestone tabs' do
let(:milestone) { create(:milestone, group: group) }
let(:milestone_path) { group_milestone_path(group, milestone.iid) }
let(:request_params) { { group_id: group, id: milestone.iid } }
end
describe '#index' do
describe 'as HTML' do
render_views

View file

@ -17,7 +17,9 @@ RSpec.describe Projects::MilestonesController do
controller.instance_variable_set(:@project, project)
end
it_behaves_like 'milestone tabs'
it_behaves_like 'milestone tabs' do
let(:request_params) { { namespace_id: project.namespace, project_id: project, id: milestone.iid } }
end
describe "#show" do
render_views

View file

@ -4,12 +4,16 @@ require 'spec_helper'
RSpec.describe "User views milestone" do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:labels) { create_list(:label, 2, project: project) }
before do
before_all do
project.add_developer(user)
end
before do
sign_in(user)
end
@ -25,7 +29,7 @@ RSpec.describe "User views milestone" do
expect { visit_milestone }.not_to exceed_query_limit(control)
end
context 'limiting milestone issues' do
context 'issues list', :js do
before_all do
2.times do
create(:issue, milestone: milestone, project: project)
@ -34,6 +38,28 @@ RSpec.describe "User views milestone" do
end
end
context 'for a project milestone' do
it 'does not show the project name' do
visit(project_milestone_path(project, milestone))
wait_for_requests
expect(page.find('#tab-issues')).not_to have_text(project.name)
end
end
context 'for a group milestone' do
let(:group_milestone) { create(:milestone, group: group) }
it 'shows the project name' do
create(:issue, project: project, milestone: group_milestone)
visit(group_milestone_path(group, group_milestone))
expect(page.find('#tab-issues')).to have_text(project.name)
end
end
context 'when issues on milestone are over DISPLAY_ISSUES_LIMIT' do
it "limits issues to display and shows warning" do
stub_const('Milestoneish::DISPLAY_ISSUES_LIMIT', 3)
@ -56,6 +82,40 @@ RSpec.describe "User views milestone" do
end
end
context 'merge requests list', :js do
context 'for a project milestone' do
it 'does not show the project name' do
create(:merge_request, source_project: project, milestone: milestone)
visit(project_milestone_path(project, milestone))
within('.js-milestone-tabs') do
click_link('Merge Requests')
end
wait_for_requests
expect(page.find('#tab-merge-requests')).not_to have_text(project.name)
end
end
context 'for a group milestone' do
let(:group_milestone) { create(:milestone, group: group) }
it 'shows the project name' do
create(:merge_request, source_project: project, milestone: group_milestone)
visit(group_milestone_path(group, group_milestone))
within('.js-milestone-tabs') do
click_link('Merge Requests')
end
expect(page.find('#tab-merge-requests')).to have_text(project.name)
end
end
end
private
def visit_milestone

View file

@ -21,7 +21,7 @@ RSpec.describe "User views milestones" do
.and have_content("Merge Requests")
end
context "with issues" do
context "with issues", :js do
let_it_be(:issue) { create(:issue, project: project, milestone: milestone) }
let_it_be(:closed_issue) { create(:closed_issue, project: project, milestone: milestone) }
@ -33,7 +33,6 @@ RSpec.describe "User views milestones" do
.and have_selector("#tab-issues li.issuable-row", count: 2)
.and have_content(issue.title)
.and have_content(closed_issue.title)
.and have_selector("#tab-merge-requests")
end
end

View file

@ -372,7 +372,7 @@ RSpec.describe 'Environments page', :js do
let(:role) { :developer }
it 'developer creates a new environment with a valid name' do
within(".top-area") { click_link 'New environment' }
within(".environments-section") { click_link 'New environment' }
fill_in('Name', with: 'production')
click_on 'Save'
@ -380,7 +380,7 @@ RSpec.describe 'Environments page', :js do
end
it 'developer creates a new environmetn with invalid name' do
within(".top-area") { click_link 'New environment' }
within(".environments-section") { click_link 'New environment' }
fill_in('Name', with: 'name,with,commas')
click_on 'Save'

View file

@ -30,6 +30,8 @@ RSpec.describe 'User uses header search field', :js do
before do
find('#search')
find('body').native.send_keys('s')
wait_for_all_requests
end
it 'shows the category search dropdown' do
@ -89,9 +91,7 @@ RSpec.describe 'User uses header search field', :js do
context 'when entering text into the search field' do
it 'does not display the category search dropdown' do
page.within('.search-input-wrap') do
fill_in('search', with: scope_name.first(4))
end
fill_in_search(scope_name.first(4))
expect(page).not_to have_selector('.dropdown-header', text: /#{scope_name}/i)
end
@ -105,9 +105,7 @@ RSpec.describe 'User uses header search field', :js do
end
it 'displays search options' do
page.within('.search-input-wrap') do
fill_in('search', with: 'test')
end
fill_in_search('test')
expect(page).to have_selector(scoped_search_link('test'))
end
@ -140,9 +138,7 @@ RSpec.describe 'User uses header search field', :js do
end
it 'displays search options' do
page.within('.search-input-wrap') do
fill_in('search', with: 'test')
end
fill_in_search('test')
expect(page).to have_selector(scoped_search_link('test'))
expect(page).to have_selector(scoped_search_link('test', group_id: group.id))
@ -157,9 +153,7 @@ RSpec.describe 'User uses header search field', :js do
end
it 'displays search options' do
page.within('.search-input-wrap') do
fill_in('search', with: 'test')
end
fill_in_search('test')
expect(page).to have_selector(scoped_search_link('test'))
expect(page).not_to have_selector(scoped_search_link('test', group_id: project.namespace_id))
@ -182,9 +176,7 @@ RSpec.describe 'User uses header search field', :js do
end
it 'displays search options' do
page.within('.search-input-wrap') do
fill_in('search', with: 'test')
end
fill_in_search('test')
expect(page).to have_selector(scoped_search_link('test'))
expect(page).to have_selector(scoped_search_link('test', group_id: group.id))
@ -208,9 +200,7 @@ RSpec.describe 'User uses header search field', :js do
end
it 'displays search options' do
page.within('.search-input-wrap') do
fill_in('search', with: 'test')
end
fill_in_search('test')
expect(page).to have_selector(scoped_search_link('test'))
expect(page).to have_selector(scoped_search_link('test', group_id: subgroup.id))

View file

@ -0,0 +1,24 @@
import { shallowMount } from '@vue/test-utils';
import InstanceStatisticsApp from '~/analytics/instance_statistics/components/app.vue';
import InstanceCounts from '~/analytics/instance_statistics/components//instance_counts.vue';
describe('InstanceStatisticsApp', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(InstanceStatisticsApp);
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('displays the instance counts component', () => {
expect(wrapper.find(InstanceCounts).exists()).toBe(true);
});
});

View file

@ -0,0 +1,54 @@
import { shallowMount } from '@vue/test-utils';
import InstanceCounts from '~/analytics/instance_statistics/components/instance_counts.vue';
import MetricCard from '~/analytics/shared/components/metric_card.vue';
import countsMockData from '../mock_data';
describe('InstanceCounts', () => {
let wrapper;
const createComponent = ({ loading = false, data = {} } = {}) => {
const $apollo = {
queries: {
counts: {
loading,
},
},
};
wrapper = shallowMount(InstanceCounts, {
mocks: { $apollo },
data() {
return {
...data,
};
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findMetricCard = () => wrapper.find(MetricCard);
describe('while loading', () => {
beforeEach(() => {
createComponent({ loading: true });
});
it('displays the metric card with isLoading=true', () => {
expect(findMetricCard().props('isLoading')).toBe(true);
});
});
describe('with data', () => {
beforeEach(() => {
createComponent({ data: { counts: countsMockData } });
});
it('passes the counts data to the metric card', () => {
expect(findMetricCard().props('metrics')).toEqual(countsMockData);
});
});
});

View file

@ -0,0 +1,4 @@
export default [
{ key: 'projects', value: 10, label: 'Projects' },
{ key: 'groups', value: 20, label: 'Group' },
];

View file

@ -0,0 +1,129 @@
import { mount } from '@vue/test-utils';
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import MetricCard from '~/analytics/shared/components/metric_card.vue';
const metrics = [
{ key: 'first_metric', value: 10, label: 'First metric', unit: 'days', link: 'some_link' },
{ key: 'second_metric', value: 20, label: 'Yet another metric' },
{ key: 'third_metric', value: null, label: 'Null metric without value', unit: 'parsecs' },
{ key: 'fourth_metric', value: '-', label: 'Metric without value', unit: 'parsecs' },
];
const defaultProps = {
title: 'My fancy title',
isLoading: false,
metrics,
};
describe('MetricCard', () => {
let wrapper;
const factory = (props = defaultProps) => {
wrapper = mount(MetricCard, {
propsData: {
...defaultProps,
...props,
},
directives: {
GlTooltip: createMockDirective(),
},
});
};
afterEach(() => {
wrapper.destroy();
});
const findTitle = () => wrapper.find({ ref: 'title' });
const findLoadingIndicator = () => wrapper.find(GlSkeletonLoading);
const findMetricsWrapper = () => wrapper.find({ ref: 'metricsWrapper' });
const findMetricItem = () => wrapper.findAll({ ref: 'metricItem' });
const findTooltip = () => wrapper.find('[data-testid="tooltip"]');
describe('template', () => {
it('renders the title', () => {
factory();
expect(findTitle().text()).toContain('My fancy title');
});
describe('when isLoading is true', () => {
beforeEach(() => {
factory({ isLoading: true });
});
it('displays a loading indicator', () => {
expect(findLoadingIndicator().exists()).toBe(true);
});
it('does not display the metrics container', () => {
expect(findMetricsWrapper().exists()).toBe(false);
});
});
describe('when isLoading is false', () => {
beforeEach(() => {
factory({ isLoading: false });
});
it('does not display a loading indicator', () => {
expect(findLoadingIndicator().exists()).toBe(false);
});
it('displays the metrics container', () => {
expect(findMetricsWrapper().exists()).toBe(true);
});
it('renders two metrics', () => {
expect(findMetricItem()).toHaveLength(metrics.length);
});
describe('with tooltip text', () => {
const tooltipText = 'This is a tooltip';
const tooltipMetric = {
key: 'fifth_metric',
value: '-',
label: 'Metric with tooltip',
unit: 'parsecs',
tooltipText,
};
beforeEach(() => {
factory({
isLoading: false,
metrics: [tooltipMetric],
});
});
it('will render a tooltip', () => {
const tt = getBinding(findTooltip().element, 'gl-tooltip');
expect(tt.value.title).toEqual(tooltipText);
});
});
describe.each`
columnIndex | label | value | unit | link
${0} | ${'First metric'} | ${10} | ${' days'} | ${'some_link'}
${1} | ${'Yet another metric'} | ${20} | ${''} | ${null}
${2} | ${'Null metric without value'} | ${'-'} | ${''} | ${null}
${3} | ${'Metric without value'} | ${'-'} | ${''} | ${null}
`('metric columns', ({ columnIndex, label, value, unit, link }) => {
it(`renders ${value}${unit} ${label} with URL ${link}`, () => {
const allMetricItems = findMetricItem();
const metricItem = allMetricItems.at(columnIndex);
const text = metricItem.text();
expect(text).toContain(`${value}${unit}`);
expect(text).toContain(label);
if (link) {
expect(metricItem.find('a').attributes('href')).toBe(link);
} else {
expect(metricItem.find('a').exists()).toBe(false);
}
});
});
});
});
});

View file

@ -40,6 +40,9 @@ describe('Environment', () => {
return axios.waitForAll();
};
const findEnvironmentsTabAvailable = () => wrapper.find('.js-environments-tab-available > a');
const findEnvironmentsTabStopped = () => wrapper.find('.js-environments-tab-stopped > a');
beforeEach(() => {
mock = new MockAdapter(axios);
});
@ -108,9 +111,16 @@ describe('Environment', () => {
it('should make an API request when using tabs', () => {
jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
wrapper.find('.js-environments-tab-stopped').trigger('click');
findEnvironmentsTabStopped().trigger('click');
expect(wrapper.vm.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
});
it('should not make the same API request when clicking on the current scope tab', () => {
// component starts at available
jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
findEnvironmentsTabAvailable().trigger('click');
expect(wrapper.vm.updateContent).toHaveBeenCalledTimes(0);
});
});
});
});

View file

@ -2,151 +2,163 @@
exports[`PackageTitle renders with tags 1`] = `
<div
class="gl-display-flex gl-justify-content-space-between gl-py-3"
class="gl-display-flex gl-flex-direction-column"
data-qa-selector="package_title"
>
<div
class="gl-flex-direction-column"
class="gl-display-flex gl-justify-content-space-between gl-py-3"
>
<div
class="gl-display-flex"
class="gl-flex-direction-column"
>
<!---->
<div
class="gl-display-flex gl-flex-direction-column"
class="gl-display-flex"
>
<h1
class="gl-font-size-h1 gl-mt-3 gl-mb-2"
data-testid="title"
>
Test package
</h1>
<!---->
<div
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
class="gl-display-flex gl-flex-direction-column"
>
<gl-icon-stub
class="gl-mr-3"
name="eye"
size="16"
/>
<h1
class="gl-font-size-h1 gl-mt-3 gl-mb-2"
data-testid="title"
>
Test package
</h1>
<gl-sprintf-stub
message="v%{version} published %{timeAgo}"
<div
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
>
<gl-icon-stub
class="gl-mr-3"
name="eye"
size="16"
/>
<gl-sprintf-stub
message="v%{version} published %{timeAgo}"
/>
</div>
</div>
</div>
<div
class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"
>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<metadata-item-stub
data-testid="package-type"
icon="package"
link=""
size="s"
text="maven"
/>
</div>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<metadata-item-stub
data-testid="package-size"
icon="disk"
link=""
size="s"
text="300 bytes"
/>
</div>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<package-tags-stub
hidelabel="true"
tagdisplaylimit="2"
tags="[object Object],[object Object],[object Object],[object Object]"
/>
</div>
</div>
</div>
<div
class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"
>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<metadata-item-stub
data-testid="package-type"
icon="package"
link=""
size="s"
text="maven"
/>
</div>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<metadata-item-stub
data-testid="package-size"
icon="disk"
link=""
size="s"
text="300 bytes"
/>
</div>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<package-tags-stub
hidelabel="true"
tagdisplaylimit="2"
tags="[object Object],[object Object],[object Object],[object Object]"
/>
</div>
</div>
<!---->
</div>
<!---->
<p />
</div>
`;
exports[`PackageTitle renders without tags 1`] = `
<div
class="gl-display-flex gl-justify-content-space-between gl-py-3"
class="gl-display-flex gl-flex-direction-column"
data-qa-selector="package_title"
>
<div
class="gl-flex-direction-column"
class="gl-display-flex gl-justify-content-space-between gl-py-3"
>
<div
class="gl-display-flex"
class="gl-flex-direction-column"
>
<!---->
<div
class="gl-display-flex gl-flex-direction-column"
class="gl-display-flex"
>
<h1
class="gl-font-size-h1 gl-mt-3 gl-mb-2"
data-testid="title"
>
Test package
</h1>
<!---->
<div
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
class="gl-display-flex gl-flex-direction-column"
>
<gl-icon-stub
class="gl-mr-3"
name="eye"
size="16"
/>
<h1
class="gl-font-size-h1 gl-mt-3 gl-mb-2"
data-testid="title"
>
Test package
</h1>
<gl-sprintf-stub
message="v%{version} published %{timeAgo}"
<div
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1"
>
<gl-icon-stub
class="gl-mr-3"
name="eye"
size="16"
/>
<gl-sprintf-stub
message="v%{version} published %{timeAgo}"
/>
</div>
</div>
</div>
<div
class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"
>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<metadata-item-stub
data-testid="package-type"
icon="package"
link=""
size="s"
text="maven"
/>
</div>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<metadata-item-stub
data-testid="package-size"
icon="disk"
link=""
size="s"
text="300 bytes"
/>
</div>
</div>
</div>
<div
class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"
>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<metadata-item-stub
data-testid="package-type"
icon="package"
link=""
size="s"
text="maven"
/>
</div>
<div
class="gl-display-flex gl-align-items-center gl-mr-5"
>
<metadata-item-stub
data-testid="package-size"
icon="disk"
link=""
size="s"
text="300 bytes"
/>
</div>
</div>
<!---->
</div>
<!---->
<p />
</div>
`;

View file

@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import { GlSprintf, GlLink } from '@gitlab/ui';
import { GlSprintf } from '@gitlab/ui';
import Component from '~/registry/explorer/components/list_page/registry_header.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import {
@ -19,12 +19,8 @@ describe('registry_header', () => {
const findTitleArea = () => wrapper.find(TitleArea);
const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]');
const findInfoArea = () => wrapper.find('[data-testid="info-area"]');
const findIntroText = () => wrapper.find('[data-testid="default-intro"]');
const findImagesCountSubHeader = () => wrapper.find('[data-testid="images-count"]');
const findExpirationPolicySubHeader = () => wrapper.find('[data-testid="expiration-policy"]');
const findDisabledExpirationPolicyMessage = () =>
wrapper.find('[data-testid="expiration-disabled-message"]');
const mountComponent = (propsData, slots) => {
wrapper = shallowMount(Component, {
@ -123,44 +119,18 @@ describe('registry_header', () => {
});
});
describe('info area', () => {
it('exists', () => {
mountComponent();
expect(findInfoArea().exists()).toBe(true);
});
describe('info messages', () => {
describe('default message', () => {
beforeEach(() => {
return mountComponent({ helpPagePath: 'bar' });
});
it('is correctly bound to title_area props', () => {
mountComponent({ helpPagePath: 'foo' });
it('exists', () => {
expect(findIntroText().exists()).toBe(true);
});
it('has the correct copy', () => {
expect(findIntroText().text()).toMatchInterpolatedText(LIST_INTRO_TEXT);
});
it('has the correct link', () => {
expect(
findIntroText()
.find(GlLink)
.attributes('href'),
).toBe('bar');
expect(findTitleArea().props('infoMessages')).toEqual([
{ text: LIST_INTRO_TEXT, link: 'foo' },
]);
});
});
describe('expiration policy info message', () => {
describe('when there are no images', () => {
it('is hidden', () => {
mountComponent();
expect(findDisabledExpirationPolicyMessage().exists()).toBe(false);
});
});
describe('when there are images', () => {
describe('when expiration policy is disabled', () => {
beforeEach(() => {
@ -170,43 +140,27 @@ describe('registry_header', () => {
imagesCount: 1,
});
});
it('message exist', () => {
expect(findDisabledExpirationPolicyMessage().exists()).toBe(true);
});
it('has the correct copy', () => {
expect(findDisabledExpirationPolicyMessage().text()).toMatchInterpolatedText(
EXPIRATION_POLICY_DISABLED_MESSAGE,
);
});
it('has the correct link', () => {
expect(
findDisabledExpirationPolicyMessage()
.find(GlLink)
.attributes('href'),
).toBe('foo');
it('the prop is correctly bound', () => {
expect(findTitleArea().props('infoMessages')).toEqual([
{ text: LIST_INTRO_TEXT, link: '' },
{ text: EXPIRATION_POLICY_DISABLED_MESSAGE, link: 'foo' },
]);
});
});
describe('when expiration policy is enabled', () => {
describe.each`
desc | props
${'when there are no images'} | ${{ expirationPolicy: { enabled: false }, imagesCount: 0 }}
${'when expiration policy is enabled'} | ${{ expirationPolicy: { enabled: true }, imagesCount: 1 }}
${'when the expiration policy is completely disabled'} | ${{ expirationPolicy: { enabled: false }, imagesCount: 1, hideExpirationPolicyData: true }}
`('$desc', ({ props }) => {
it('message does not exist', () => {
mountComponent({
expirationPolicy: { enabled: true },
imagesCount: 1,
});
mountComponent(props);
expect(findDisabledExpirationPolicyMessage().exists()).toBe(false);
});
});
describe('when the expiration policy is completely disabled', () => {
it('message does not exist', () => {
mountComponent({
expirationPolicy: { enabled: true },
imagesCount: 1,
hideExpirationPolicyData: true,
});
expect(findDisabledExpirationPolicyMessage().exists()).toBe(false);
expect(findTitleArea().props('infoMessages')).toEqual([
{ text: LIST_INTRO_TEXT, link: '' },
]);
});
});
});

View file

@ -1,4 +1,4 @@
import { GlAvatar } from '@gitlab/ui';
import { GlAvatar, GlSprintf, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import component from '~/vue_shared/components/registry/title_area.vue';
@ -10,10 +10,12 @@ describe('title area', () => {
const findMetadataSlot = name => wrapper.find(`[data-testid="${name}"]`);
const findTitle = () => wrapper.find('[data-testid="title"]');
const findAvatar = () => wrapper.find(GlAvatar);
const findInfoMessages = () => wrapper.findAll('[data-testid="info-message"]');
const mountComponent = ({ propsData = { title: 'foo' }, slots } = {}) => {
wrapper = shallowMount(component, {
propsData,
stubs: { GlSprintf },
slots: {
'sub-header': '<div data-testid="sub-header" />',
'right-actions': '<div data-testid="right-actions" />',
@ -95,4 +97,33 @@ describe('title area', () => {
});
});
});
describe('info-messages', () => {
it('shows a message when the props contains one', () => {
mountComponent({ propsData: { infoMessages: [{ text: 'foo foo bar bar' }] } });
const messages = findInfoMessages();
expect(messages).toHaveLength(1);
expect(messages.at(0).text()).toBe('foo foo bar bar');
});
it('shows a link when the props contains one', () => {
mountComponent({
propsData: {
infoMessages: [{ text: 'foo %{docLinkStart}link%{docLinkEnd}', link: 'bar' }],
},
});
const message = findInfoMessages().at(0);
expect(message.find(GlLink).attributes('href')).toBe('bar');
expect(message.text()).toBe('foo link');
});
it('multiple messages generates multiple spans', () => {
mountComponent({ propsData: { infoMessages: [{ text: 'foo' }, { text: 'bar' }] } });
expect(findInfoMessages()).toHaveLength(2);
});
});
});

View file

@ -124,6 +124,7 @@ RSpec.configure do |config|
config.include LoginHelpers, type: :feature
config.include SearchHelpers, type: :feature
config.include WaitHelpers, type: :feature
config.include WaitForRequests, type: :feature
config.include EmailHelpers, :mailer, type: :mailer
config.include Warden::Test::Helpers, type: :request
config.include Gitlab::Routing, type: :routing
@ -133,7 +134,6 @@ RSpec.configure do |config|
config.include InputHelper, :js
config.include SelectionHelper, :js
config.include InspectRequests, :js
config.include WaitForRequests, :js
config.include LiveDebugger, :js
config.include MigrationsHelpers, :migration
config.include RedisHelpers

View file

@ -1,6 +1,14 @@
# frozen_string_literal: true
module SearchHelpers
def fill_in_search(text)
page.within('.search-input-wrap') do
fill_in('search', with: text)
end
wait_for_all_requests
end
def submit_search(query, scope: nil)
page.within('.search-form, .search-page-form') do
field = find_field('search')
@ -11,6 +19,8 @@ module SearchHelpers
else
click_button('Search')
end
wait_for_all_requests
end
end

View file

@ -2,9 +2,28 @@
RSpec.shared_examples 'milestone tabs' do
def go(path, extra_params = {})
params = { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid }
get path, params: request_params.merge(extra_params)
end
get path, params: params.merge(extra_params)
describe '#issues' do
context 'as html' do
before do
go(:issues, format: 'html')
end
it 'redirects to milestone#show' do
expect(response).to redirect_to(milestone_path)
end
end
context 'as json' do
it 'renders the issues tab template to a string' do
go(:issues, format: 'json')
expect(response).to render_template('shared/milestones/_issues_tab')
expect(json_response).to have_key('html')
end
end
end
describe '#merge_requests' do

View file

@ -1,38 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'projects/merge_requests/diffs/_diffs.html.haml' do
include Devise::Test::ControllerHelpers
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project, author: user) }
before do
allow(view).to receive(:url_for).and_return(controller.request.fullpath)
assign(:merge_request, merge_request)
assign(:environment, merge_request.environments_for(user).last)
assign(:diffs, merge_request.diffs)
assign(:merge_request_diffs, merge_request.diffs)
assign(:diff_notes_disabled, true) # disable note creation
assign(:use_legacy_diff_notes, false)
assign(:grouped_diff_discussions, {})
assign(:notes, [])
end
context 'for a commit' do
let(:commit) { merge_request.commits.last }
before do
assign(:commit, commit)
end
it "shows the commit scope" do
render
expect(rendered).to have_content "Only comments from the following commit are shown below"
end
end
end

View file

@ -6,8 +6,7 @@ RSpec.describe 'shared/milestones/_issuables.html.haml' do
let(:issuables_size) { 100 }
before do
allow(view).to receive_messages(title: nil, id: nil, show_project_name: nil,
show_full_project_name: nil, dom_class: '',
allow(view).to receive_messages(title: nil, id: nil, show_project_name: nil, dom_class: '',
issuables: double(length: issuables_size).as_null_object)
stub_template 'shared/milestones/_issuable.html.haml' => ''