Commit ad0265ee authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 4dfc8711
......@@ -18,7 +18,7 @@ export default {
computed: {
...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
...mapCommitState(['commitAction']),
...mapGetters(['currentBranch']),
...mapGetters(['currentBranch', 'emptyRepo', 'canPushToBranch']),
commitToCurrentBranchText() {
return sprintf(
s__('IDE|Commit to %{branchName} branch'),
......@@ -29,6 +29,13 @@ export default {
containsStagedChanges() {
return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
},
shouldDefaultToCurrentBranch() {
if (this.emptyRepo) {
return true;
}
return this.canPushToBranch && !this.currentBranch?.default;
},
},
watch: {
containsStagedChanges() {
......@@ -43,13 +50,11 @@ export default {
methods: {
...mapCommitActions(['updateCommitAction']),
updateSelectedCommitAction() {
if (!this.currentBranch) {
if (!this.currentBranch && !this.emptyRepo) {
return;
}
const { can_push: canPush = false, default: isDefault = false } = this.currentBranch;
if (canPush && !isDefault) {
if (this.shouldDefaultToCurrentBranch) {
this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
} else {
this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH);
......@@ -68,7 +73,7 @@ export default {
<div class="append-bottom-15 ide-commit-options">
<radio-group
:value="$options.commitToCurrentBranch"
:disabled="currentBranch && !currentBranch.can_push"
:disabled="!canPushToBranch"
:title="$options.currentBranchPermissionsTooltip"
>
<span
......@@ -77,11 +82,13 @@ export default {
v-html="commitToCurrentBranchText"
></span>
</radio-group>
<radio-group
:value="$options.commitToNewBranch"
:label="__('Create a new branch')"
:show-input="true"
/>
<new-merge-request-option />
<template v-if="!emptyRepo">
<radio-group
:value="$options.commitToNewBranch"
:label="__('Create a new branch')"
:show-input="true"
/>
<new-merge-request-option />
</template>
</div>
</template>
......@@ -10,6 +10,7 @@ export const FILE_VIEW_MODE_PREVIEW = 'preview';
export const PERMISSION_CREATE_MR = 'createMergeRequestIn';
export const PERMISSION_READ_MR = 'readMergeRequest';
export const PERMISSION_PUSH_CODE = 'pushCode';
export const viewerTypes = {
mr: 'mrdiff',
......
......@@ -2,7 +2,8 @@ query getUserPermissions($projectPath: ID!) {
project(fullPath: $projectPath) {
userPermissions {
createMergeRequestIn,
readMergeRequest
readMergeRequest,
pushCode
}
}
}
......@@ -83,10 +83,14 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => {
});
};
export const showEmptyState = ({ commit, state, dispatch }, { projectId, branchId }) => {
export const loadEmptyBranch = ({ commit, state }, { projectId, branchId }) => {
const treePath = `${projectId}/${branchId}`;
const currentTree = state.trees[`${projectId}/${branchId}`];
dispatch('setCurrentBranchId', branchId);
// If we already have a tree, let's not recreate an empty one
if (currentTree) {
return;
}
commit(types.CREATE_TREE, { treePath });
commit(types.TOGGLE_LOADING, {
......@@ -114,8 +118,16 @@ export const loadFile = ({ dispatch, state }, { basePath }) => {
}
};
export const loadBranch = ({ dispatch, getters }, { projectId, branchId }) =>
dispatch('getBranchData', {
export const loadBranch = ({ dispatch, getters, state }, { projectId, branchId }) => {
const currentProject = state.projects[projectId];
if (currentProject?.branches?.[branchId]) {
return Promise.resolve();
} else if (getters.emptyRepo) {
return dispatch('loadEmptyBranch', { projectId, branchId });
}
return dispatch('getBranchData', {
projectId,
branchId,
})
......@@ -137,29 +149,23 @@ export const loadBranch = ({ dispatch, getters }, { projectId, branchId }) =>
dispatch('showBranchNotFoundError', branchId);
throw err;
});
};
export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, basePath }) => {
const currentProject = state.projects[projectId];
if (getters.emptyRepo) {
return dispatch('showEmptyState', { projectId, branchId });
}
if (!currentProject || !currentProject.branches[branchId]) {
dispatch('setCurrentBranchId', branchId);
return dispatch('loadBranch', { projectId, branchId })
.then(() => dispatch('loadFile', { basePath }))
.catch(
() =>
new Error(
sprintf(
__('An error occurred while getting files for - %{branchId}'),
{
branchId: `<strong>${esc(projectId)}/${esc(branchId)}</strong>`,
},
false,
),
export const openBranch = ({ dispatch }, { projectId, branchId, basePath }) => {
dispatch('setCurrentBranchId', branchId);
return dispatch('loadBranch', { projectId, branchId })
.then(() => dispatch('loadFile', { basePath }))
.catch(
() =>
new Error(
sprintf(
__('An error occurred while getting files for - %{branchId}'),
{
branchId: `<strong>${esc(projectId)}/${esc(branchId)}</strong>`,
},
false,
),
);
}
return Promise.resolve(dispatch('loadFile', { basePath }));
),
);
};
......@@ -4,6 +4,7 @@ import {
packageJsonPath,
PERMISSION_READ_MR,
PERMISSION_CREATE_MR,
PERMISSION_PUSH_CODE,
} from '../constants';
export const activeFile = state => state.openFiles.find(file => file.active) || null;
......@@ -120,8 +121,9 @@ export const packageJson = state => state.entries[packageJsonPath];
export const isOnDefaultBranch = (_state, getters) =>
getters.currentProject && getters.currentProject.default_branch === getters.branchName;
export const canPushToBranch = (_state, getters) =>
getters.currentBranch && getters.currentBranch.can_push;
export const canPushToBranch = (_state, getters) => {
return Boolean(getters.currentBranch ? getters.currentBranch.can_push : getters.canPushCode);
};
export const isFileDeletedAndReadded = (state, getters) => path => {
const stagedFile = getters.getStagedFile(path);
......@@ -157,5 +159,8 @@ export const canReadMergeRequests = (state, getters) =>
export const canCreateMergeRequests = (state, getters) =>
Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_CREATE_MR]);
export const canPushCode = (state, getters) =>
Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_PUSH_CODE]);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -106,6 +106,9 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState, rootGetter
};
export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => {
// Pull commit options out because they could change
// During some of the pre and post commit processing
const { shouldCreateMR, isCreatingNewBranch, branchName } = getters;
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
const stageFilesPromise = rootState.stagedFiles.length
? Promise.resolve()
......@@ -116,7 +119,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
return stageFilesPromise
.then(() => {
const payload = createCommitPayload({
branch: getters.branchName,
branch: branchName,
newBranch,
getters,
state,
......@@ -149,7 +152,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
dispatch('updateCommitMessage', '');
return dispatch('updateFilesAfterCommit', {
data,
branch: getters.branchName,
branch: branchName,
})
.then(() => {
commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
......@@ -158,15 +161,15 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 5000);
if (getters.shouldCreateMR) {
if (shouldCreateMR) {
const { currentProject } = rootGetters;
const targetBranch = getters.isCreatingNewBranch
const targetBranch = isCreatingNewBranch
? rootState.currentBranchId
: currentProject.default_branch;
dispatch(
'redirectToUrl',
createNewMergeRequestUrl(currentProject.web_url, getters.branchName, targetBranch),
createNewMergeRequestUrl(currentProject.web_url, branchName, targetBranch),
{ root: true },
);
}
......@@ -194,7 +197,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
if (rootGetters.activeFile) {
router.push(
`/project/${rootState.currentProjectId}/blob/${getters.branchName}/-/${rootGetters.activeFile.path}`,
`/project/${rootState.currentProjectId}/blob/${branchName}/-/${rootGetters.activeFile.path}`,
);
}
}
......
......@@ -55,7 +55,7 @@ export const shouldHideNewMrOption = (_state, getters, _rootState, rootGetters)
rootGetters.canPushToBranch;
export const shouldDisableNewMrOption = (state, getters, rootState, rootGetters) =>
!rootGetters.canCreateMergeRequests;
!rootGetters.canCreateMergeRequests || rootGetters.emptyRepo;
export const shouldCreateMR = (state, getters) =>
state.shouldCreateMR && !getters.shouldDisableNewMrOption;
......
......@@ -22,7 +22,6 @@ export default class SSHMirror {
this.$dropdownAuthType = this.$form.find('.js-mirror-auth-type');
this.$hiddenAuthType = this.$form.find('.js-hidden-mirror-auth-type');
this.$wellAuthTypeChanging = this.$form.find('.js-well-changing-auth');
this.$wellPasswordAuth = this.$form.find('.js-well-password-auth');
}
......
import { graphTypes, symbolSizes } from '../../constants';
import { graphTypes, symbolSizes, colorValues } from '../../constants';
/**
* Annotations and deployments are decoration layers on
......@@ -40,33 +40,50 @@ export const annotationsYAxis = {
formatter: () => {},
},
};
/**
* This util method check if a particular series data point
* is of annotation type. Annotations are generally scatter
* plot charts
* Fetched list of annotations are parsed into a
* format the eCharts accepts to draw markLines
*
* If Annotation is a single line, the `from` property
* has a value and the `to` is null. Because annotations
* only supports lines the from value does not exist yet.
*
*
* @param {String} type series component type
* @returns {Boolean}
* @param {Object} annotation object
* @returns {Object} markLine object
*/
export const isAnnotation = type => type === graphTypes.annotationsData;
export const parseAnnotations = ({
from: annotationFrom = '',
color = colorValues.primaryColor,
}) => ({
xAxis: annotationFrom,
lineStyle: {
color,
},
});
/**
* This method currently supports only deployments. After
* https://gitlab.com/gitlab-org/gitlab/-/issues/211418 annotations
* support will be added in this method.
* This method currently generates deployments and annotations
* but are not used in the chart. The method calling
* generateAnnotationsSeries will not pass annotations until
* https://gitlab.com/gitlab-org/gitlab/-/issues/211330 is
* implemented.
*
* This method is extracted out of the charts so that
* annotation lines can be easily supported in
* the future.
*
* In order to make hover work, hidden annotation data points
* are created along with the markLines. These data points have
* the necessart metadata that is used to display in the tooltip.
*
* @param {Array} deployments deployments data
* @returns {Object} annotation series object
*/
export const generateAnnotationsSeries = (deployments = []) => {
if (!deployments.length) {
return [];
}
const data = deployments.map(deployment => {
export const generateAnnotationsSeries = ({ deployments = [], annotations = [] } = {}) => {
// deployment data points
const deploymentsData = deployments.map(deployment => {
return {
name: 'deployments',
value: [deployment.createdAt, annotationsYAxisCoords.pos],
......@@ -78,9 +95,27 @@ export const generateAnnotationsSeries = (deployments = []) => {
};
});
// annotation data points
const annotationsData = annotations.map(annotation => {
return {
name: 'annotations',
value: [annotation.from, annotationsYAxisCoords.pos],
symbol: 'none',
description: annotation.description,
};
});
// annotation markLine option
const markLine = {
symbol: 'none',
silent: true,
data: annotations.map(parseAnnotations),
};
return {
type: graphTypes.annotationsData,
yAxisIndex: 1, // annotationsYAxis index
data,
data: [...deploymentsData, ...annotationsData],
markLine,
};
};
......@@ -6,9 +6,9 @@ import dateFormat from 'dateformat';
import { s__, __ } from '~/locale';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import Icon from '~/vue_shared/components/icon.vue';
import { chartHeight, lineTypes, lineWidths, dateFormats } from '../../constants';
import { chartHeight, lineTypes, lineWidths, dateFormats, tooltipTypes } from '../../constants';
import { getYAxisOptions, getChartGrid, getTooltipFormatter } from './options';
import { annotationsYAxis, generateAnnotationsSeries, isAnnotation } from './annotations';
import { annotationsYAxis, generateAnnotationsSeries } from './annotations';
import { makeDataSeries } from '~/helpers/monitor_helper';
import { graphDataValidatorForValues } from '../../utils';
......@@ -20,6 +20,7 @@ const events = {
};
export default {
tooltipTypes,
components: {
GlAreaChart,
GlLineChart,
......@@ -88,10 +89,10 @@ export default {
data() {
return {
tooltip: {
type: '',
title: '',
content: [],
commitUrl: '',
isDeployment: false,
sha: '',
},
width: 0,
......@@ -137,7 +138,13 @@ export default {
}, []);
},
chartOptionSeries() {
return (this.option.series || []).concat(generateAnnotationsSeries(this.recentDeployments));
// After https://gitlab.com/gitlab-org/gitlab/-/issues/211330 is implemented,
// this method will have access to annotations data
return (this.option.series || []).concat(
generateAnnotationsSeries({
deployments: this.recentDeployments,
}),
);
},
chartOptions() {
const { yAxis, xAxis } = this.option;
......@@ -246,6 +253,9 @@ export default {
formatLegendLabel(query) {
return `${query.label}`;
},
isTooltipOfType(tooltipType, defaultType) {
return tooltipType === defaultType;
},
formatTooltipText(params) {
this.tooltip.title = dateFormat(params.value, dateFormats.default);
this.tooltip.content = [];
......@@ -253,13 +263,18 @@ export default {
params.seriesData.forEach(dataPoint => {
if (dataPoint.value) {
const [xVal, yVal] = dataPoint.value;
this.tooltip.isDeployment = isAnnotation(dataPoint.componentSubType);
if (this.tooltip.isDeployment) {
this.tooltip.type = dataPoint.name;
if (this.isTooltipOfType(this.tooltip.type, this.$options.tooltipTypes.deployments)) {
const [deploy] = this.recentDeployments.filter(
deployment => deployment.createdAt === xVal,
);
this.tooltip.sha = deploy.sha.substring(0, 8);
this.tooltip.commitUrl = deploy.commitUrl;
} else if (
this.isTooltipOfType(this.tooltip.type, this.$options.tooltipTypes.annotations)
) {
const { data } = dataPoint;
this.tooltip.content.push(data?.description);
} else {
const { seriesName, color, dataIndex } = dataPoint;
......@@ -288,7 +303,6 @@ export default {
onChartUpdated(eChart) {
[this.primaryColor] = eChart.getOption().color;
},
onChartCreated(eChart) {
// Emit a datazoom event that corresponds to the eChart
// `datazoom` event.
......@@ -346,7 +360,7 @@ export default {
@created="onChartCreated"
@updated="onChartUpdated"
>
<template v-if="tooltip.isDeployment">
<template v-if="isTooltipOfType(tooltip.type, this.$options.tooltipTypes.deployments)">
<template slot="tooltipTitle">
{{ __('Deployed') }}
</template>
......@@ -355,29 +369,35 @@ export default {
<gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
</div>
</template>
<template v-else-if="isTooltipOfType(tooltip.type, this.$options.tooltipTypes.annotations)">
<template slot="tooltipTitle">
<div class="text-nowrap">
{{ tooltip.title }}
</div>
</template>
<div slot="tooltipContent" class="d-flex align-items-center">
{{ tooltip.content.join('\n') }}
</div>
</template>
<template v-else>
<template slot="tooltipTitle">
<slot name="tooltipTitle">
<div class="text-nowrap">
{{ tooltip.title }}
</div>
</slot>
<div class="text-nowrap">
{{ tooltip.title }}
</div>
</template>
<template slot="tooltipContent">
<slot name="tooltipContent" :tooltip="tooltip">
<div
v-for="(content, key) in tooltip.content"
:key="key"
class="d-flex justify-content-between"
>
<gl-chart-series-label :color="isMultiSeries ? content.color : ''">
{{ content.name }}
</gl-chart-series-label>
<div class="prepend-left-32">
{{ content.value }}
</div>
<template slot="tooltipContent" :tooltip="tooltip">
<div
v-for="(content, key) in tooltip.content"
:key="key"
class="d-flex justify-content-between"
>
<gl-chart-series-label :color="isMultiSeries ? content.color : ''">
{{ content.name }}
</gl-chart-series-label>
<div class="prepend-left-32">
{{ content.value }}
</div>
</slot>
</div>
</template>
</template>
</component>
......
......@@ -115,3 +115,12 @@ export const NOT_IN_DB_PREFIX = 'NO_DB';
* Used as a value for the 'states' query filter
*/
export const ENVIRONMENT_AVAILABLE_STATE = 'available';
/**
* Time series charts have different types of
* tooltip based on the hovered data point.
*/
export const tooltipTypes = {
deployments: 'deployments',
annotations: 'annotations',
};
......@@ -81,4 +81,8 @@ export default {
text: s__('ProjectTemplates|Serverless Framework/JS'),
icon: '.template-option .icon-serverless_framework',
},
cluster_management: {
text: s__('ProjectTemplates|GitLab Cluster Management'),
icon: '.template-option .icon-cluster_management',
},
};
......@@ -15,6 +15,14 @@ $item-weight-max-width: 48px;
max-width: 85%;
}
.related-items-tree {
.card-header {
.gl-label {
line-height: $gl-line-height;
}
}
}
.item-body {
position: relative;
line-height: $gl-line-height;
......@@ -49,6 +57,10 @@ $item-weight-max-width: 48px;
color: $orange-600;
}
.item-title-wrapper {
max-width: 100%;
}
.item-title {
flex-basis: 100%;
font-size: $gl-font-size-small;
......@@ -72,15 +84,62 @@ $item-weight-max-width: 48px;
overflow: hidden;
white-space: nowrap;
}
@include media-breakpoint-down(lg) {
.issue-count-badge {
padding-left: 0;
}
}
}
.item-body,
.card-header {
.health-label-short {
display: initial;
max-width: 0;
}
.health-label-long {
display: none;
}
.status {
&-at-risk {
color: $red-500;
background-color: $red-100;
}
&-needs-attention {
color: $orange-700;
background-color: $orange-100;
}
&-on-track {
color: $green-600;
background-color: $green-100;
}
}
.gl-label-text {
font-weight: $gl-font-weight-bold;
}
.bullet-separator {
font-size: 9px;
color: $gray-400;
}
}
.item-meta {
flex-basis: 100%;
font-size: $gl-font-size-small;
font-size: $gl-font-size;
color: $gl-text-color-secondary;
.item-meta-child {
flex-basis: 100%;
.item-due-date,
.board-card-weight {
&.board-card-info {
margin-right: 0;
}
}
.item-attributes-area {
......@@ -88,10 +147,6 @@ $item-weight-max-width: 48px;
margin-left: 8px;
}
.board-card-info {
margin-right: 0;
}
@include media-breakpoint-down(sm) {
margin-left: -8px;
}
......@@ -107,13 +162,21 @@ $item-weight-max-width: 48px;
max-width: $item-milestone-max-width;
.ic-clock {
color: $gl-text-color-tertiary;
color: $gl-text-color-secondary;
margin-right: $gl-padding-4;
}
}
.item-weight {
max-width: $item-weight-max-width;
.ic-weight {
color: $gl-text-color-secondary;
}
}
.item-due-date .ic-calendar {
color: $gl-text-color-secondary;
}
}
......@@ -194,6 +257,13 @@ $item-weight-max-width: 48px;
.sortable-link {
max-width: 90%;
}
.item-body,
.card-header {
.health-label-short {
max-width: 30px;
}
}
}
/* Small devices (landscape phones, 768px and up) */
......@@ -232,6 +302,13 @@ $item-weight-max-width: 48px;
}
}
}
.item-body,
.card-header {
.health-label-short {
max-width: 60px;
}
}
}
/* Medium devices (desktops, 992px and up) */
......@@ -245,6 +322,17 @@ $item-weight-max-width: 48px;
font-size: inherit; // Base size given to `item-meta` is `$gl-font-size-small`
}
}
.item-body,
.card-header {
.health-label-short {
max-width: 100px;
}
}
.health-label-long {
display: none;
}
}
/* Large devices (large desktops, 1200px and up) */
......@@ -264,11 +352,23 @@ $item-weight-max-width: 48px;
}
}
.item-title-wrapper {
max-width: calc(100% - 440px);
}
.item-info-area {
flex-basis: auto;
}
}
.health-label-short {
display: initial;
}
.health-label-long {
display: none;
}
.item-contents {
overflow: hidden;
}
......@@ -306,3 +406,20 @@ $item-weight-max-width: 48px;
line-height: 1.3;
}
}
@media only screen and (min-width: 1400px) {
.card-header,
.item-body {
.health-label-short {
display: none;
}
.health-label-long {
display: initial;
}
}
.item-body .item-title-wrapper {
max-width: calc(100% - 570px);
}
}
......@@ -144,4 +144,15 @@ class GitlabSchema < GraphQL::Schema
end
end
GitlabSchema.prepend_if_ee('EE::GitlabSchema')
GitlabSchema.prepend_if_ee('EE::GitlabSchema') # rubocop: disable Cop/InjectEnterpriseEditionModule
# Force the schema to load as a workaround for intermittent errors we
# see due to a lack of thread safety.
#
# TODO: We can remove this workaround when we convert the schema to use
# the new query interpreter runtime.
#
# See:
# - https://gitlab.com/gitlab-org/gitlab/-/issues/211478
# - https://gitlab.com/gitlab-org/gitlab/-/issues/210556
GitlabSchema.graphql_definition
......@@ -5,7 +5,7 @@
.mobile-overlay
.alert-wrapper
= render 'shared/outdated_browser'
= render_if_exists "layouts/header/ee_license_banner"
= render_if_exists "layouts/header/ee_subscribable_banner"
= render "layouts/broadcast"
= render "layouts/header/read_only_banner"
= render "layouts/nav/classification_level_banner"
......
- navbar_links = links.sort_by(&:title)
- all_paths = navbar_links.map(&:path)
- analytics_link = navbar_links.find { |link| link.title == _('Value Stream') } || navbar_links.first
- if navbar_links.any?
= nav_link(path: all_paths) do
= link_to navbar_links.first.link do
= link_to analytics_link.link, { data: { qa_selector: 'analytics_anchor' } } do
.nav-icon-container
= sprite_icon('chart')
%span.nav-item-name{ data: { qa_selector: 'analytics_link' } }
= _('Analytics')
%ul.sidebar-sub-level-items{ data: { qa_selector: 'analytics_sidebar_submenu' } }
= nav_link(path: navbar_links.first.path, html_options: { class: "fly-out-top-item" } ) do
= link_to navbar_links.first.link do
= nav_link(path: analytics_link.path, html_options: { class: "fly-out-top-item" } ) do
= link_to analytics_link.link do
%strong.fly-out-top-item-name
= _('Analytics')
%li.divider.fly-out-top-item
......
......@@ -26,7 +26,7 @@
%ul.nav.navbar-nav
%li.header-user.dropdown
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
= image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar", data: { qa_selector: 'user_avatar' }
= sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right
= render 'layouts/header/current_user_dropdown'
......
......@@ -8,4 +8,5 @@
- unless project.empty_repo?
= render 'shared/auto_devops_implicitly_enabled_banner', project: project
= render_if_exists 'projects/above_size_limit_warning', project: project
= render_if_exists "layouts/header/ee_subscribable_banner", subscription: true
= render_if_exists 'shared/shared_runners_minutes_limit', project: project, classes: [container_class, ("limit-container-width" unless fluid_layout)]
......@@ -11,8 +11,6 @@
= f.hidden_field :auth_method, value: "password", class: "js-hidden-mirror-auth-type"
.form-group
.collapse.js-well-changing-auth
.changing-auth-method
.well-password-auth.collapse.js-well-password-auth
= f.label :password, _("Password"), class: "label-bold"
= f.password_field :password, value: mirror.password, class: 'form-control qa-password', autocomplete: 'new-password'
- redirect_params = { redirect: @redirect } if @redirect
.card-body.rendered-terms
.card-body.rendered-terms{ data: { qa_selector: 'terms_content' } }
= markdown_field(@term, :terms)
- if current_user
.card-footer.footer-block.clearfix
- if can?(current_user, :accept_terms, @term)
.float-right
= button_to accept_term_path(@term, redirect_params), class: 'btn btn-success prepend-left-8' do
= button_to accept_term_path(@term, redirect_params), class: 'btn btn-success prepend-left-8', data: { qa_selector: 'accept_terms_button' } do
= _('Accept terms')
- else
.pull-right
......
---
title: Fix some Web IDE bugs with empty projects
merge_request: 25463
author:
type: fixed
---
title: Add cluster management project template
merge_request: 25318
author:
type: added
---
title: Ensure members are always added on Project Import when importing as admin
merge_request: 29046
author:
type: fixed
---
title: Update GitLab Shell to v12.1.0
merge_request: 29167
author:
type: other
......@@ -28,7 +28,7 @@ graph TD
## Use cases
- Suppose your team is working on a large feature that involves multiple discussions throughout different issues created in distinct projects within a [Group](../index.md). With Epics, you can track all the related activities that together contribute to that single feature.
- Track when the work for the group of issues is targeted to begin, and when it is targeted to end.
- Track when the work for the group of issues is targeted to begin, and when it's targeted to end.
- Discuss and collaborate on feature ideas and scope at a high level.
![epics list view](img/epics_list_view_v12.5.png)
......@@ -62,7 +62,7 @@ An epic's page contains the following tabs:
## Adding an issue to an epic
You can add an existing issue to an epic, or, from an epic's page, create a new issue that is automatically added to the epic.
You can add an existing issue to an epic, or, from an epic's page, create a new issue that's automatically added to the epic.
### Adding an existing issue to an epic
......@@ -70,7 +70,7 @@ Existing issues that belong to a project in an epic's group, or any of the epic'
subgroups, are eligible to be added to the epic. Newly added issues appear at the top of the list of issues in the **Epics and Issues** tab.
An epic contains a list of issues and an issue can be associated with at most
one epic. When you add an issue that is already linked to an epic,
one epic. When you add an issue that's already linked to an epic,
the issue is automatically unlinked from its current parent.
To add an issue to an epic:
......@@ -101,6 +101,19 @@ To remove an issue from an epic:
1. Click on the <kbd>x</kbd> button in the epic's issue list.
1. Click **Remove** in the **Remove issue** warning message.
## Issue health status in Epic tree **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/199184) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10.
You can report on and quickly respond to the health of individual issues and epics by setting a
red, amber, or green [health status on an issue](../../project/issues/index.md#health-status-ultimate),
which will appear on your Epic tree.
### Disable Issue health status in Epic tree
This feature comes with a feature flag enabled by default. For steps to disable it, see
[Disable issue health status](../../project/issues/index.md#disable-issue-health-status).
## Multi-level child epics **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/8333) in GitLab Ultimate 11.7.
......@@ -108,7 +121,7 @@ To remove an issue from an epic:
Any epic that belongs to a group, or subgroup of the parent epic's group, is
eligible to be added. New child epics appear at the top of the list of epics in the **Epics and Issues** tab.
When you add an epic that is already linked to a parent epic, the link to its current parent is removed.
When you add an epic that's already linked to a parent epic, the link to its current parent is removed.
An epic can have multiple child epics with
the maximum depth being 5.
......
......@@ -52,7 +52,7 @@ must be set.
<li>State</li>
<ul>
<li>State (open or closed)</li>
<li>Status (On track, Needs attention, or At risk)</li>
<li>Health status (on track, needs attention, or at risk)</li>
<li>Confidentiality</li>
<li>Tasks (completed vs. outstanding)</li>
</ul>
......@@ -166,11 +166,12 @@ requires [GraphQL](../../../api/graphql/index.md) to be enabled.
---
### Status **(ULTIMATE)**
### Health status **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/36427) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10.
To help you track the status of your issues, you can assign a status to each issue to flag work that's progressing as planned or needs attention to keep on schedule:
To help you track the status of your issues, you can assign a status to each issue to flag work
that's progressing as planned or needs attention to keep on schedule:
- **On track** (green)
- **Needs attention** (amber)
......@@ -178,9 +179,10 @@ To help you track the status of your issues, you can assign a status to each iss
!["On track" health status on an issue](img/issue_health_status_v12_10.png)
---
You can then see issue statuses on the
[Epic tree](../../group/epics/index.md#issue-health-status-in-epic-tree-ultimate).
#### Enable issue health status
#### Disable issue health status
This feature comes with the `:save_issuable_health_status` feature flag enabled by default. However, in some cases
this feature is incompatible with old configuration. To turn off the feature while configuration is
......
......@@ -25,6 +25,11 @@ module Gitlab
@project_members = relation_reader.consume_relation(importable_path, 'project_members')
.map(&:first)
# ensure users are mapped before tree restoration
# so that even if there is no content to associate
# users with, they are still added to the project
members_mapper.map
if relation_tree_restorer.restore
import_failure_service.with_retry(action: 'set_latest_merge_request_diff_ids!') do
@project.merge_requests.set_latest_merge_request_diff_ids!
......
......@@ -56,7 +56,8 @@ module Gitlab
ProjectTemplate.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook', 'illustrations/logos/netlify.svg'),
ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg'),
ProjectTemplate.new('salesforcedx', 'SalesforceDX', _('A project boilerplate for Salesforce App development with Salesforce Developer tools.'), 'https://gitlab.com/gitlab-org/project-templates/salesforcedx'),
ProjectTemplate.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg')
ProjectTemplate.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg'),
ProjectTemplate.new('cluster_management', 'GitLab Cluster Management', _('An example project for managing Kubernetes clusters integrated with GitLab.'), 'https://gitlab.com/gitlab-org/project-templates/cluster-management')
].freeze
class << self
......
......@@ -2102,6 +2102,9 @@ msgstr ""
msgid "An error ocurred while loading your content. Please try again."
msgstr ""
msgid "An example project for managing Kubernetes clusters integrated with GitLab."
msgstr ""
msgid "An instance-level serverless domain already exists."
msgstr ""
......@@ -15895,6 +15898,9 @@ msgstr ""
msgid "ProjectTemplates|Android"
msgstr ""
msgid "ProjectTemplates|GitLab Cluster Management"
msgstr ""
msgid "ProjectTemplates|Go Micro"
msgstr ""
......@@ -23409,6 +23415,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
msgstr ""
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
......@@ -23637,6 +23646,9 @@ msgstr ""
msgid "YouTube"
msgstr ""
msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
......@@ -23861,6 +23873,9 @@ msgstr ""
msgid "assign yourself"
msgstr ""
msgid "at risk"
msgstr ""
msgid "attach a new file"
msgstr ""
......@@ -24364,6 +24379,15 @@ msgstr ""
msgid "issue"
msgstr ""
msgid "issues at risk"
msgstr ""
msgid "issues need attention"
msgstr ""
msgid "issues on track"
msgstr ""
msgid "it is stored externally"
msgstr ""
......@@ -24744,6 +24768,9 @@ msgstr ""
msgid "n/a"
msgstr ""
msgid "need attention"
msgstr ""
msgid "needs to be between 10 minutes and 1 month"
msgstr ""
......@@ -24777,6 +24804,9 @@ msgstr ""
msgid "nounSeries|%{item}, and %{lastItem}"
msgstr ""
msgid "on track"
msgstr ""
msgid "opened %{timeAgoString} by %{user}"
msgstr ""
......
......@@ -168,6 +168,7 @@ module QA
autoload :Menu, 'qa/page/main/menu'
autoload :OAuth, 'qa/page/main/oauth'
autoload :SignUp, 'qa/page/main/sign_up'
autoload :Terms, 'qa/page/main/terms'
end
module Settings
......
......@@ -10,9 +10,10 @@ module QA
sign_in(as: as, address: address)
yield
result = yield
Page::Main::Menu.perform(&:sign_out)
result
end
def while_signed_in_as_admin(address: :gitlab)
......
......@@ -14,6 +14,20 @@ module QA
ElementNotFound = Class.new(RuntimeError)
class NoRequiredElementsError < RuntimeError
def initialize(page_class)
@page_class = page_class
super
end
def to_s
<<~MSG.strip % { page: @page_class }
%{page} has no required elements.
See https://docs.gitlab.com/ee/development/testing_guide/end_to_end/dynamic_element_validation.html#required-elements
MSG
end
end
def_delegators :evaluator, :view, :views
def initialize
......@@ -250,6 +264,8 @@ module QA
end
def element_selector_css(name, *attributes)
return name.selector_css if name.is_a? Page::Element
Page::Element.new(name, *attributes).selector_css
end
......@@ -296,10 +312,24 @@ module QA
views.flat_map(&:elements)
end
def self.required_elements
elements.select(&:required?)
end
def send_keys_to_element(name, keys)
find_element(name).send_keys(keys)
end
def visible?
raise NoRequiredElementsError.new(self.class) if self.class.required_elements.empty?
self.class.required_elements.each do |required_element|
return false if has_no_element? required_element
end
true
end
class DSL
attr_reader :views
......
......@@ -159,7 +159,13 @@ module QA
fill_element :login_field, user.username
fill_element :password_field, user.password
click_element :sign_in_button, !skip_page_validation && Page::Main::Menu
click_element :sign_in_button
Page::Main::Terms.perform do |terms|
terms.accept_terms if terms.visible?
end
Page::Main::Menu.validate_elements_present! unless skip_page_validation
end
def set_initial_password_if_present
......
# frozen_string_literal: true
module QA
module Page::Main
class Terms < Page::Base
view 'app/views/layouts/terms.html.haml' do
element :user_avatar, required: true
end
view 'app/views/users/terms/index.html.haml' do
element :terms_content, required: true
element :accept_terms_button
end
def accept_terms
click_element :accept_terms_button, Page::Main::Menu
end
end
end
end
......@@ -120,10 +120,12 @@ module QA
def add_to_modified_content(content)
finished_loading?
modified_text_area.click
modified_text_area.set content
end
def modified_text_area
wait_for_animated_element(:editor_container)
within_element(:editor_container) do
find('.modified textarea.inputarea')
end
......
......@@ -10,9 +10,7 @@ module QA
base_page.wait_if_retry_later
elements.each do |element|
next unless element.required?
required_elements.each do |element|
unless base_page.has_element?(element.name, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
raise Validatable::PageValidationError, "#{element.name} did not appear on #{self.name} as expected"
end
......
......@@ -4,12 +4,13 @@ module QA
context 'Create', quarantine: { type: :new } do
describe 'Review a merge request in Web IDE' do
let(:new_file) { 'awesome_new_file.txt' }
let(:original_text) { 'Text' }
let(:review_text) { 'Reviewed ' }
let(:merge_request) do
Resource::MergeRequest.fabricate_via_api! do |mr|
mr.file_name = new_file
mr.file_content = 'Text'
mr.file_content = original_text
end
end
......
......@@ -107,4 +107,76 @@ describe QA::Page::Base do
end
end
end
context 'elements' do
subject do
Class.new(described_class) do
view 'path/to/some/view.html.haml' do
element :something, required: true
element :something_else
end
end
end
describe '#elements' do
it 'returns all elements' do
expect(subject.elements.size).to eq(2)
end
end
describe '#required_elements' do
it 'returns only required elements' do
expect(subject.required_elements.size).to eq(1)
end
end
describe '#visible?', 'Page is currently visible' do
let(:page) { subject.new }
context 'with elements' do
context 'on the page' do
before do
# required elements not there, meaning not on page
allow(page).to receive(:has_no_element?).and_return(false)
end
it 'is visible' do
expect(page).to be_visible
end
end
context 'not on the page' do
before do
# required elements are not on the page
allow(page).to receive(:has_no_element?).and_return(true)
end
it 'is not visible' do
expect(page).not_to be_visible
end
end
it 'does not raise error if page has elements' do
expect { page.visible? }.not_to raise_error
end
end
context 'no elements' do
subject do
Class.new(described_class) do
view 'path/to/some/view.html.haml' do
element :something
element :something_else
end
end
end
let(:page) { subject.new }
it 'raises error if page has no required elements' do
expect { page.visible? }.to raise_error(described_class::NoRequiredElementsError)
end
end
end
end
end
......@@ -17,7 +17,11 @@ describe('IDE commit sidebar actions', () => {
let store;
let vm;
const createComponent = ({ hasMR = false, currentBranchId = 'master' } = {}) => {
const createComponent = ({
hasMR = false,
currentBranchId = 'master',
emptyRepo = false,
} = {}) => {
const Component = Vue.extend(commitActions);
vm = createComponentWithStore(Component, store);
......@@ -27,6 +31,7 @@ describe('IDE commit sidebar actions', () => {
const proj = { ...projectData };
proj.branches[currentBranchId] = branches.find(branch => branch.name === currentBranchId);
proj.empty_repo = emptyRepo;
Vue.set(vm.$store.state.projects, 'abcproject', proj);
......@@ -52,24 +57,27 @@ describe('IDE commit sidebar actions', () => {
vm = null;
});
const findText = () => vm.$el.textContent;
const findRadios = () => Array.from(vm.$el.querySelectorAll('input[type="radio"]'));
it('renders 2 groups', () => {
createComponent();
expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2);
expect(findRadios().length).toBe(2);
});
it('renders current branch text', () => {
createComponent();
expect(vm.$el.textContent).toContain('Commit to master branch');
expect(findText()).toContain('Commit to master branch');
});
it('hides merge request option when project merge requests are disabled', done => {
createComponent({ mergeRequestsEnabled: false });
createComponent({ hasMR: false });
vm.$nextTick(() => {
expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2);
expect(vm.$el.textContent).not.toContain('Create a new branch and merge request');
expect(findRadios().length).toBe(2);
expect(findText()).not.toContain('Create a new branch and merge request');
done();
});
......@@ -119,6 +127,7 @@ describe('IDE commit sidebar actions', () => {
it.each`
input | expectedOption
${{ currentBranchId: BRANCH_DEFAULT }} | ${consts.COMMIT_TO_NEW_BRANCH}
${{ currentBranchId: BRANCH_DEFAULT, emptyRepo: true }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
${{ currentBranchId: BRANCH_PROTECTED, hasMR: true }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
${{ currentBranchId: BRANCH_PROTECTED, hasMR: false }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
${{ currentBranchId: BRANCH_PROTECTED_NO_ACCESS, hasMR: true }} | ${consts.COMMIT_TO_NEW_BRANCH}
......@@ -138,4 +147,15 @@ describe('IDE commit sidebar actions', () => {
},
);
});
describe('when empty project', () => {
beforeEach(() => {
createComponent({ emptyRepo: true });
});
it('only renders commit to current branch', () => {
expect(findRadios().length).toBe(1);
expect(findText()).toContain('Commit to master branch');
});
});
});
......@@ -280,39 +280,21 @@ describe('IDE store getters', () => {
});
describe('canPushToBranch', () => {
it('returns false when no currentBranch exists', () => {
const localGetters = {
currentProject: undefined,
};
expect(getters.canPushToBranch({}, localGetters)).toBeFalsy();
});
it('returns true when can_push to currentBranch', () => {
const localGetters = {
currentProject: {
default_branch: 'master',
},
currentBranch: {
can_push: true,
},
};
expect(getters.canPushToBranch({}, localGetters)).toBeTruthy();
});
it('returns false when !can_push to currentBranch', () => {
const localGetters = {
currentProject: {
default_branch: 'master',
},
currentBranch: {
can_push: false,
},
};
expect(getters.canPushToBranch({}, localGetters)).toBeFalsy();
});
it.each`
currentBranch | canPushCode | expectedValue
${undefined} | ${undefined} | ${false}
${{ can_push: true }} | ${false} | ${true}
${{ can_push: true }} | ${true} | ${true}
${{ can_push: false }} | ${false} | ${false}
${{ can_push: false }} | ${true} | ${false}
${undefined} | ${true} | ${true}
${undefined} | ${false} | ${false}
`(
'with currentBranch ($currentBranch) and canPushCode ($canPushCode), it is $expectedValue',
({ currentBranch, canPushCode, expectedValue }) => {
expect(getters.canPushToBranch({}, { currentBranch, canPushCode })).toBe(expectedValue);
},
);
});
describe('isFileDeletedAndReadded', () => {
......@@ -422,6 +404,7 @@ describe('IDE store getters', () => {
getterName | permissionKey
${'canReadMergeRequests'} | ${'readMergeRequest'}
${'canCreateMergeRequests'} | ${'createMergeRequestIn'}
${'canPushCode'} | ${'pushCode'}
`('$getterName', ({ getterName, permissionKey }) => {
it.each([true, false])('finds permission for current project (%s)', val => {
localState.projects[TEST_PROJECT_ID] = {
......
......@@ -292,4 +292,15 @@ describe('IDE commit module getters', () => {
expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy();
});
});
describe('shouldDisableNewMrOption', () => {
it.each`
rootGetters | expectedValue
${{ canCreateMergeRequests: false, emptyRepo: false }} | ${true}
${{ canCreateMergeRequests: true, emptyRepo: true }} | ${true}
${{ canCreateMergeRequests: true, emptyRepo: false }} | ${false}
`('with $rootGetters, it is $expectedValue', ({ rootGetters, expectedValue }) => {
expect(getters.shouldDisableNewMrOption(state, getters, {}, rootGetters)).toBe(expectedValue);
});
});
});
import { generateAnnotationsSeries } from '~/monitoring/components/charts/annotations';
import { deploymentData } from '../../mock_data';
import { deploymentData, annotationsData } from '../../mock_data';
describe('annotations spec', () => {
describe('generateAnnotationsSeries', () => {
it('default options', () => {
it('with default options', () => {
const annotations = generateAnnotationsSeries();
expect(annotations).toEqual([]);
expect(annotations).toEqual(
expect.objectContaining({
type: 'scatter',
yAxisIndex: 1,
data: [],
markLine: {
data: [],
symbol: 'none',
silent: true,
},
}),
);
});
it('with deployments', () => {
const annotations = generateAnnotationsSeries(deploymentData);
it('when only deployments data is passed', () => {
const annotations = generateAnnotationsSeries({ deployments: deploymentData });
expect(annotations).toEqual(
expect.objectContaining({
type: 'scatter',
yAxisIndex: 1,
data: expect.any(Array),
markLine: {
data: [],
symbol: 'none',
silent: true,
},
}),
);
annotations.data.forEach(annotation => {
expect(annotation).toEqual(expect.any(Object));
});
expect(annotations.data).toHaveLength(deploymentData.length);
});
it('when only annotations data is passed', () => {
const annotations = generateAnnotationsSeries({
annotations: annotationsData,
});
expect(annotations).toEqual(
expect.objectContaining({
type: 'scatter',
yAxisIndex: 1,
data: expect.any(Array),
markLine: expect.any(Object),
}),
);
annotations.markLine.data.forEach(annotation => {
expect(annotation).toEqual(expect.any(Object));
});
expect(annotations.data).toHaveLength(annotationsData.length);
expect(annotations.markLine.data).toHaveLength(annotationsData.length);
});
it('when deploments and annotations data is passed', () => {
const annotations = generateAnnotationsSeries({
deployments: deploymentData,
annotations: annotationsData,
});
expect(annotations).toEqual(
expect.objectContaining({
type: 'scatter',
yAxisIndex: 1,
data: expect.any(Array),
markLine: expect.any(Object),
}),
);
annotations.markLine.data.forEach(annotation => {
expect(annotation).toEqual(expect.any(Object));
});
expect(annotations.data).toHaveLength(deploymentData.length + annotationsData.length);
});
});
});
......@@ -169,6 +169,7 @@ describe('Time series component', () => {
componentSubType: type,
value: [mockDate, 5.55555],
dataIndex: 0,
...(type === 'scatter' && { name: 'deployments' }),
},
],
value: mockDate,
......@@ -225,6 +226,10 @@ describe('Time series component', () => {
timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter'));
});
it('set tooltip type to deployments', () => {
expect(timeSeriesChart.vm.tooltip.type).toBe('deployments');
});
it('formats tooltip title', () => {
expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM');
});
......@@ -521,7 +526,11 @@ describe('Time series component', () => {
const commitUrl = `${mockProjectDir}/-/commit/${mockSha}`;
beforeEach(done => {
timeSeriesAreaChart.vm.tooltip.isDeployment = true;
timeSeriesAreaChart.setData({
tooltip: {
type: 'deployments',
},
});
timeSeriesAreaChart.vm.$nextTick(done);
});
......
......@@ -210,6 +210,30 @@ export const deploymentData = [
},
];
export const annotationsData = [
{
id: 'gid://gitlab/Metrics::Dashboard::Annotation/1',
from: '2020-04-01T12:51:58.373Z',
to: null,
panelId: null,
description: 'This is a test annotation',
},
{
id: 'gid://gitlab/Metrics::Dashboard::Annotation/2',
description: 'test annotation 2',
from: '2020-04-02T12:51:58.373Z',
to: null,
panelId: null,
},
{
id: 'gid://gitlab/Metrics::Dashboard::Annotation/3',
description: 'test annotation 3',
from: '2020-04-04T12:51:58.373Z',
to: null,
panelId: null,
},
];
export const metricsNewGroupsAPIResponse = [
{
group: 'System metrics (Kubernetes)',
......
......@@ -4,7 +4,7 @@ import {
refreshLastCommitData,
showBranchNotFoundError,
createNewBranchFromDefault,
showEmptyState,
loadEmptyBranch,
openBranch,
loadFile,
loadBranch,
......@@ -16,6 +16,8 @@ import router from '~/ide/ide_router';
import { resetStore } from '../../helpers';
import testAction from '../../../helpers/vuex_action_helper';
const TEST_PROJECT_ID = 'abc/def';
describe('IDE store project actions', () => {
let mock;
let store;
......@@ -24,7 +26,7 @@ describe('IDE store project actions', () => {
store = createStore();
mock = new MockAdapter(axios);
store.state.projects['abc/def'] = {
store.state.projects[TEST_PROJECT_ID] = {
branches: {},
};
});
......@@ -83,7 +85,7 @@ describe('IDE store project actions', () => {
{
type: 'SET_BRANCH_COMMIT',
payload: {
projectId: 'abc/def',
projectId: TEST_PROJECT_ID,
branchId: 'master',
commit: { id: '123' },
},
......@@ -200,17 +202,17 @@ describe('IDE store project actions', () => {
});
});
describe('showEmptyState', () => {
describe('loadEmptyBranch', () => {
it('creates a blank tree and sets loading state to false', done => {
testAction(
showEmptyState,
{ projectId: 'abc/def', branchId: 'master' },
loadEmptyBranch,
{ projectId: TEST_PROJECT_ID, branchId: 'master' },
store.state,
[
{ type: 'CREATE_TREE', payload: { treePath: 'abc/def/master' } },
{ type: 'CREATE_TREE', payload: { treePath: `${TEST_PROJECT_ID}/master` } },
{
type: 'TOGGLE_LOADING',
payload: { entry: store.state.trees['abc/def/master'], forceValue: false },
payload: { entry: store.state.trees[`${TEST_PROJECT_ID}/master`], forceValue: false },
},
],
jasmine.any(Object),
......@@ -218,13 +220,15 @@ describe('IDE store project actions', () => {
);
});
it('sets the currentBranchId to the branchId that was passed', done => {
it('does nothing, if tree already exists', done => {
const trees = { [`${TEST_PROJECT_ID}/master`]: [] };
testAction(
showEmptyState,
{ projectId: 'abc/def', branchId: 'master' },
store.state,
jasmine.any(Object),
[{ type: 'setCurrentBranchId', payload: 'master' }],
loadEmptyBranch,
{ projectId: TEST_PROJECT_ID, branchId: 'master' },
{ trees },
[],
[],
done,
);
});
......@@ -278,10 +282,29 @@ describe('IDE store project actions', () => {
});
describe('loadBranch', () => {
const projectId = 'abc/def';
const projectId = TEST_PROJECT_ID;
const branchId = '123-lorem';
const ref = 'abcd2322';
it('when empty repo, loads empty branch', done => {
const mockGetters = { emptyRepo: true };
testAction(
loadBranch,
{ projectId, branchId },
{ ...store.state, ...mockGetters },
[],
[{ type: 'loadEmptyBranch', payload: { projectId, branchId } }],
done,
);
});
it('when branch already exists, does nothing', done => {
store.state.projects[projectId].branches[branchId] = {};
testAction(loadBranch, { projectId, branchId }, store.state, [], [], done);
});
it('fetches branch data', done => {
const mockGetters = { findBranch: () => ({ commit: { id: ref } }) };
spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
......@@ -317,7 +340,7 @@ describe('IDE store project actions', () => {
});
describe('openBranch', () => {
const projectId = 'abc/def';
const projectId = TEST_PROJECT_ID;
const branchId = '123-lorem';
const branch = {
......@@ -335,55 +358,6 @@ describe('IDE store project actions', () => {
});
});
it('loads file right away if the branch has already been fetched', done => {
spyOn(store, 'dispatch');
Object.assign(store.state, {
projects: {
[projectId]: {
branches: {
[branchId]: { foo: 'bar' },
},
},
},
});
openBranch(store, branch)
.then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([['loadFile', { basePath: undefined }]]);
})
.then(done)
.catch(done.fail);
});
describe('empty repo', () => {
beforeEach(() => {
spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
Object.assign(store.state, {
currentProjectId: 'abc/def',
projects: {
'abc/def': {
empty_repo: true,
},
},
});
});
afterEach(() => {
resetStore(store);
});
it('dispatches showEmptyState action right away', done => {
openBranch(store, branch)
.then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([['showEmptyState', branch]]);
done();
})
.catch(done.fail);
});
});
describe('existing branch', () => {
beforeEach(() => {
spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
......@@ -410,11 +384,17 @@ describe('IDE store project actions', () => {
it('dispatches correct branch actions', done => {
openBranch(store, branch)
.then(() => {
.then(val => {
expect(store.dispatch.calls.allArgs()).toEqual([
['setCurrentBranchId', branchId],
['loadBranch', { projectId, branchId }],
]);
expect(val).toEqual(
new Error(
`An error occurred while getting files for - <strong>${projectId}/${branchId}</strong>`,
),
);
})
.then(done)
.catch(done.fail);
......
......@@ -956,6 +956,37 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
end
end
end
context 'with project members' do
let(:user) { create(:user, :admin) }
let(:user2) { create(:user) }
let(:project_members) do
[
{
"id" => 2,
"access_level" => 40,
"source_type" => "Project",
"notification_level" => 3,
"user" => {
"id" => user2.id,
"email" => user2.email,
"username" => 'test'
}
}
]
end
let(:tree_hash) { { 'project_members' => project_members } }
before do
project.add_maintainer(user)
end
it 'restores project members' do
restorer.restore
expect(project.members.map(&:user)).to contain_exactly(user, user2)
end
end
end
context 'JSON with invalid records' do
......
......@@ -25,7 +25,8 @@ describe Gitlab::ProjectTemplate do
described_class.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook'),
described_class.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo'),
described_class.new('salesforcedx', 'SalesforceDX', _('A project boilerplate for Salesforce App development with Salesforce Developer tools.'), 'https://gitlab.com/gitlab-org/project-templates/salesforcedx'),
described_class.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg')
described_class.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg'),
described_class.new('cluster_management', 'GitLab Cluster Management', _('An example project for managing Kubernetes clusters integrated with GitLab.'), 'https://gitlab.com/gitlab-org/project-templates/cluster-management')
]
expect(described_class.all).to be_an(Array)
......
......@@ -30,6 +30,12 @@ describe BasePolicy, :do_not_mock_admin_mode do
it { is_expected.to be_allowed(:read_cross_project) }
context 'for anonymous' do
let(:current_user) { nil }
it { is_expected.to be_allowed(:read_cross_project) }
end
context 'when an external authorization service is enabled' do
before do
enable_external_authorization_service_check
......@@ -52,6 +58,12 @@ describe BasePolicy, :do_not_mock_admin_mode do
is_expected.not_to be_allowed(:read_cross_project)
end
end
context 'for anonymous' do
let(:current_user) { nil }
it { is_expected.not_to be_allowed(:read_cross_project) }
end
end
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment