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 { ...@@ -18,7 +18,7 @@ export default {
computed: { computed: {
...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']), ...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
...mapCommitState(['commitAction']), ...mapCommitState(['commitAction']),
...mapGetters(['currentBranch']), ...mapGetters(['currentBranch', 'emptyRepo', 'canPushToBranch']),
commitToCurrentBranchText() { commitToCurrentBranchText() {
return sprintf( return sprintf(
s__('IDE|Commit to %{branchName} branch'), s__('IDE|Commit to %{branchName} branch'),
...@@ -29,6 +29,13 @@ export default { ...@@ -29,6 +29,13 @@ export default {
containsStagedChanges() { containsStagedChanges() {
return this.changedFiles.length > 0 && this.stagedFiles.length > 0; return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
}, },
shouldDefaultToCurrentBranch() {
if (this.emptyRepo) {
return true;
}
return this.canPushToBranch && !this.currentBranch?.default;
},
}, },
watch: { watch: {
containsStagedChanges() { containsStagedChanges() {
...@@ -43,13 +50,11 @@ export default { ...@@ -43,13 +50,11 @@ export default {
methods: { methods: {
...mapCommitActions(['updateCommitAction']), ...mapCommitActions(['updateCommitAction']),
updateSelectedCommitAction() { updateSelectedCommitAction() {
if (!this.currentBranch) { if (!this.currentBranch && !this.emptyRepo) {
return; return;
} }
const { can_push: canPush = false, default: isDefault = false } = this.currentBranch; if (this.shouldDefaultToCurrentBranch) {
if (canPush && !isDefault) {
this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH); this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
} else { } else {
this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH); this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH);
...@@ -68,7 +73,7 @@ export default { ...@@ -68,7 +73,7 @@ export default {
<div class="append-bottom-15 ide-commit-options"> <div class="append-bottom-15 ide-commit-options">
<radio-group <radio-group
:value="$options.commitToCurrentBranch" :value="$options.commitToCurrentBranch"
:disabled="currentBranch && !currentBranch.can_push" :disabled="!canPushToBranch"
:title="$options.currentBranchPermissionsTooltip" :title="$options.currentBranchPermissionsTooltip"
> >
<span <span
...@@ -77,11 +82,13 @@ export default { ...@@ -77,11 +82,13 @@ export default {
v-html="commitToCurrentBranchText" v-html="commitToCurrentBranchText"
></span> ></span>
</radio-group> </radio-group>
<template v-if="!emptyRepo">
<radio-group <radio-group
:value="$options.commitToNewBranch" :value="$options.commitToNewBranch"
:label="__('Create a new branch')" :label="__('Create a new branch')"
:show-input="true" :show-input="true"
/> />
<new-merge-request-option /> <new-merge-request-option />
</template>
</div> </div>
</template> </template>
...@@ -10,6 +10,7 @@ export const FILE_VIEW_MODE_PREVIEW = 'preview'; ...@@ -10,6 +10,7 @@ export const FILE_VIEW_MODE_PREVIEW = 'preview';
export const PERMISSION_CREATE_MR = 'createMergeRequestIn'; export const PERMISSION_CREATE_MR = 'createMergeRequestIn';
export const PERMISSION_READ_MR = 'readMergeRequest'; export const PERMISSION_READ_MR = 'readMergeRequest';
export const PERMISSION_PUSH_CODE = 'pushCode';
export const viewerTypes = { export const viewerTypes = {
mr: 'mrdiff', mr: 'mrdiff',
......
...@@ -2,7 +2,8 @@ query getUserPermissions($projectPath: ID!) { ...@@ -2,7 +2,8 @@ query getUserPermissions($projectPath: ID!) {
project(fullPath: $projectPath) { project(fullPath: $projectPath) {
userPermissions { userPermissions {
createMergeRequestIn, createMergeRequestIn,
readMergeRequest readMergeRequest,
pushCode
} }
} }
} }
...@@ -83,10 +83,14 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => { ...@@ -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 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.CREATE_TREE, { treePath });
commit(types.TOGGLE_LOADING, { commit(types.TOGGLE_LOADING, {
...@@ -114,8 +118,16 @@ export const loadFile = ({ dispatch, state }, { basePath }) => { ...@@ -114,8 +118,16 @@ export const loadFile = ({ dispatch, state }, { basePath }) => {
} }
}; };
export const loadBranch = ({ dispatch, getters }, { projectId, branchId }) => export const loadBranch = ({ dispatch, getters, state }, { projectId, branchId }) => {
dispatch('getBranchData', { 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, projectId,
branchId, branchId,
}) })
...@@ -137,13 +149,9 @@ export const loadBranch = ({ dispatch, getters }, { projectId, branchId }) => ...@@ -137,13 +149,9 @@ export const loadBranch = ({ dispatch, getters }, { projectId, branchId }) =>
dispatch('showBranchNotFoundError', branchId); dispatch('showBranchNotFoundError', branchId);
throw err; throw err;
}); });
};
export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, basePath }) => { export const openBranch = ({ dispatch }, { projectId, branchId, basePath }) => {
const currentProject = state.projects[projectId];
if (getters.emptyRepo) {
return dispatch('showEmptyState', { projectId, branchId });
}
if (!currentProject || !currentProject.branches[branchId]) {
dispatch('setCurrentBranchId', branchId); dispatch('setCurrentBranchId', branchId);
return dispatch('loadBranch', { projectId, branchId }) return dispatch('loadBranch', { projectId, branchId })
...@@ -160,6 +168,4 @@ export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, ...@@ -160,6 +168,4 @@ export const openBranch = ({ dispatch, state, getters }, { projectId, branchId,
), ),
), ),
); );
}
return Promise.resolve(dispatch('loadFile', { basePath }));
}; };
...@@ -4,6 +4,7 @@ import { ...@@ -4,6 +4,7 @@ import {
packageJsonPath, packageJsonPath,
PERMISSION_READ_MR, PERMISSION_READ_MR,
PERMISSION_CREATE_MR, PERMISSION_CREATE_MR,
PERMISSION_PUSH_CODE,
} from '../constants'; } from '../constants';
export const activeFile = state => state.openFiles.find(file => file.active) || null; export const activeFile = state => state.openFiles.find(file => file.active) || null;
...@@ -120,8 +121,9 @@ export const packageJson = state => state.entries[packageJsonPath]; ...@@ -120,8 +121,9 @@ export const packageJson = state => state.entries[packageJsonPath];
export const isOnDefaultBranch = (_state, getters) => export const isOnDefaultBranch = (_state, getters) =>
getters.currentProject && getters.currentProject.default_branch === getters.branchName; getters.currentProject && getters.currentProject.default_branch === getters.branchName;
export const canPushToBranch = (_state, getters) => export const canPushToBranch = (_state, getters) => {
getters.currentBranch && getters.currentBranch.can_push; return Boolean(getters.currentBranch ? getters.currentBranch.can_push : getters.canPushCode);
};
export const isFileDeletedAndReadded = (state, getters) => path => { export const isFileDeletedAndReadded = (state, getters) => path => {
const stagedFile = getters.getStagedFile(path); const stagedFile = getters.getStagedFile(path);
...@@ -157,5 +159,8 @@ export const canReadMergeRequests = (state, getters) => ...@@ -157,5 +159,8 @@ export const canReadMergeRequests = (state, getters) =>
export const canCreateMergeRequests = (state, getters) => export const canCreateMergeRequests = (state, getters) =>
Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_CREATE_MR]); 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 // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -106,6 +106,9 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState, rootGetter ...@@ -106,6 +106,9 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState, rootGetter
}; };
export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => { 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 newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
const stageFilesPromise = rootState.stagedFiles.length const stageFilesPromise = rootState.stagedFiles.length
? Promise.resolve() ? Promise.resolve()
...@@ -116,7 +119,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ...@@ -116,7 +119,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
return stageFilesPromise return stageFilesPromise
.then(() => { .then(() => {
const payload = createCommitPayload({ const payload = createCommitPayload({
branch: getters.branchName, branch: branchName,
newBranch, newBranch,
getters, getters,
state, state,
...@@ -149,7 +152,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ...@@ -149,7 +152,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
dispatch('updateCommitMessage', ''); dispatch('updateCommitMessage', '');
return dispatch('updateFilesAfterCommit', { return dispatch('updateFilesAfterCommit', {
data, data,
branch: getters.branchName, branch: branchName,
}) })
.then(() => { .then(() => {
commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true }); commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
...@@ -158,15 +161,15 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ...@@ -158,15 +161,15 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true }); commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 5000); }, 5000);
if (getters.shouldCreateMR) { if (shouldCreateMR) {
const { currentProject } = rootGetters; const { currentProject } = rootGetters;
const targetBranch = getters.isCreatingNewBranch const targetBranch = isCreatingNewBranch
? rootState.currentBranchId ? rootState.currentBranchId
: currentProject.default_branch; : currentProject.default_branch;
dispatch( dispatch(
'redirectToUrl', 'redirectToUrl',
createNewMergeRequestUrl(currentProject.web_url, getters.branchName, targetBranch), createNewMergeRequestUrl(currentProject.web_url, branchName, targetBranch),
{ root: true }, { root: true },
); );
} }
...@@ -194,7 +197,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ...@@ -194,7 +197,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
if (rootGetters.activeFile) { if (rootGetters.activeFile) {
router.push( 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) ...@@ -55,7 +55,7 @@ export const shouldHideNewMrOption = (_state, getters, _rootState, rootGetters)
rootGetters.canPushToBranch; rootGetters.canPushToBranch;
export const shouldDisableNewMrOption = (state, getters, rootState, rootGetters) => export const shouldDisableNewMrOption = (state, getters, rootState, rootGetters) =>
!rootGetters.canCreateMergeRequests; !rootGetters.canCreateMergeRequests || rootGetters.emptyRepo;
export const shouldCreateMR = (state, getters) => export const shouldCreateMR = (state, getters) =>
state.shouldCreateMR && !getters.shouldDisableNewMrOption; state.shouldCreateMR && !getters.shouldDisableNewMrOption;
......
...@@ -22,7 +22,6 @@ export default class SSHMirror { ...@@ -22,7 +22,6 @@ export default class SSHMirror {
this.$dropdownAuthType = this.$form.find('.js-mirror-auth-type'); this.$dropdownAuthType = this.$form.find('.js-mirror-auth-type');
this.$hiddenAuthType = this.$form.find('.js-hidden-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'); 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 * Annotations and deployments are decoration layers on
...@@ -40,33 +40,50 @@ export const annotationsYAxis = { ...@@ -40,33 +40,50 @@ export const annotationsYAxis = {
formatter: () => {}, formatter: () => {},
}, },
}; };
/** /**
* This util method check if a particular series data point * Fetched list of annotations are parsed into a
* is of annotation type. Annotations are generally scatter * format the eCharts accepts to draw markLines
* plot charts *
* 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 * @param {Object} annotation object
* @returns {Boolean} * @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 * This method currently generates deployments and annotations
* https://gitlab.com/gitlab-org/gitlab/-/issues/211418 annotations * but are not used in the chart. The method calling
* support will be added in this method. * 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 * This method is extracted out of the charts so that
* annotation lines can be easily supported in * annotation lines can be easily supported in
* the future. * 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 * @param {Array} deployments deployments data
* @returns {Object} annotation series object * @returns {Object} annotation series object
*/ */
export const generateAnnotationsSeries = (deployments = []) => { export const generateAnnotationsSeries = ({ deployments = [], annotations = [] } = {}) => {
if (!deployments.length) { // deployment data points
return []; const deploymentsData = deployments.map(deployment => {
}
const data = deployments.map(deployment => {
return { return {
name: 'deployments', name: 'deployments',
value: [deployment.createdAt, annotationsYAxisCoords.pos], value: [deployment.createdAt, annotationsYAxisCoords.pos],
...@@ -78,9 +95,27 @@ export const generateAnnotationsSeries = (deployments = []) => { ...@@ -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 { return {
type: graphTypes.annotationsData, type: graphTypes.annotationsData,
yAxisIndex: 1, // annotationsYAxis index yAxisIndex: 1, // annotationsYAxis index
data, data: [...deploymentsData, ...annotationsData],
markLine,
}; };
}; };
...@@ -6,9 +6,9 @@ import dateFormat from 'dateformat'; ...@@ -6,9 +6,9 @@ import dateFormat from 'dateformat';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import Icon from '~/vue_shared/components/icon.vue'; 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 { getYAxisOptions, getChartGrid, getTooltipFormatter } from './options';
import { annotationsYAxis, generateAnnotationsSeries, isAnnotation } from './annotations'; import { annotationsYAxis, generateAnnotationsSeries } from './annotations';
import { makeDataSeries } from '~/helpers/monitor_helper'; import { makeDataSeries } from '~/helpers/monitor_helper';
import { graphDataValidatorForValues } from '../../utils'; import { graphDataValidatorForValues } from '../../utils';
...@@ -20,6 +20,7 @@ const events = { ...@@ -20,6 +20,7 @@ const events = {
}; };
export default { export default {
tooltipTypes,
components: { components: {
GlAreaChart, GlAreaChart,
GlLineChart, GlLineChart,
...@@ -88,10 +89,10 @@ export default { ...@@ -88,10 +89,10 @@ export default {
data() { data() {
return { return {
tooltip: { tooltip: {
type: '',
title: '', title: '',
content: [], content: [],
commitUrl: '', commitUrl: '',
isDeployment: false,
sha: '', sha: '',
}, },
width: 0, width: 0,
...@@ -137,7 +138,13 @@ export default { ...@@ -137,7 +138,13 @@ export default {
}, []); }, []);
}, },
chartOptionSeries() { 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() { chartOptions() {
const { yAxis, xAxis } = this.option; const { yAxis, xAxis } = this.option;
...@@ -246,6 +253,9 @@ export default { ...@@ -246,6 +253,9 @@ export default {
formatLegendLabel(query) { formatLegendLabel(query) {
return `${query.label}`; return `${query.label}`;
}, },
isTooltipOfType(tooltipType, defaultType) {
return tooltipType === defaultType;
},
formatTooltipText(params) { formatTooltipText(params) {
this.tooltip.title = dateFormat(params.value, dateFormats.default); this.tooltip.title = dateFormat(params.value, dateFormats.default);
this.tooltip.content = []; this.tooltip.content = [];
...@@ -253,13 +263,18 @@ export default { ...@@ -253,13 +263,18 @@ export default {
params.seriesData.forEach(dataPoint => { params.seriesData.forEach(dataPoint => {
if (dataPoint.value) { if (dataPoint.value) {
const [xVal, yVal] = dataPoint.value; const [xVal, yVal] = dataPoint.value;
this.tooltip.isDeployment = isAnnotation(dataPoint.componentSubType); this.tooltip.type = dataPoint.name;
if (this.tooltip.isDeployment) { if (this.isTooltipOfType(this.tooltip.type, this.$options.tooltipTypes.deployments)) {
const [deploy] = this.recentDeployments.filter( const [deploy] = this.recentDeployments.filter(
deployment => deployment.createdAt === xVal, deployment => deployment.createdAt === xVal,
); );
this.tooltip.sha = deploy.sha.substring(0, 8); this.tooltip.sha = deploy.sha.substring(0, 8);
this.tooltip.commitUrl = deploy.commitUrl; 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 { } else {
const { seriesName, color, dataIndex } = dataPoint; const { seriesName, color, dataIndex } = dataPoint;
...@@ -288,7 +303,6 @@ export default { ...@@ -288,7 +303,6 @@ export default {
onChartUpdated(eChart) { onChartUpdated(eChart) {
[this.primaryColor] = eChart.getOption().color; [this.primaryColor] = eChart.getOption().color;
}, },
onChartCreated(eChart) { onChartCreated(eChart) {
// Emit a datazoom event that corresponds to the eChart // Emit a datazoom event that corresponds to the eChart
// `datazoom` event. // `datazoom` event.
...@@ -346,7 +360,7 @@ export default { ...@@ -346,7 +360,7 @@ export default {
@created="onChartCreated" @created="onChartCreated"
@updated="onChartUpdated" @updated="onChartUpdated"
> >
<template v-if="tooltip.isDeployment"> <template v-if="isTooltipOfType(tooltip.type, this.$options.tooltipTypes.deployments)">
<template slot="tooltipTitle"> <template slot="tooltipTitle">
{{ __('Deployed') }} {{ __('Deployed') }}
</template> </template>
...@@ -355,16 +369,23 @@ export default { ...@@ -355,16 +369,23 @@ export default {
<gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link> <gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
</div> </div>
</template> </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 v-else>
<template slot="tooltipTitle"> <template slot="tooltipTitle">
<slot name="tooltipTitle">
<div class="text-nowrap"> <div class="text-nowrap">
{{ tooltip.title }} {{ tooltip.title }}
</div> </div>
</slot>
</template> </template>
<template slot="tooltipContent"> <template slot="tooltipContent" :tooltip="tooltip">
<slot name="tooltipContent" :tooltip="tooltip">
<div <div
v-for="(content, key) in tooltip.content" v-for="(content, key) in tooltip.content"
:key="key" :key="key"
...@@ -377,7 +398,6 @@ export default { ...@@ -377,7 +398,6 @@ export default {
{{ content.value }} {{ content.value }}
</div> </div>
</div> </div>
</slot>
</template> </template>
</template> </template>
</component> </component>
......
...@@ -115,3 +115,12 @@ export const NOT_IN_DB_PREFIX = 'NO_DB'; ...@@ -115,3 +115,12 @@ export const NOT_IN_DB_PREFIX = 'NO_DB';
* Used as a value for the 'states' query filter * Used as a value for the 'states' query filter
*/ */
export const ENVIRONMENT_AVAILABLE_STATE = 'available'; 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 { ...@@ -81,4 +81,8 @@ export default {
text: s__('ProjectTemplates|Serverless Framework/JS'), text: s__('ProjectTemplates|Serverless Framework/JS'),
icon: '.template-option .icon-serverless_framework', 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; ...@@ -15,6 +15,14 @@ $item-weight-max-width: 48px;
max-width: 85%; max-width: 85%;
} }
.related-items-tree {
.card-header {
.gl-label {
line-height: $gl-line-height;
}
}
}
.item-body { .item-body {
position: relative; position: relative;
line-height: $gl-line-height; line-height: $gl-line-height;
...@@ -49,6 +57,10 @@ $item-weight-max-width: 48px; ...@@ -49,6 +57,10 @@ $item-weight-max-width: 48px;
color: $orange-600; color: $orange-600;
} }
.item-title-wrapper {
max-width: 100%;
}
.item-title { .item-title {
flex-basis: 100%; flex-basis: 100%;
font-size: $gl-font-size-small; font-size: $gl-font-size-small;
...@@ -72,15 +84,62 @@ $item-weight-max-width: 48px; ...@@ -72,15 +84,62 @@ $item-weight-max-width: 48px;
overflow: hidden; overflow: hidden;
white-space: nowrap; 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 { .item-meta {
flex-basis: 100%; flex-basis: 100%;
font-size: $gl-font-size-small; font-size: $gl-font-size;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
.item-meta-child { .item-due-date,
flex-basis: 100%; .board-card-weight {
&.board-card-info {
margin-right: 0;
}
} }
.item-attributes-area { .item-attributes-area {
...@@ -88,10 +147,6 @@ $item-weight-max-width: 48px; ...@@ -88,10 +147,6 @@ $item-weight-max-width: 48px;
margin-left: 8px; margin-left: 8px;
} }
.board-card-info {
margin-right: 0;
}
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {
margin-left: -8px; margin-left: -8px;
} }
...@@ -107,13 +162,21 @@ $item-weight-max-width: 48px; ...@@ -107,13 +162,21 @@ $item-weight-max-width: 48px;
max-width: $item-milestone-max-width; max-width: $item-milestone-max-width;
.ic-clock { .ic-clock {
color: $gl-text-color-tertiary; color: $gl-text-color-secondary;
margin-right: $gl-padding-4; margin-right: $gl-padding-4;
} }
} }
.item-weight { .item-weight {
max-width: $item-weight-max-width; 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; ...@@ -194,6 +257,13 @@ $item-weight-max-width: 48px;
.sortable-link { .sortable-link {
max-width: 90%; max-width: 90%;
} }
.item-body,
.card-header {
.health-label-short {
max-width: 30px;
}
}
} }
/* Small devices (landscape phones, 768px and up) */ /* Small devices (landscape phones, 768px and up) */
...@@ -232,6 +302,13 @@ $item-weight-max-width: 48px; ...@@ -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) */ /* Medium devices (desktops, 992px and up) */
...@@ -245,6 +322,17 @@ $item-weight-max-width: 48px; ...@@ -245,6 +322,17 @@ $item-weight-max-width: 48px;
font-size: inherit; // Base size given to `item-meta` is `$gl-font-size-small` 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) */ /* Large devices (large desktops, 1200px and up) */
...@@ -264,11 +352,23 @@ $item-weight-max-width: 48px; ...@@ -264,11 +352,23 @@ $item-weight-max-width: 48px;
} }
} }
.item-title-wrapper {
max-width: calc(100% - 440px);
}
.item-info-area { .item-info-area {
flex-basis: auto; flex-basis: auto;
} }
} }
.health-label-short {
display: initial;
}
.health-label-long {
display: none;
}
.item-contents { .item-contents {
overflow: hidden; overflow: hidden;
} }
...@@ -306,3 +406,20 @@ $item-weight-max-width: 48px; ...@@ -306,3 +406,20 @@ $item-weight-max-width: 48px;
line-height: 1.3; 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 ...@@ -144,4 +144,15 @@ class GitlabSchema < GraphQL::Schema
end end
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 @@ ...@@ -5,7 +5,7 @@
.mobile-overlay .mobile-overlay
.alert-wrapper .alert-wrapper
= render 'shared/outdated_browser' = 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/broadcast"
= render "layouts/header/read_only_banner" = render "layouts/header/read_only_banner"
= render "layouts/nav/classification_level_banner" = render "layouts/nav/classification_level_banner"
......
- navbar_links = links.sort_by(&:title) - navbar_links = links.sort_by(&:title)
- all_paths = navbar_links.map(&:path) - all_paths = navbar_links.map(&:path)
- analytics_link = navbar_links.find { |link| link.title == _('Value Stream') } || navbar_links.first
- if navbar_links.any? - if navbar_links.any?
= nav_link(path: all_paths) do = 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 .nav-icon-container
= sprite_icon('chart') = sprite_icon('chart')
%span.nav-item-name{ data: { qa_selector: 'analytics_link' } } %span.nav-item-name{ data: { qa_selector: 'analytics_link' } }
= _('Analytics') = _('Analytics')
%ul.sidebar-sub-level-items{ data: { qa_selector: 'analytics_sidebar_submenu' } } %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 = nav_link(path: analytics_link.path, html_options: { class: "fly-out-top-item" } ) do
= link_to navbar_links.first.link do = link_to analytics_link.link do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
= _('Analytics') = _('Analytics')
%li.divider.fly-out-top-item %li.divider.fly-out-top-item
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
%ul.nav.navbar-nav %ul.nav.navbar-nav
%li.header-user.dropdown %li.header-user.dropdown
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do = 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') = sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right .dropdown-menu.dropdown-menu-right
= render 'layouts/header/current_user_dropdown' = render 'layouts/header/current_user_dropdown'
......
...@@ -8,4 +8,5 @@ ...@@ -8,4 +8,5 @@
- unless project.empty_repo? - unless project.empty_repo?
= render 'shared/auto_devops_implicitly_enabled_banner', project: project = render 'shared/auto_devops_implicitly_enabled_banner', project: project
= render_if_exists 'projects/above_size_limit_warning', 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)] = render_if_exists 'shared/shared_runners_minutes_limit', project: project, classes: [container_class, ("limit-container-width" unless fluid_layout)]
...@@ -11,8 +11,6 @@ ...@@ -11,8 +11,6 @@
= f.hidden_field :auth_method, value: "password", class: "js-hidden-mirror-auth-type" = f.hidden_field :auth_method, value: "password", class: "js-hidden-mirror-auth-type"
.form-group .form-group
.collapse.js-well-changing-auth
.changing-auth-method
.well-password-auth.collapse.js-well-password-auth .well-password-auth.collapse.js-well-password-auth
= f.label :password, _("Password"), class: "label-bold" = f.label :password, _("Password"), class: "label-bold"
= f.password_field :password, value: mirror.password, class: 'form-control qa-password', autocomplete: 'new-password' = f.password_field :password, value: mirror.password, class: 'form-control qa-password', autocomplete: 'new-password'
- redirect_params = { redirect: @redirect } if @redirect - redirect_params = { redirect: @redirect } if @redirect
.card-body.rendered-terms .card-body.rendered-terms{ data: { qa_selector: 'terms_content' } }
= markdown_field(@term, :terms) = markdown_field(@term, :terms)
- if current_user - if current_user
.card-footer.footer-block.clearfix .card-footer.footer-block.clearfix
- if can?(current_user, :accept_terms, @term) - if can?(current_user, :accept_terms, @term)
.float-right .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') = _('Accept terms')
- else - else
.pull-right .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 ...@@ -28,7 +28,7 @@ graph TD
## Use cases ## 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. - 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. - Discuss and collaborate on feature ideas and scope at a high level.
![epics list view](img/epics_list_view_v12.5.png) ![epics list view](img/epics_list_view_v12.5.png)
...@@ -62,7 +62,7 @@ An epic's page contains the following tabs: ...@@ -62,7 +62,7 @@ An epic's page contains the following tabs:
## Adding an issue to an epic ## 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 ### 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' ...@@ -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. 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 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. the issue is automatically unlinked from its current parent.
To add an issue to an epic: To add an issue to an epic:
...@@ -101,6 +101,19 @@ To remove an issue from 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 on the <kbd>x</kbd> button in the epic's issue list.
1. Click **Remove** in the **Remove issue** warning message. 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)** ## Multi-level child epics **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/8333) in GitLab Ultimate 11.7. > [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: ...@@ -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 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. 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 An epic can have multiple child epics with
the maximum depth being 5. the maximum depth being 5.
......
...@@ -52,7 +52,7 @@ must be set. ...@@ -52,7 +52,7 @@ must be set.
<li>State</li> <li>State</li>
<ul> <ul>
<li>State (open or closed)</li> <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>Confidentiality</li>
<li>Tasks (completed vs. outstanding)</li> <li>Tasks (completed vs. outstanding)</li>
</ul> </ul>
...@@ -166,11 +166,12 @@ requires [GraphQL](../../../api/graphql/index.md) to be enabled. ...@@ -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. > [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) - **On track** (green)
- **Needs attention** (amber) - **Needs attention** (amber)
...@@ -178,9 +179,10 @@ To help you track the status of your issues, you can assign a status to each iss ...@@ -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) !["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 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 this feature is incompatible with old configuration. To turn off the feature while configuration is
......
...@@ -25,6 +25,11 @@ module Gitlab ...@@ -25,6 +25,11 @@ module Gitlab
@project_members = relation_reader.consume_relation(importable_path, 'project_members') @project_members = relation_reader.consume_relation(importable_path, 'project_members')
.map(&:first) .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 if relation_tree_restorer.restore
import_failure_service.with_retry(action: 'set_latest_merge_request_diff_ids!') do import_failure_service.with_retry(action: 'set_latest_merge_request_diff_ids!') do
@project.merge_requests.set_latest_merge_request_diff_ids! @project.merge_requests.set_latest_merge_request_diff_ids!
......
...@@ -56,7 +56,8 @@ module Gitlab ...@@ -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('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('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('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 ].freeze
class << self class << self
......
...@@ -2102,6 +2102,9 @@ msgstr "" ...@@ -2102,6 +2102,9 @@ msgstr ""
msgid "An error ocurred while loading your content. Please try again." msgid "An error ocurred while loading your content. Please try again."
msgstr "" msgstr ""
msgid "An example project for managing Kubernetes clusters integrated with GitLab."
msgstr ""
msgid "An instance-level serverless domain already exists." msgid "An instance-level serverless domain already exists."
msgstr "" msgstr ""
...@@ -15895,6 +15898,9 @@ msgstr "" ...@@ -15895,6 +15898,9 @@ msgstr ""
msgid "ProjectTemplates|Android" msgid "ProjectTemplates|Android"
msgstr "" msgstr ""
msgid "ProjectTemplates|GitLab Cluster Management"
msgstr ""
msgid "ProjectTemplates|Go Micro" msgid "ProjectTemplates|Go Micro"
msgstr "" msgstr ""
...@@ -23409,6 +23415,9 @@ msgstr "" ...@@ -23409,6 +23415,9 @@ msgstr ""
msgid "You could not create a new trigger." msgid "You could not create a new trigger."
msgstr "" 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." msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr "" msgstr ""
...@@ -23637,6 +23646,9 @@ msgstr "" ...@@ -23637,6 +23646,9 @@ msgstr ""
msgid "YouTube" msgid "YouTube"
msgstr "" 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." 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 "" msgstr ""
...@@ -23861,6 +23873,9 @@ msgstr "" ...@@ -23861,6 +23873,9 @@ msgstr ""
msgid "assign yourself" msgid "assign yourself"
msgstr "" msgstr ""
msgid "at risk"
msgstr ""
msgid "attach a new file" msgid "attach a new file"
msgstr "" msgstr ""
...@@ -24364,6 +24379,15 @@ msgstr "" ...@@ -24364,6 +24379,15 @@ msgstr ""
msgid "issue" msgid "issue"
msgstr "" msgstr ""
msgid "issues at risk"
msgstr ""
msgid "issues need attention"
msgstr ""
msgid "issues on track"
msgstr ""
msgid "it is stored externally" msgid "it is stored externally"
msgstr "" msgstr ""
...@@ -24744,6 +24768,9 @@ msgstr "" ...@@ -24744,6 +24768,9 @@ msgstr ""
msgid "n/a" msgid "n/a"
msgstr "" msgstr ""
msgid "need attention"
msgstr ""
msgid "needs to be between 10 minutes and 1 month" msgid "needs to be between 10 minutes and 1 month"
msgstr "" msgstr ""
...@@ -24777,6 +24804,9 @@ msgstr "" ...@@ -24777,6 +24804,9 @@ msgstr ""
msgid "nounSeries|%{item}, and %{lastItem}" msgid "nounSeries|%{item}, and %{lastItem}"
msgstr "" msgstr ""
msgid "on track"
msgstr ""
msgid "opened %{timeAgoString} by %{user}" msgid "opened %{timeAgoString} by %{user}"
msgstr "" msgstr ""
......
...@@ -168,6 +168,7 @@ module QA ...@@ -168,6 +168,7 @@ module QA
autoload :Menu, 'qa/page/main/menu' autoload :Menu, 'qa/page/main/menu'
autoload :OAuth, 'qa/page/main/oauth' autoload :OAuth, 'qa/page/main/oauth'
autoload :SignUp, 'qa/page/main/sign_up' autoload :SignUp, 'qa/page/main/sign_up'
autoload :Terms, 'qa/page/main/terms'
end end
module Settings module Settings
......
...@@ -10,9 +10,10 @@ module QA ...@@ -10,9 +10,10 @@ module QA
sign_in(as: as, address: address) sign_in(as: as, address: address)
yield result = yield
Page::Main::Menu.perform(&:sign_out) Page::Main::Menu.perform(&:sign_out)
result
end end
def while_signed_in_as_admin(address: :gitlab) def while_signed_in_as_admin(address: :gitlab)
......
...@@ -14,6 +14,20 @@ module QA ...@@ -14,6 +14,20 @@ module QA
ElementNotFound = Class.new(RuntimeError) 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_delegators :evaluator, :view, :views
def initialize def initialize
...@@ -250,6 +264,8 @@ module QA ...@@ -250,6 +264,8 @@ module QA
end end
def element_selector_css(name, *attributes) def element_selector_css(name, *attributes)
return name.selector_css if name.is_a? Page::Element
Page::Element.new(name, *attributes).selector_css Page::Element.new(name, *attributes).selector_css
end end
...@@ -296,10 +312,24 @@ module QA ...@@ -296,10 +312,24 @@ module QA
views.flat_map(&:elements) views.flat_map(&:elements)
end end
def self.required_elements
elements.select(&:required?)
end
def send_keys_to_element(name, keys) def send_keys_to_element(name, keys)
find_element(name).send_keys(keys) find_element(name).send_keys(keys)
end 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 class DSL
attr_reader :views attr_reader :views
......
...@@ -159,7 +159,13 @@ module QA ...@@ -159,7 +159,13 @@ module QA
fill_element :login_field, user.username fill_element :login_field, user.username
fill_element :password_field, user.password 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 end
def set_initial_password_if_present 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 ...@@ -120,10 +120,12 @@ module QA
def add_to_modified_content(content) def add_to_modified_content(content)
finished_loading? finished_loading?
modified_text_area.click
modified_text_area.set content modified_text_area.set content
end end
def modified_text_area def modified_text_area
wait_for_animated_element(:editor_container)
within_element(:editor_container) do within_element(:editor_container) do
find('.modified textarea.inputarea') find('.modified textarea.inputarea')
end end
......
...@@ -10,9 +10,7 @@ module QA ...@@ -10,9 +10,7 @@ module QA
base_page.wait_if_retry_later base_page.wait_if_retry_later
elements.each do |element| required_elements.each do |element|
next unless element.required?
unless base_page.has_element?(element.name, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) 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" raise Validatable::PageValidationError, "#{element.name} did not appear on #{self.name} as expected"
end end
......
...@@ -4,12 +4,13 @@ module QA ...@@ -4,12 +4,13 @@ module QA
context 'Create', quarantine: { type: :new } do context 'Create', quarantine: { type: :new } do
describe 'Review a merge request in Web IDE' do describe 'Review a merge request in Web IDE' do
let(:new_file) { 'awesome_new_file.txt' } let(:new_file) { 'awesome_new_file.txt' }
let(:original_text) { 'Text' }
let(:review_text) { 'Reviewed ' } let(:review_text) { 'Reviewed ' }
let(:merge_request) do let(:merge_request) do
Resource::MergeRequest.fabricate_via_api! do |mr| Resource::MergeRequest.fabricate_via_api! do |mr|
mr.file_name = new_file mr.file_name = new_file
mr.file_content = 'Text' mr.file_content = original_text
end end
end end
......
...@@ -107,4 +107,76 @@ describe QA::Page::Base do ...@@ -107,4 +107,76 @@ describe QA::Page::Base do
end end
end 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 end
...@@ -17,7 +17,11 @@ describe('IDE commit sidebar actions', () => { ...@@ -17,7 +17,11 @@ describe('IDE commit sidebar actions', () => {
let store; let store;
let vm; let vm;
const createComponent = ({ hasMR = false, currentBranchId = 'master' } = {}) => { const createComponent = ({
hasMR = false,
currentBranchId = 'master',
emptyRepo = false,
} = {}) => {
const Component = Vue.extend(commitActions); const Component = Vue.extend(commitActions);
vm = createComponentWithStore(Component, store); vm = createComponentWithStore(Component, store);
...@@ -27,6 +31,7 @@ describe('IDE commit sidebar actions', () => { ...@@ -27,6 +31,7 @@ describe('IDE commit sidebar actions', () => {
const proj = { ...projectData }; const proj = { ...projectData };
proj.branches[currentBranchId] = branches.find(branch => branch.name === currentBranchId); proj.branches[currentBranchId] = branches.find(branch => branch.name === currentBranchId);
proj.empty_repo = emptyRepo;
Vue.set(vm.$store.state.projects, 'abcproject', proj); Vue.set(vm.$store.state.projects, 'abcproject', proj);
...@@ -52,24 +57,27 @@ describe('IDE commit sidebar actions', () => { ...@@ -52,24 +57,27 @@ describe('IDE commit sidebar actions', () => {
vm = null; vm = null;
}); });
const findText = () => vm.$el.textContent;
const findRadios = () => Array.from(vm.$el.querySelectorAll('input[type="radio"]'));
it('renders 2 groups', () => { it('renders 2 groups', () => {
createComponent(); createComponent();
expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2); expect(findRadios().length).toBe(2);
}); });
it('renders current branch text', () => { it('renders current branch text', () => {
createComponent(); 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 => { it('hides merge request option when project merge requests are disabled', done => {
createComponent({ mergeRequestsEnabled: false }); createComponent({ hasMR: false });
vm.$nextTick(() => { vm.$nextTick(() => {
expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2); expect(findRadios().length).toBe(2);
expect(vm.$el.textContent).not.toContain('Create a new branch and merge request'); expect(findText()).not.toContain('Create a new branch and merge request');
done(); done();
}); });
...@@ -119,6 +127,7 @@ describe('IDE commit sidebar actions', () => { ...@@ -119,6 +127,7 @@ describe('IDE commit sidebar actions', () => {
it.each` it.each`
input | expectedOption input | expectedOption
${{ currentBranchId: BRANCH_DEFAULT }} | ${consts.COMMIT_TO_NEW_BRANCH} ${{ 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: true }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
${{ currentBranchId: BRANCH_PROTECTED, hasMR: false }} | ${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} ${{ currentBranchId: BRANCH_PROTECTED_NO_ACCESS, hasMR: true }} | ${consts.COMMIT_TO_NEW_BRANCH}
...@@ -138,4 +147,15 @@ describe('IDE commit sidebar actions', () => { ...@@ -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', () => { ...@@ -280,39 +280,21 @@ describe('IDE store getters', () => {
}); });
describe('canPushToBranch', () => { describe('canPushToBranch', () => {
it('returns false when no currentBranch exists', () => { it.each`
const localGetters = { currentBranch | canPushCode | expectedValue
currentProject: undefined, ${undefined} | ${undefined} | ${false}
}; ${{ can_push: true }} | ${false} | ${true}
${{ can_push: true }} | ${true} | ${true}
expect(getters.canPushToBranch({}, localGetters)).toBeFalsy(); ${{ can_push: false }} | ${false} | ${false}
}); ${{ can_push: false }} | ${true} | ${false}
${undefined} | ${true} | ${true}
it('returns true when can_push to currentBranch', () => { ${undefined} | ${false} | ${false}
const localGetters = { `(
currentProject: { 'with currentBranch ($currentBranch) and canPushCode ($canPushCode), it is $expectedValue',
default_branch: 'master', ({ currentBranch, canPushCode, expectedValue }) => {
}, expect(getters.canPushToBranch({}, { currentBranch, canPushCode })).toBe(expectedValue);
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();
});
}); });
describe('isFileDeletedAndReadded', () => { describe('isFileDeletedAndReadded', () => {
...@@ -422,6 +404,7 @@ describe('IDE store getters', () => { ...@@ -422,6 +404,7 @@ describe('IDE store getters', () => {
getterName | permissionKey getterName | permissionKey
${'canReadMergeRequests'} | ${'readMergeRequest'} ${'canReadMergeRequests'} | ${'readMergeRequest'}
${'canCreateMergeRequests'} | ${'createMergeRequestIn'} ${'canCreateMergeRequests'} | ${'createMergeRequestIn'}
${'canPushCode'} | ${'pushCode'}
`('$getterName', ({ getterName, permissionKey }) => { `('$getterName', ({ getterName, permissionKey }) => {
it.each([true, false])('finds permission for current project (%s)', val => { it.each([true, false])('finds permission for current project (%s)', val => {
localState.projects[TEST_PROJECT_ID] = { localState.projects[TEST_PROJECT_ID] = {
......
...@@ -292,4 +292,15 @@ describe('IDE commit module getters', () => { ...@@ -292,4 +292,15 @@ describe('IDE commit module getters', () => {
expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); 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 { generateAnnotationsSeries } from '~/monitoring/components/charts/annotations';
import { deploymentData } from '../../mock_data'; import { deploymentData, annotationsData } from '../../mock_data';
describe('annotations spec', () => { describe('annotations spec', () => {
describe('generateAnnotationsSeries', () => { describe('generateAnnotationsSeries', () => {
it('default options', () => { it('with default options', () => {
const annotations = generateAnnotationsSeries(); 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', () => { it('when only deployments data is passed', () => {
const annotations = generateAnnotationsSeries(deploymentData); const annotations = generateAnnotationsSeries({ deployments: deploymentData });
expect(annotations).toEqual( expect(annotations).toEqual(
expect.objectContaining({ expect.objectContaining({
type: 'scatter', type: 'scatter',
yAxisIndex: 1, yAxisIndex: 1,
data: expect.any(Array), data: expect.any(Array),
markLine: {
data: [],
symbol: 'none',
silent: true,
},
}), }),
); );
annotations.data.forEach(annotation => { annotations.data.forEach(annotation => {
expect(annotation).toEqual(expect.any(Object)); 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', () => { ...@@ -169,6 +169,7 @@ describe('Time series component', () => {
componentSubType: type, componentSubType: type,
value: [mockDate, 5.55555], value: [mockDate, 5.55555],
dataIndex: 0, dataIndex: 0,
...(type === 'scatter' && { name: 'deployments' }),
}, },
], ],
value: mockDate, value: mockDate,
...@@ -225,6 +226,10 @@ describe('Time series component', () => { ...@@ -225,6 +226,10 @@ describe('Time series component', () => {
timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter')); timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter'));
}); });
it('set tooltip type to deployments', () => {
expect(timeSeriesChart.vm.tooltip.type).toBe('deployments');
});
it('formats tooltip title', () => { it('formats tooltip title', () => {
expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM'); expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM');
}); });
...@@ -521,7 +526,11 @@ describe('Time series component', () => { ...@@ -521,7 +526,11 @@ describe('Time series component', () => {
const commitUrl = `${mockProjectDir}/-/commit/${mockSha}`; const commitUrl = `${mockProjectDir}/-/commit/${mockSha}`;
beforeEach(done => { beforeEach(done => {
timeSeriesAreaChart.vm.tooltip.isDeployment = true; timeSeriesAreaChart.setData({
tooltip: {
type: 'deployments',
},
});
timeSeriesAreaChart.vm.$nextTick(done); timeSeriesAreaChart.vm.$nextTick(done);
}); });
......
...@@ -210,6 +210,30 @@ export const deploymentData = [ ...@@ -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 = [ export const metricsNewGroupsAPIResponse = [
{ {
group: 'System metrics (Kubernetes)', group: 'System metrics (Kubernetes)',
......
...@@ -4,7 +4,7 @@ import { ...@@ -4,7 +4,7 @@ import {
refreshLastCommitData, refreshLastCommitData,
showBranchNotFoundError, showBranchNotFoundError,
createNewBranchFromDefault, createNewBranchFromDefault,
showEmptyState, loadEmptyBranch,
openBranch, openBranch,
loadFile, loadFile,
loadBranch, loadBranch,
...@@ -16,6 +16,8 @@ import router from '~/ide/ide_router'; ...@@ -16,6 +16,8 @@ import router from '~/ide/ide_router';
import { resetStore } from '../../helpers'; import { resetStore } from '../../helpers';
import testAction from '../../../helpers/vuex_action_helper'; import testAction from '../../../helpers/vuex_action_helper';
const TEST_PROJECT_ID = 'abc/def';
describe('IDE store project actions', () => { describe('IDE store project actions', () => {
let mock; let mock;
let store; let store;
...@@ -24,7 +26,7 @@ describe('IDE store project actions', () => { ...@@ -24,7 +26,7 @@ describe('IDE store project actions', () => {
store = createStore(); store = createStore();
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
store.state.projects['abc/def'] = { store.state.projects[TEST_PROJECT_ID] = {
branches: {}, branches: {},
}; };
}); });
...@@ -83,7 +85,7 @@ describe('IDE store project actions', () => { ...@@ -83,7 +85,7 @@ describe('IDE store project actions', () => {
{ {
type: 'SET_BRANCH_COMMIT', type: 'SET_BRANCH_COMMIT',
payload: { payload: {
projectId: 'abc/def', projectId: TEST_PROJECT_ID,
branchId: 'master', branchId: 'master',
commit: { id: '123' }, commit: { id: '123' },
}, },
...@@ -200,17 +202,17 @@ describe('IDE store project actions', () => { ...@@ -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 => { it('creates a blank tree and sets loading state to false', done => {
testAction( testAction(
showEmptyState, loadEmptyBranch,
{ projectId: 'abc/def', branchId: 'master' }, { projectId: TEST_PROJECT_ID, branchId: 'master' },
store.state, store.state,
[ [
{ type: 'CREATE_TREE', payload: { treePath: 'abc/def/master' } }, { type: 'CREATE_TREE', payload: { treePath: `${TEST_PROJECT_ID}/master` } },
{ {
type: 'TOGGLE_LOADING', 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), jasmine.any(Object),
...@@ -218,13 +220,15 @@ describe('IDE store project actions', () => { ...@@ -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( testAction(
showEmptyState, loadEmptyBranch,
{ projectId: 'abc/def', branchId: 'master' }, { projectId: TEST_PROJECT_ID, branchId: 'master' },
store.state, { trees },
jasmine.any(Object), [],
[{ type: 'setCurrentBranchId', payload: 'master' }], [],
done, done,
); );
}); });
...@@ -278,10 +282,29 @@ describe('IDE store project actions', () => { ...@@ -278,10 +282,29 @@ describe('IDE store project actions', () => {
}); });
describe('loadBranch', () => { describe('loadBranch', () => {
const projectId = 'abc/def'; const projectId = TEST_PROJECT_ID;
const branchId = '123-lorem'; const branchId = '123-lorem';
const ref = 'abcd2322'; 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 => { it('fetches branch data', done => {
const mockGetters = { findBranch: () => ({ commit: { id: ref } }) }; const mockGetters = { findBranch: () => ({ commit: { id: ref } }) };
spyOn(store, 'dispatch').and.returnValue(Promise.resolve()); spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
...@@ -317,7 +340,7 @@ describe('IDE store project actions', () => { ...@@ -317,7 +340,7 @@ describe('IDE store project actions', () => {
}); });
describe('openBranch', () => { describe('openBranch', () => {
const projectId = 'abc/def'; const projectId = TEST_PROJECT_ID;
const branchId = '123-lorem'; const branchId = '123-lorem';
const branch = { const branch = {
...@@ -335,55 +358,6 @@ describe('IDE store project actions', () => { ...@@ -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', () => { describe('existing branch', () => {
beforeEach(() => { beforeEach(() => {
spyOn(store, 'dispatch').and.returnValue(Promise.resolve()); spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
...@@ -410,11 +384,17 @@ describe('IDE store project actions', () => { ...@@ -410,11 +384,17 @@ describe('IDE store project actions', () => {
it('dispatches correct branch actions', done => { it('dispatches correct branch actions', done => {
openBranch(store, branch) openBranch(store, branch)
.then(() => { .then(val => {
expect(store.dispatch.calls.allArgs()).toEqual([ expect(store.dispatch.calls.allArgs()).toEqual([
['setCurrentBranchId', branchId], ['setCurrentBranchId', branchId],
['loadBranch', { projectId, branchId }], ['loadBranch', { projectId, branchId }],
]); ]);
expect(val).toEqual(
new Error(
`An error occurred while getting files for - <strong>${projectId}/${branchId}</strong>`,
),
);
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
......
...@@ -956,6 +956,37 @@ describe Gitlab::ImportExport::Project::TreeRestorer do ...@@ -956,6 +956,37 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
end end
end 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 end
context 'JSON with invalid records' do context 'JSON with invalid records' do
......
...@@ -25,7 +25,8 @@ describe Gitlab::ProjectTemplate 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('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('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('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) expect(described_class.all).to be_an(Array)
......
...@@ -30,6 +30,12 @@ describe BasePolicy, :do_not_mock_admin_mode do ...@@ -30,6 +30,12 @@ describe BasePolicy, :do_not_mock_admin_mode do
it { is_expected.to be_allowed(:read_cross_project) } 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 context 'when an external authorization service is enabled' do
before do before do
enable_external_authorization_service_check enable_external_authorization_service_check
...@@ -52,6 +58,12 @@ describe BasePolicy, :do_not_mock_admin_mode do ...@@ -52,6 +58,12 @@ describe BasePolicy, :do_not_mock_admin_mode do
is_expected.not_to be_allowed(:read_cross_project) is_expected.not_to be_allowed(:read_cross_project)
end end
end end
context 'for anonymous' do
let(:current_user) { nil }
it { is_expected.not_to be_allowed(:read_cross_project) }
end
end 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