summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue31
-rw-r--r--app/assets/javascripts/ide/constants.js1
-rw-r--r--app/assets/javascripts/ide/queries/getUserPermissions.query.graphql3
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js60
-rw-r--r--app/assets/javascripts/ide/stores/getters.js9
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js15
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js2
-rw-r--r--app/assets/javascripts/mirrors/ssh_mirror.js1
-rw-r--r--app/assets/javascripts/monitoring/components/charts/annotations.js67
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue74
-rw-r--r--app/assets/javascripts/monitoring/constants.js9
-rw-r--r--app/assets/javascripts/projects/default_project_templates.js4
-rw-r--r--app/assets/stylesheets/components/related_items_list.scss133
-rw-r--r--app/graphql/gitlab_schema.rb13
-rw-r--r--app/views/layouts/_page.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_analytics_links.html.haml7
-rw-r--r--app/views/layouts/terms.html.haml2
-rw-r--r--app/views/projects/_flash_messages.html.haml1
-rw-r--r--app/views/projects/mirrors/_authentication_method.html.haml2
-rw-r--r--app/views/users/terms/index.html.haml4
-rw-r--r--changelogs/unreleased/27915-fix-ide-empty-repo.yml5
-rw-r--r--changelogs/unreleased/37964-add-cluster-management-template.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-always-run-members-mapper.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-shell.yml5
-rw-r--r--doc/user/group/epics/index.md21
-rw-r--r--doc/user/project/issues/index.md12
-rw-r--r--lib/gitlab/import_export/project/tree_restorer.rb5
-rw-r--r--lib/gitlab/project_template.rb3
-rw-r--r--locale/gitlab.pot30
-rw-r--r--qa/qa.rb1
-rw-r--r--qa/qa/flow/login.rb3
-rw-r--r--qa/qa/page/base.rb30
-rw-r--r--qa/qa/page/main/login.rb8
-rw-r--r--qa/qa/page/main/terms.rb21
-rw-r--r--qa/qa/page/project/web_ide/edit.rb2
-rw-r--r--qa/qa/page/validatable.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide/review_merge_request_spec.rb3
-rw-r--r--qa/spec/page/base_spec.rb72
-rw-r--r--spec/frontend/ide/components/commit_sidebar/actions_spec.js32
-rw-r--r--spec/frontend/ide/stores/getters_spec.js49
-rw-r--r--spec/frontend/ide/stores/modules/commit/getters_spec.js11
-rw-r--r--spec/frontend/monitoring/components/charts/annotations_spec.js73
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js11
-rw-r--r--spec/frontend/monitoring/mock_data.js24
-rw-r--r--spec/javascripts/ide/stores/actions/project_spec.js112
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb31
-rw-r--r--spec/lib/gitlab/project_template_spec.rb3
-rw-r--r--spec/policies/base_policy_spec.rb12
-rw-r--r--vendor/project_templates/cluster_management.tar.gzbin0 -> 4051 bytes
50 files changed, 795 insertions, 240 deletions
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 4044f90867d..77903b35f3a 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-12.0.0
+12.1.0
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index 3276d21a04e..6a8ea506d1b 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -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>
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index e7762f9e0f2..fa2672aaece 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -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',
diff --git a/app/assets/javascripts/ide/queries/getUserPermissions.query.graphql b/app/assets/javascripts/ide/queries/getUserPermissions.query.graphql
index 48f63995f44..2c9013ffa9c 100644
--- a/app/assets/javascripts/ide/queries/getUserPermissions.query.graphql
+++ b/app/assets/javascripts/ide/queries/getUserPermissions.query.graphql
@@ -2,7 +2,8 @@ query getUserPermissions($projectPath: ID!) {
project(fullPath: $projectPath) {
userPermissions {
createMergeRequestIn,
- readMergeRequest
+ readMergeRequest,
+ pushCode
}
}
}
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 0b168009847..ae3829dc35e 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -83,10 +83,14 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => {
});
};
-export const showEmptyState = ({ commit, state, dispatch }, { projectId, branchId }) => {
+export const loadEmptyBranch = ({ commit, state }, { projectId, branchId }) => {
const treePath = `${projectId}/${branchId}`;
+ const currentTree = state.trees[`${projectId}/${branchId}`];
- dispatch('setCurrentBranchId', branchId);
+ // If we already have a tree, let's not recreate an empty one
+ if (currentTree) {
+ return;
+ }
commit(types.CREATE_TREE, { treePath });
commit(types.TOGGLE_LOADING, {
@@ -114,8 +118,16 @@ export const loadFile = ({ dispatch, state }, { basePath }) => {
}
};
-export const loadBranch = ({ dispatch, getters }, { projectId, branchId }) =>
- dispatch('getBranchData', {
+export const loadBranch = ({ dispatch, getters, state }, { projectId, branchId }) => {
+ const currentProject = state.projects[projectId];
+
+ if (currentProject?.branches?.[branchId]) {
+ return Promise.resolve();
+ } else if (getters.emptyRepo) {
+ return dispatch('loadEmptyBranch', { projectId, branchId });
+ }
+
+ return dispatch('getBranchData', {
projectId,
branchId,
})
@@ -137,29 +149,23 @@ export const loadBranch = ({ dispatch, getters }, { projectId, branchId }) =>
dispatch('showBranchNotFoundError', branchId);
throw err;
});
+};
-export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, basePath }) => {
- const currentProject = state.projects[projectId];
- if (getters.emptyRepo) {
- return dispatch('showEmptyState', { projectId, branchId });
- }
- if (!currentProject || !currentProject.branches[branchId]) {
- dispatch('setCurrentBranchId', branchId);
-
- return dispatch('loadBranch', { projectId, branchId })
- .then(() => dispatch('loadFile', { basePath }))
- .catch(
- () =>
- new Error(
- sprintf(
- __('An error occurred while getting files for - %{branchId}'),
- {
- branchId: `<strong>${esc(projectId)}/${esc(branchId)}</strong>`,
- },
- false,
- ),
+export const openBranch = ({ dispatch }, { projectId, branchId, basePath }) => {
+ dispatch('setCurrentBranchId', branchId);
+
+ return dispatch('loadBranch', { projectId, branchId })
+ .then(() => dispatch('loadFile', { basePath }))
+ .catch(
+ () =>
+ new Error(
+ sprintf(
+ __('An error occurred while getting files for - %{branchId}'),
+ {
+ branchId: `<strong>${esc(projectId)}/${esc(branchId)}</strong>`,
+ },
+ false,
),
- );
- }
- return Promise.resolve(dispatch('loadFile', { basePath }));
+ ),
+ );
};
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index d7ad39019a5..5d0a8570906 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -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 () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 9bf0542cd0b..505daa8834d 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -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}`,
);
}
}
diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js
index e421d44b6de..413c4b0110d 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js
@@ -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;
diff --git a/app/assets/javascripts/mirrors/ssh_mirror.js b/app/assets/javascripts/mirrors/ssh_mirror.js
index 550e1aeeb9c..2276a723326 100644
--- a/app/assets/javascripts/mirrors/ssh_mirror.js
+++ b/app/assets/javascripts/mirrors/ssh_mirror.js
@@ -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');
}
diff --git a/app/assets/javascripts/monitoring/components/charts/annotations.js b/app/assets/javascripts/monitoring/components/charts/annotations.js
index cc2b2bd0900..b0c89d5e374 100644
--- a/app/assets/javascripts/monitoring/components/charts/annotations.js
+++ b/app/assets/javascripts/monitoring/components/charts/annotations.js
@@ -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
+ * Fetched list of annotations are parsed into a
+ * format the eCharts accepts to draw markLines
+ *
+ * If Annotation is a single line, the `from` property
+ * has a value and the `to` is null. Because annotations
+ * only supports lines the from value does not exist yet.
+ *
*
- * @param {String} type series component type
- * @returns {Boolean}
+ * @param {Object} annotation object
+ * @returns {Object} markLine object
*/
-export const isAnnotation = type => type === graphTypes.annotationsData;
+export const parseAnnotations = ({
+ from: annotationFrom = '',
+ color = colorValues.primaryColor,
+}) => ({
+ xAxis: annotationFrom,
+ lineStyle: {
+ color,
+ },
+});
/**
- * This method currently supports only deployments. After
- * https://gitlab.com/gitlab-org/gitlab/-/issues/211418 annotations
- * support will be added in this method.
+ * This method currently generates deployments and annotations
+ * but are not used in the chart. The method calling
+ * generateAnnotationsSeries will not pass annotations until
+ * https://gitlab.com/gitlab-org/gitlab/-/issues/211330 is
+ * implemented.
*
* This method is extracted out of the charts so that
* annotation lines can be easily supported in
* the future.
*
+ * In order to make hover work, hidden annotation data points
+ * are created along with the markLines. These data points have
+ * the necessart metadata that is used to display in the tooltip.
+ *
* @param {Array} deployments deployments data
* @returns {Object} annotation series object
*/
-export const generateAnnotationsSeries = (deployments = []) => {
- if (!deployments.length) {
- return [];
- }
- const data = deployments.map(deployment => {
+export const generateAnnotationsSeries = ({ deployments = [], annotations = [] } = {}) => {
+ // deployment data points
+ const deploymentsData = deployments.map(deployment => {
return {
name: 'deployments',
value: [deployment.createdAt, annotationsYAxisCoords.pos],
@@ -78,9 +95,27 @@ export const generateAnnotationsSeries = (deployments = []) => {
};
});
+ // annotation data points
+ const annotationsData = annotations.map(annotation => {
+ return {
+ name: 'annotations',
+ value: [annotation.from, annotationsYAxisCoords.pos],
+ symbol: 'none',
+ description: annotation.description,
+ };
+ });
+
+ // annotation markLine option
+ const markLine = {
+ symbol: 'none',
+ silent: true,
+ data: annotations.map(parseAnnotations),
+ };
+
return {
type: graphTypes.annotationsData,
yAxisIndex: 1, // annotationsYAxis index
- data,
+ data: [...deploymentsData, ...annotationsData],
+ markLine,
};
};
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index 73c63a0580f..e43a0131528 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -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>
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index e092c0ccae0..6af1d399cfc 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -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',
+};
diff --git a/app/assets/javascripts/projects/default_project_templates.js b/app/assets/javascripts/projects/default_project_templates.js
index 37bad1efaaf..e5898c3b047 100644
--- a/app/assets/javascripts/projects/default_project_templates.js
+++ b/app/assets/javascripts/projects/default_project_templates.js
@@ -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',
+ },
};
diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss
index 59224d37744..6820bdca2fa 100644
--- a/app/assets/stylesheets/components/related_items_list.scss
+++ b/app/assets/stylesheets/components/related_items_list.scss
@@ -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);
+ }
+}
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 3c996978b6d..592167a633b 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -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
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 06e3bca99a1..16089a00386 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -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"
diff --git a/app/views/layouts/nav/sidebar/_analytics_links.html.haml b/app/views/layouts/nav/sidebar/_analytics_links.html.haml
index 90f9432af03..a99eb8cf457 100644
--- a/app/views/layouts/nav/sidebar/_analytics_links.html.haml
+++ b/app/views/layouts/nav/sidebar/_analytics_links.html.haml
@@ -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
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
index cdad617f006..1f7cf486b2c 100644
--- a/app/views/layouts/terms.html.haml
+++ b/app/views/layouts/terms.html.haml
@@ -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'
diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml
index f9222387e97..52bfc4f1b14 100644
--- a/app/views/projects/_flash_messages.html.haml
+++ b/app/views/projects/_flash_messages.html.haml
@@ -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)]
diff --git a/app/views/projects/mirrors/_authentication_method.html.haml b/app/views/projects/mirrors/_authentication_method.html.haml
index c4f564e26f4..dae0fa958ba 100644
--- a/app/views/projects/mirrors/_authentication_method.html.haml
+++ b/app/views/projects/mirrors/_authentication_method.html.haml
@@ -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'
diff --git a/app/views/users/terms/index.html.haml b/app/views/users/terms/index.html.haml
index a869cf9cdee..4fa04402a1c 100644
--- a/app/views/users/terms/index.html.haml
+++ b/app/views/users/terms/index.html.haml
@@ -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
diff --git a/changelogs/unreleased/27915-fix-ide-empty-repo.yml b/changelogs/unreleased/27915-fix-ide-empty-repo.yml
new file mode 100644
index 00000000000..cffae81354e
--- /dev/null
+++ b/changelogs/unreleased/27915-fix-ide-empty-repo.yml
@@ -0,0 +1,5 @@
+---
+title: Fix some Web IDE bugs with empty projects
+merge_request: 25463
+author:
+type: fixed
diff --git a/changelogs/unreleased/37964-add-cluster-management-template.yml b/changelogs/unreleased/37964-add-cluster-management-template.yml
new file mode 100644
index 00000000000..dae1fe4b3e3
--- /dev/null
+++ b/changelogs/unreleased/37964-add-cluster-management-template.yml
@@ -0,0 +1,5 @@
+---
+title: Add cluster management project template
+merge_request: 25318
+author:
+type: added
diff --git a/changelogs/unreleased/georgekoltsov-always-run-members-mapper.yml b/changelogs/unreleased/georgekoltsov-always-run-members-mapper.yml
new file mode 100644
index 00000000000..a025aa542e4
--- /dev/null
+++ b/changelogs/unreleased/georgekoltsov-always-run-members-mapper.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure members are always added on Project Import when importing as admin
+merge_request: 29046
+author:
+type: fixed
diff --git a/changelogs/unreleased/update-gitlab-shell.yml b/changelogs/unreleased/update-gitlab-shell.yml
new file mode 100644
index 00000000000..bdde0951d01
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-shell.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Shell to v12.1.0
+merge_request: 29167
+author:
+type: other
diff --git a/doc/user/group/epics/index.md b/doc/user/group/epics/index.md
index 7712b86bbe2..024f346ad47 100644
--- a/doc/user/group/epics/index.md
+++ b/doc/user/group/epics/index.md
@@ -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.
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index 3c480490e02..c020d6e5802 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -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
diff --git a/lib/gitlab/import_export/project/tree_restorer.rb b/lib/gitlab/import_export/project/tree_restorer.rb
index ad3720b56be..e9c89b803ba 100644
--- a/lib/gitlab/import_export/project/tree_restorer.rb
+++ b/lib/gitlab/import_export/project/tree_restorer.rb
@@ -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!
diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb
index f9a226be3cf..beeaeb70d51 100644
--- a/lib/gitlab/project_template.rb
+++ b/lib/gitlab/project_template.rb
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 862aca39951..cadbce443c7 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/qa/qa.rb b/qa/qa.rb
index ed425e205e3..af418033252 100644
--- a/qa/qa.rb
+++ b/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
diff --git a/qa/qa/flow/login.rb b/qa/qa/flow/login.rb
index 5505fabd4ae..8ad303df4de 100644
--- a/qa/qa/flow/login.rb
+++ b/qa/qa/flow/login.rb
@@ -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)
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index 4ccf9e2f168..42208f05c89 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -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
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index 0638097a7b7..8eb28eb53e7 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -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
diff --git a/qa/qa/page/main/terms.rb b/qa/qa/page/main/terms.rb
new file mode 100644
index 00000000000..a4928f24397
--- /dev/null
+++ b/qa/qa/page/main/terms.rb
@@ -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
diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb
index a9b82ac6046..7809f9246ec 100644
--- a/qa/qa/page/project/web_ide/edit.rb
+++ b/qa/qa/page/project/web_ide/edit.rb
@@ -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
diff --git a/qa/qa/page/validatable.rb b/qa/qa/page/validatable.rb
index f09a9aa9943..31ac6f71790 100644
--- a/qa/qa/page/validatable.rb
+++ b/qa/qa/page/validatable.rb
@@ -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
diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/review_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/review_merge_request_spec.rb
index c37ad6d4318..8ea1534492c 100644
--- a/qa/qa/specs/features/browser_ui/3_create/web_ide/review_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/review_merge_request_spec.rb
@@ -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
diff --git a/qa/spec/page/base_spec.rb b/qa/spec/page/base_spec.rb
index f6080bcad49..0cbb0a2b12e 100644
--- a/qa/spec/page/base_spec.rb
+++ b/qa/spec/page/base_spec.rb
@@ -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
diff --git a/spec/frontend/ide/components/commit_sidebar/actions_spec.js b/spec/frontend/ide/components/commit_sidebar/actions_spec.js
index b3b98a64891..a303e2b9bee 100644
--- a/spec/frontend/ide/components/commit_sidebar/actions_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/actions_spec.js
@@ -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');
+ });
+ });
});
diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js
index 011be95c1d2..408ea2b2939 100644
--- a/spec/frontend/ide/stores/getters_spec.js
+++ b/spec/frontend/ide/stores/getters_spec.js
@@ -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] = {
diff --git a/spec/frontend/ide/stores/modules/commit/getters_spec.js b/spec/frontend/ide/stores/modules/commit/getters_spec.js
index 07445c22917..adbfd7c6835 100644
--- a/spec/frontend/ide/stores/modules/commit/getters_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/getters_spec.js
@@ -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);
+ });
+ });
});
diff --git a/spec/frontend/monitoring/components/charts/annotations_spec.js b/spec/frontend/monitoring/components/charts/annotations_spec.js
index 100b13eabf4..69bf1fe4ced 100644
--- a/spec/frontend/monitoring/components/charts/annotations_spec.js
+++ b/spec/frontend/monitoring/components/charts/annotations_spec.js
@@ -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);
});
});
});
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index f2478a583dc..c9b670fd7a8 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -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);
});
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index dde47178c1d..c9f2b110147 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -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)',
diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js
index bd51222ac3c..e962224d1ad 100644
--- a/spec/javascripts/ide/stores/actions/project_spec.js
+++ b/spec/javascripts/ide/stores/actions/project_spec.js
@@ -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);
diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
index c8229eeee94..9fc0fdeb1bc 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb
index 3948e53bc17..ddc41e64147 100644
--- a/spec/lib/gitlab/project_template_spec.rb
+++ b/spec/lib/gitlab/project_template_spec.rb
@@ -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)
diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb
index ae5af9e0f29..e15221492c3 100644
--- a/spec/policies/base_policy_spec.rb
+++ b/spec/policies/base_policy_spec.rb
@@ -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
diff --git a/vendor/project_templates/cluster_management.tar.gz b/vendor/project_templates/cluster_management.tar.gz
new file mode 100644
index 00000000000..598b96d0308
--- /dev/null
+++ b/vendor/project_templates/cluster_management.tar.gz
Binary files differ