Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4dfc871117
commit
ad0265eead
50 changed files with 800 additions and 245 deletions
|
@ -1 +1 @@
|
|||
12.0.0
|
||||
12.1.0
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Promise.resolve(dispatch('loadFile', { basePath }));
|
||||
};
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
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
|
||||
*
|
||||
* @param {String} type series component type
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export const isAnnotation = type => type === graphTypes.annotationsData;
|
||||
|
||||
/**
|
||||
* This method currently supports only deployments. After
|
||||
* https://gitlab.com/gitlab-org/gitlab/-/issues/211418 annotations
|
||||
* support will be added in this method.
|
||||
* 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 {Object} annotation object
|
||||
* @returns {Object} markLine object
|
||||
*/
|
||||
export const parseAnnotations = ({
|
||||
from: annotationFrom = '',
|
||||
color = colorValues.primaryColor,
|
||||
}) => ({
|
||||
xAxis: annotationFrom,
|
||||
lineStyle: {
|
||||
color,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 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"
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
- 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'
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
- 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
|
||||
|
|
5
changelogs/unreleased/27915-fix-ide-empty-repo.yml
Normal file
5
changelogs/unreleased/27915-fix-ide-empty-repo.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix some Web IDE bugs with empty projects
|
||||
merge_request: 25463
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add cluster management project template
|
||||
merge_request: 25318
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Ensure members are always added on Project Import when importing as admin
|
||||
merge_request: 29046
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/update-gitlab-shell.yml
Normal file
5
changelogs/unreleased/update-gitlab-shell.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
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 ""
|
||||
|
||||
|
|
1
qa/qa.rb
1
qa/qa.rb
|
@ -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
|
||||
|
|
21
qa/qa/page/main/terms.rb
Normal file
21
qa/qa/page/main/terms.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
# 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,27 +1,90 @@
|
|||
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
|
||||
|
||||
|
|
BIN
vendor/project_templates/cluster_management.tar.gz
vendored
Normal file
BIN
vendor/project_templates/cluster_management.tar.gz
vendored
Normal file
Binary file not shown.
Loading…
Reference in a new issue