summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorShinya Maeda <shinya@gitlab.com>2017-11-08 01:46:53 +0900
committerShinya Maeda <shinya@gitlab.com>2017-11-08 01:46:53 +0900
commit4fac95a64d0c04599af9c762edb94414d85bf42f (patch)
treeb87775aff426603984bc547208860ad147fcff69 /app
parent02878cd958557128cd9c22b27bd2fb97a843266b (diff)
parent396f45ade1dddec36df9861f8a1bb80aabd2ff15 (diff)
downloadgitlab-ce-4fac95a64d0c04599af9c762edb94414d85bf42f.tar.gz
Merge branch 'master' into 38464-k8s-apps
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/repo/components/repo_file.vue26
-rw-r--r--app/assets/javascripts/repo/components/repo_loading_file.vue41
-rw-r--r--app/assets/javascripts/repo/components/repo_sidebar.vue2
-rw-r--r--app/assets/javascripts/repo/services/index.js7
-rw-r--r--app/assets/javascripts/repo/stores/actions.js18
-rw-r--r--app/assets/javascripts/repo/stores/actions/file.js2
-rw-r--r--app/assets/javascripts/repo/stores/actions/tree.js58
-rw-r--r--app/assets/javascripts/repo/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/repo/stores/mutations.js7
-rw-r--r--app/assets/javascripts/repo/stores/mutations/tree.js30
-rw-r--r--app/assets/javascripts/repo/stores/state.js1
-rw-r--r--app/assets/javascripts/repo/stores/utils.js37
-rw-r--r--app/assets/javascripts/smart_interval.js29
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue37
-rw-r--r--app/controllers/concerns/issuable_actions.rb8
-rw-r--r--app/controllers/concerns/issuable_collections.rb91
-rw-r--r--app/controllers/concerns/issues_action.rb8
-rw-r--r--app/controllers/concerns/merge_requests_action.rb9
-rw-r--r--app/controllers/projects/issues_controller.rb17
-rw-r--r--app/controllers/projects/merge_requests_controller.rb32
-rw-r--r--app/controllers/projects/refs_controller.rb10
-rw-r--r--app/controllers/projects_controller.rb3
-rw-r--r--app/helpers/issuables_helper.rb2
-rw-r--r--app/models/clusters/providers/gcp.rb2
-rw-r--r--app/models/commit_status.rb12
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/merge_request.rb2
-rw-r--r--app/serializers/blob_entity.rb4
-rw-r--r--app/serializers/tree_entity.rb4
-rw-r--r--app/serializers/tree_root_entity.rb4
-rw-r--r--app/services/ci/ensure_stage_service.rb39
-rw-r--r--app/views/projects/clusters/_form.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml2
-rw-r--r--app/views/projects/issues/_nav_btns.html.haml4
-rw-r--r--app/views/shared/issuable/_filter.html.haml1
37 files changed, 373 insertions, 193 deletions
diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue
index 7a23154b340..9f16e6bb2cc 100644
--- a/app/assets/javascripts/repo/components/repo_file.vue
+++ b/app/assets/javascripts/repo/components/repo_file.vue
@@ -1,11 +1,15 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
+ import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
export default {
mixins: [
timeAgoMixin,
],
+ components: {
+ skeletonLoadingContainer,
+ },
props: {
file: {
type: Object,
@@ -16,6 +20,9 @@
...mapGetters([
'isCollapsed',
]),
+ isSubmodule() {
+ return this.file.type === 'submodule';
+ },
fileIcon() {
return {
'fa-spinner fa-spin': this.file.loading,
@@ -31,6 +38,9 @@
shortId() {
return this.file.id.substr(0, 8);
},
+ submoduleColSpan() {
+ return !this.isCollapsed && this.isSubmodule ? 3 : 1;
+ },
},
methods: {
...mapActions([
@@ -44,7 +54,7 @@
<tr
class="file"
@click.prevent="clickedTreeRow(file)">
- <td>
+ <td :colspan="submoduleColSpan">
<i
class="fa fa-fw file-icon"
:class="fileIcon"
@@ -58,7 +68,7 @@
>
{{ file.name }}
</a>
- <template v-if="file.type === 'submodule' && file.id">
+ <template v-if="isSubmodule && file.id">
@
<span class="commit-sha">
<a
@@ -71,15 +81,20 @@
</template>
</td>
- <template v-if="!isCollapsed">
+ <template v-if="!isCollapsed && !isSubmodule">
<td class="hidden-sm hidden-xs">
<a
+ v-if="file.lastCommit.message"
@click.stop
:href="file.lastCommit.url"
class="commit-message"
>
{{ file.lastCommit.message }}
</a>
+ <skeleton-loading-container
+ v-else
+ :small="true"
+ />
</td>
<td class="commit-update hidden-xs text-right">
@@ -89,6 +104,11 @@
>
{{ timeFormated(file.lastCommit.updatedAt) }}
</span>
+ <skeleton-loading-container
+ v-else
+ class="animation-container-right"
+ :small="true"
+ />
</td>
</template>
</tr>
diff --git a/app/assets/javascripts/repo/components/repo_loading_file.vue b/app/assets/javascripts/repo/components/repo_loading_file.vue
index 1e6c405f292..14c2ddfb615 100644
--- a/app/assets/javascripts/repo/components/repo_loading_file.vue
+++ b/app/assets/javascripts/repo/components/repo_loading_file.vue
@@ -1,17 +1,16 @@
<script>
import { mapGetters } from 'vuex';
+ import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
export default {
+ components: {
+ skeletonLoadingContainer,
+ },
computed: {
...mapGetters([
'isCollapsed',
]),
},
- methods: {
- lineOfCode(n) {
- return `skeleton-line-${n}`;
- },
- },
};
</script>
@@ -21,36 +20,24 @@
aria-label="Loading files"
>
<td>
- <div
- class="animation-container animation-container-small">
- <div
- v-for="n in 6"
- :key="n"
- :class="lineOfCode(n)">
- </div>
- </div>
+ <skeleton-loading-container
+ :small="true"
+ />
</td>
<template v-if="!isCollapsed">
<td
class="hidden-sm hidden-xs">
- <div class="animation-container">
- <div
- v-for="n in 6"
- :key="n"
- :class="lineOfCode(n)">
- </div>
- </div>
+ <skeleton-loading-container
+ :small="true"
+ />
</td>
<td
class="hidden-xs">
- <div class="animation-container animation-container-small animation-container-right">
- <div
- v-for="n in 6"
- :key="n"
- :class="lineOfCode(n)">
- </div>
- </div>
+ <skeleton-loading-container
+ class="animation-container-right"
+ :small="true"
+ />
</td>
</template>
</tr>
diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue
index 63c0d70f5c0..3f61d048288 100644
--- a/app/assets/javascripts/repo/components/repo_sidebar.vue
+++ b/app/assets/javascripts/repo/components/repo_sidebar.vue
@@ -80,7 +80,7 @@ export default {
/>
<repo-file
v-for="(file, index) in treeList"
- :key="index"
+ :key="file.key"
:file="file"
/>
</tbody>
diff --git a/app/assets/javascripts/repo/services/index.js b/app/assets/javascripts/repo/services/index.js
index dc222ccac01..2fb45dcb03c 100644
--- a/app/assets/javascripts/repo/services/index.js
+++ b/app/assets/javascripts/repo/services/index.js
@@ -30,4 +30,11 @@ export default {
commit(projectId, payload) {
return Api.commitMultiple(projectId, payload);
},
+ getTreeLastCommit(endpoint) {
+ return Vue.http.get(endpoint, {
+ params: {
+ format: 'json',
+ },
+ });
+ },
};
diff --git a/app/assets/javascripts/repo/stores/actions.js b/app/assets/javascripts/repo/stores/actions.js
index ca2f2a5ce7a..be290c268b1 100644
--- a/app/assets/javascripts/repo/stores/actions.js
+++ b/app/assets/javascripts/repo/stores/actions.js
@@ -64,7 +64,7 @@ export const checkCommitStatus = ({ state }) => service.getBranchData(
})
.catch(() => flash('Error checking branch data. Please try again.'));
-export const commitChanges = ({ commit, state, dispatch }, { payload, newMr }) =>
+export const commitChanges = ({ commit, state, dispatch, getters }, { payload, newMr }) =>
service.commit(state.project.id, payload)
.then((data) => {
const { branch } = payload;
@@ -73,12 +73,28 @@ export const commitChanges = ({ commit, state, dispatch }, { payload, newMr }) =
return;
}
+ const lastCommit = {
+ commit_path: `${state.project.url}/commit/${data.id}`,
+ commit: {
+ message: data.message,
+ authored_date: data.committed_date,
+ },
+ };
+
flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
if (newMr) {
redirectToUrl(`${state.endpoints.newMergeRequestUrl}${branch}`);
} else {
commit(types.SET_COMMIT_REF, data.id);
+
+ getters.changedFiles.forEach((entry) => {
+ commit(types.SET_LAST_COMMIT_DATA, {
+ entry,
+ lastCommit,
+ });
+ });
+
dispatch('discardAllChanges');
dispatch('closeAllFiles');
dispatch('toggleEditMode');
diff --git a/app/assets/javascripts/repo/stores/actions/file.js b/app/assets/javascripts/repo/stores/actions/file.js
index afbe0b78a82..5bae4fa826a 100644
--- a/app/assets/javascripts/repo/stores/actions/file.js
+++ b/app/assets/javascripts/repo/stores/actions/file.js
@@ -27,6 +27,8 @@ export const closeFile = ({ commit, state, dispatch }, { file, force = false })
} else if (!state.openFiles.length) {
pushState(file.parentTreeUrl);
}
+
+ dispatch('getLastCommitData');
};
export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
diff --git a/app/assets/javascripts/repo/stores/actions/tree.js b/app/assets/javascripts/repo/stores/actions/tree.js
index 129743c66c2..aa830e946a2 100644
--- a/app/assets/javascripts/repo/stores/actions/tree.js
+++ b/app/assets/javascripts/repo/stores/actions/tree.js
@@ -7,10 +7,11 @@ import {
setPageTitle,
findEntry,
createTemp,
+ createOrMergeEntry,
} from '../utils';
export const getTreeData = (
- { commit, state },
+ { commit, state, dispatch },
{ endpoint = state.endpoints.rootEndpoint, tree = state } = {},
) => {
commit(types.TOGGLE_LOADING, tree);
@@ -24,14 +25,20 @@ export const getTreeData = (
return res.json();
})
.then((data) => {
+ const prevLastCommitPath = tree.lastCommitPath;
if (!state.isInitialRoot) {
commit(types.SET_ROOT, data.path === '/');
}
- commit(types.SET_DIRECTORY_DATA, { data, tree });
+ dispatch('updateDirectoryData', { data, tree });
commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
+ commit(types.SET_LAST_COMMIT_URL, { tree, url: data.last_commit_path });
commit(types.TOGGLE_LOADING, tree);
+ if (prevLastCommitPath !== null) {
+ dispatch('getLastCommitData', tree);
+ }
+
pushState(endpoint);
})
.catch(() => {
@@ -48,7 +55,7 @@ export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
pushState(tree.parentTreeUrl);
commit(types.SET_PREVIOUS_URL, tree.parentTreeUrl);
- commit(types.SET_DIRECTORY_DATA, { data, tree });
+ dispatch('updateDirectoryData', { data, tree });
} else {
commit(types.SET_PREVIOUS_URL, endpoint);
dispatch('getTreeData', { endpoint, tree });
@@ -108,3 +115,48 @@ export const createTempTree = ({ state, commit, dispatch }, name) => {
});
}
};
+
+export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
+ if (tree.lastCommitPath === null || getters.isCollapsed) return;
+
+ service.getTreeLastCommit(tree.lastCommitPath)
+ .then((res) => {
+ const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
+
+ commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
+
+ return res.json();
+ })
+ .then((data) => {
+ data.forEach((lastCommit) => {
+ const entry = findEntry(tree, lastCommit.type, lastCommit.file_name);
+
+ if (entry) {
+ commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
+ }
+ });
+
+ dispatch('getLastCommitData', tree);
+ })
+ .catch(() => flash('Error fetching log data.'));
+};
+
+export const updateDirectoryData = ({ commit, state }, { data, tree }) => {
+ const level = tree.level !== undefined ? tree.level + 1 : 0;
+ const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
+ const createEntry = (entry, type) => createOrMergeEntry({
+ tree,
+ entry,
+ level,
+ type,
+ parentTreeUrl,
+ });
+
+ const formattedData = [
+ ...data.trees.map(t => createEntry(t, 'tree')),
+ ...data.submodules.map(m => createEntry(m, 'submodule')),
+ ...data.blobs.map(b => createEntry(b, 'blob')),
+ ];
+
+ commit(types.SET_DIRECTORY_DATA, { tree, data: formattedData });
+};
diff --git a/app/assets/javascripts/repo/stores/mutation_types.js b/app/assets/javascripts/repo/stores/mutation_types.js
index 4722a7dd0df..bc3390f1506 100644
--- a/app/assets/javascripts/repo/stores/mutation_types.js
+++ b/app/assets/javascripts/repo/stores/mutation_types.js
@@ -4,11 +4,13 @@ export const SET_COMMIT_REF = 'SET_COMMIT_REF';
export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
export const SET_ROOT = 'SET_ROOT';
export const SET_PREVIOUS_URL = 'SET_PREVIOUS_URL';
+export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
// Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
export const CREATE_TMP_TREE = 'CREATE_TMP_TREE';
+export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
// File mutation types
export const SET_FILE_DATA = 'SET_FILE_DATA';
diff --git a/app/assets/javascripts/repo/stores/mutations.js b/app/assets/javascripts/repo/stores/mutations.js
index 2f9b038322b..ae2ba5bedf7 100644
--- a/app/assets/javascripts/repo/stores/mutations.js
+++ b/app/assets/javascripts/repo/stores/mutations.js
@@ -48,6 +48,13 @@ export default {
previousUrl,
});
},
+ [types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
+ Object.assign(entry.lastCommit, {
+ url: lastCommit.commit_path,
+ message: lastCommit.commit.message,
+ updatedAt: lastCommit.commit.authored_date,
+ });
+ },
...fileMutations,
...treeMutations,
...branchMutations,
diff --git a/app/assets/javascripts/repo/stores/mutations/tree.js b/app/assets/javascripts/repo/stores/mutations/tree.js
index 52be2673107..130221c9fda 100644
--- a/app/assets/javascripts/repo/stores/mutations/tree.js
+++ b/app/assets/javascripts/repo/stores/mutations/tree.js
@@ -1,5 +1,4 @@
import * as types from '../mutation_types';
-import * as utils from '../utils';
export default {
[types.TOGGLE_TREE_OPEN](state, tree) {
@@ -8,30 +7,8 @@ export default {
});
},
[types.SET_DIRECTORY_DATA](state, { data, tree }) {
- const level = tree.level !== undefined ? tree.level + 1 : 0;
- const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
-
Object.assign(tree, {
- tree: [
- ...data.trees.map(t => utils.decorateData({
- ...t,
- type: 'tree',
- parentTreeUrl,
- level,
- }, state.project.url)),
- ...data.submodules.map(m => utils.decorateData({
- ...m,
- type: 'submodule',
- parentTreeUrl,
- level,
- }, state.project.url)),
- ...data.blobs.map(b => utils.decorateData({
- ...b,
- type: 'blob',
- parentTreeUrl,
- level,
- }, state.project.url)),
- ],
+ tree: data,
});
},
[types.SET_PARENT_TREE_URL](state, url) {
@@ -39,6 +16,11 @@ export default {
parentTreeUrl: url,
});
},
+ [types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
+ Object.assign(tree, {
+ lastCommitPath: url,
+ });
+ },
[types.CREATE_TMP_TREE](state, { parent, tmpEntry }) {
parent.tree.push(tmpEntry);
},
diff --git a/app/assets/javascripts/repo/stores/state.js b/app/assets/javascripts/repo/stores/state.js
index aab74754f02..0068834831e 100644
--- a/app/assets/javascripts/repo/stores/state.js
+++ b/app/assets/javascripts/repo/stores/state.js
@@ -8,6 +8,7 @@ export default () => ({
endpoints: {},
isRoot: false,
isInitialRoot: false,
+ lastCommitPath: '',
loading: false,
onTopOfBranch: false,
openFiles: [],
diff --git a/app/assets/javascripts/repo/stores/utils.js b/app/assets/javascripts/repo/stores/utils.js
index 797c2b1e5b9..fae1f4439a9 100644
--- a/app/assets/javascripts/repo/stores/utils.js
+++ b/app/assets/javascripts/repo/stores/utils.js
@@ -1,5 +1,6 @@
export const dataStructure = () => ({
id: '',
+ key: '',
type: '',
name: '',
url: '',
@@ -12,7 +13,12 @@ export const dataStructure = () => ({
opened: false,
active: false,
changed: false,
- lastCommit: {},
+ lastCommitPath: '',
+ lastCommit: {
+ url: '',
+ message: '',
+ updatedAt: '',
+ },
tree_url: '',
blamePath: '',
commitsPath: '',
@@ -27,14 +33,13 @@ export const dataStructure = () => ({
base64: false,
});
-export const decorateData = (entity, projectUrl = '') => {
+export const decorateData = (entity) => {
const {
id,
type,
url,
name,
icon,
- last_commit,
tree_url,
path,
renderError,
@@ -51,6 +56,7 @@ export const decorateData = (entity, projectUrl = '') => {
return {
...dataStructure(),
id,
+ key: `${name}-${type}-${id}`,
type,
name,
url,
@@ -66,12 +72,6 @@ export const decorateData = (entity, projectUrl = '') => {
renderError,
content,
base64,
- // eslint-disable-next-line camelcase
- lastCommit: last_commit ? {
- url: `${projectUrl}/commit/${last_commit.id}`,
- message: last_commit.message,
- updatedAt: last_commit.committed_date,
- } : {},
};
};
@@ -106,3 +106,22 @@ export const createTemp = ({ name, path, type, level, changed, content, base64 }
renderError: base64,
});
};
+
+export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level }) => {
+ const found = findEntry(tree, type, entry.name);
+
+ if (found) {
+ return Object.assign({}, found, {
+ id: entry.id,
+ url: entry.url,
+ tempFile: false,
+ });
+ }
+
+ return decorateData({
+ ...entry,
+ type,
+ parentTreeUrl,
+ level,
+ });
+};
diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js
index 2bf7a3a5d61..8e931995fc6 100644
--- a/app/assets/javascripts/smart_interval.js
+++ b/app/assets/javascripts/smart_interval.js
@@ -3,9 +3,10 @@
* and controllable by a public API.
*/
-class SmartInterval {
+export default class SmartInterval {
/**
- * @param { function } opts.callback Function to be called on each iteration (required)
+ * @param { function } opts.callback Function that returns a promise, called on each iteration
+ * unless still in progress (required)
* @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
* @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
* @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
@@ -42,13 +43,16 @@ class SmartInterval {
const cfg = this.cfg;
const state = this.state;
- if (cfg.immediateExecution) {
+ if (cfg.immediateExecution && !this.isLoading) {
cfg.immediateExecution = false;
- cfg.callback();
+ this.triggerCallback();
}
state.intervalId = window.setInterval(() => {
- cfg.callback();
+ if (this.isLoading) {
+ return;
+ }
+ this.triggerCallback();
if (this.getCurrentInterval() === cfg.maxInterval) {
return;
@@ -76,7 +80,7 @@ class SmartInterval {
// start a timer, using the existing interval
resume() {
- this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
+ this.stopTimer(); // stop existing timer, in case timer was not previously stopped
this.start();
}
@@ -104,6 +108,18 @@ class SmartInterval {
this.initPageUnloadHandling();
}
+ triggerCallback() {
+ this.isLoading = true;
+ this.cfg.callback()
+ .then(() => {
+ this.isLoading = false;
+ })
+ .catch((err) => {
+ this.isLoading = false;
+ throw err;
+ });
+ }
+
initVisibilityChangeHandling() {
// cancel interval when tab no longer shown (prevents cached pages from polling)
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
@@ -154,4 +170,3 @@ class SmartInterval {
}
}
-window.gl.SmartInterval = SmartInterval;
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
index 4f497b204a3..aaff9ee6518 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -1,3 +1,4 @@
+import SmartInterval from '~/smart_interval';
import Flash from '../flash';
import {
WidgetHeader,
@@ -81,7 +82,7 @@ export default {
return new MRWidgetService(endpoints);
},
checkStatus(cb) {
- this.service.checkStatus()
+ return this.service.checkStatus()
.then(res => res.json())
.then((res) => {
this.handleNotification(res);
@@ -97,7 +98,7 @@ export default {
});
},
initPolling() {
- this.pollingInterval = new gl.SmartInterval({
+ this.pollingInterval = new SmartInterval({
callback: this.checkStatus,
startingInterval: 10000,
maxInterval: 30000,
@@ -106,7 +107,7 @@ export default {
});
},
initDeploymentsPolling() {
- this.deploymentsInterval = new gl.SmartInterval({
+ this.deploymentsInterval = new SmartInterval({
callback: this.fetchDeployments,
startingInterval: 30000,
maxInterval: 120000,
@@ -121,7 +122,7 @@ export default {
}
},
fetchDeployments() {
- this.service.fetchDeployments()
+ return this.service.fetchDeployments()
.then(res => res.json())
.then((res) => {
if (res.length) {
diff --git a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
new file mode 100644
index 00000000000..b06493e6c66
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue
@@ -0,0 +1,37 @@
+<script>
+ export default {
+ props: {
+ small: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ lines: {
+ type: Number,
+ required: false,
+ default: 6,
+ },
+ },
+ computed: {
+ lineClasses() {
+ return new Array(this.lines).fill().map((_, i) => `skeleton-line-${i + 1}`);
+ },
+ },
+ };
+</script>
+
+<template>
+ <div
+ class="animation-container"
+ :class="{
+ 'animation-container-small': small,
+ }"
+ >
+ <div
+ v-for="(css, index) in lineClasses"
+ :key="index"
+ :class="css"
+ >
+ </div>
+ </div>
+</template>
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 9c222549cdc..072dffaff7a 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -9,9 +9,7 @@ module IssuableActions
def show
respond_to do |format|
- format.html do
- render show_view
- end
+ format.html
format.json do
render json: serializer.represent(issuable, serializer: params[:serializer])
end
@@ -152,10 +150,6 @@ module IssuableActions
end
end
- def show_view
- 'show'
- end
-
def serializer
raise NotImplementedError
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 3181f517087..2b011bc87b0 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -4,58 +4,44 @@ module IssuableCollections
include Gitlab::IssuableMetadata
included do
- helper_method :issues_finder
- helper_method :merge_requests_finder
+ helper_method :finder
end
private
- def set_issues_index
- @collection_type = "Issue"
- @issues = issues_collection
- @issues = @issues.page(params[:page])
- @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
- @total_pages = issues_page_count(@issues)
+ def set_issuables_index
+ @issuables = issuables_collection
+ @issuables = @issuables.page(params[:page])
+ @issuable_meta_data = issuable_meta_data(@issuables, collection_type)
+ @total_pages = issuable_page_count
- return if redirect_out_of_range(@issues, @total_pages)
+ return if redirect_out_of_range(@total_pages)
if params[:label_name].present?
- @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
+ labels_params = { project_id: @project.id, title: params[:label_name] }
+ @labels = LabelsFinder.new(current_user, labels_params).execute
end
@users = []
- end
-
- def issues_collection
- issues_finder.execute.preload(:project, :author, :assignees, :labels, :milestone, project: :namespace)
- end
-
- def merge_requests_collection
- merge_requests_finder.execute.preload(
- :source_project,
- :target_project,
- :author,
- :assignee,
- :labels,
- :milestone,
- head_pipeline: :project,
- target_project: :namespace,
- merge_request_diff: :merge_request_diff_commits
- )
- end
+ if params[:assignee_id].present?
+ assignee = User.find_by_id(params[:assignee_id])
+ @users.push(assignee) if assignee
+ end
- def issues_finder
- @issues_finder ||= issuable_finder_for(IssuesFinder)
+ if params[:author_id].present?
+ author = User.find_by_id(params[:author_id])
+ @users.push(author) if author
+ end
end
- def merge_requests_finder
- @merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder)
+ def issuables_collection
+ finder.execute.preload(preload_for_collection)
end
- def redirect_out_of_range(relation, total_pages)
+ def redirect_out_of_range(total_pages)
return false if total_pages.zero?
- out_of_range = relation.current_page > total_pages
+ out_of_range = @issuables.current_page > total_pages
if out_of_range
redirect_to(url_for(params.merge(page: total_pages, only_path: true)))
@@ -64,12 +50,8 @@ module IssuableCollections
out_of_range
end
- def issues_page_count(relation)
- page_count_for_relation(relation, issues_finder.row_count)
- end
-
- def merge_requests_page_count(relation)
- page_count_for_relation(relation, merge_requests_finder.row_count)
+ def issuable_page_count
+ page_count_for_relation(@issuables, finder.row_count)
end
def page_count_for_relation(relation, row_count)
@@ -145,4 +127,31 @@ module IssuableCollections
else value
end
end
+
+ def finder
+ return @finder if defined?(@finder)
+
+ @finder = issuable_finder_for(@finder_type)
+ end
+
+ def collection_type
+ @collection_type ||= case finder
+ when IssuesFinder
+ 'Issue'
+ when MergeRequestsFinder
+ 'MergeRequest'
+ end
+ end
+
+ def preload_for_collection
+ @preload_for_collection ||= case collection_type
+ when 'Issue'
+ [:project, :author, :assignees, :labels, :milestone, project: :namespace]
+ when 'MergeRequest'
+ [
+ :source_project, :target_project, :author, :assignee, :labels, :milestone,
+ head_pipeline: :project, target_project: :namespace, merge_request_diff: :merge_request_diff_commits
+ ]
+ end
+ end
end
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index 404559c8707..ad594903331 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -3,14 +3,14 @@ module IssuesAction
include IssuableCollections
def issues
- @label = issues_finder.labels.first
+ @finder_type = IssuesFinder
+ @label = finder.labels.first
- @issues = issues_collection
+ @issues = issuables_collection
.non_archived
.page(params[:page])
- @collection_type = "Issue"
- @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
+ @issuable_meta_data = issuable_meta_data(@issues, collection_type)
respond_to do |format|
format.html
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index d3c8e4888bc..8b569a01afd 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -3,13 +3,12 @@ module MergeRequestsAction
include IssuableCollections
def merge_requests
- @label = merge_requests_finder.labels.first
+ @finder_type = MergeRequestsFinder
+ @label = finder.labels.first
- @merge_requests = merge_requests_collection
- .page(params[:page])
+ @merge_requests = issuables_collection.page(params[:page])
- @collection_type = "MergeRequest"
- @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
+ @issuable_meta_data = issuable_meta_data(@merge_requests, collection_type)
end
private
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index d4e763aa5b8..dbc9106ba6d 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -10,7 +10,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :check_issues_available!
before_action :issue, except: [:index, :new, :create, :bulk_update]
- before_action :set_issues_index, only: [:index]
+ before_action :set_issuables_index, only: [:index]
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
@@ -24,15 +24,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to :html
def index
- if params[:assignee_id].present?
- assignee = User.find_by_id(params[:assignee_id])
- @users.push(assignee) if assignee
- end
-
- if params[:author_id].present?
- author = User.find_by_id(params[:author_id])
- @users.push(author) if author
- end
+ @issues = @issuables
respond_to do |format|
format.html
@@ -252,4 +244,9 @@ class Projects::IssuesController < Projects::ApplicationController
update_params = issue_params.merge(spammable_params)
Issues::UpdateService.new(project, current_user, update_params)
end
+
+ def set_issuables_index
+ @finder_type = IssuesFinder
+ super
+ end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index c86acae8fe4..402420b851e 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -10,33 +10,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
+ before_action :set_issuables_index, only: [:index]
+
before_action :authenticate_user!, only: [:assign_related_issues]
def index
- @collection_type = "MergeRequest"
- @merge_requests = merge_requests_collection
- @merge_requests = @merge_requests.page(params[:page])
- @merge_requests = @merge_requests.preload(merge_request_diff: :merge_request)
- @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
- @total_pages = merge_requests_page_count(@merge_requests)
-
- return if redirect_out_of_range(@merge_requests, @total_pages)
-
- if params[:label_name].present?
- labels_params = { project_id: @project.id, title: params[:label_name] }
- @labels = LabelsFinder.new(current_user, labels_params).execute
- end
-
- @users = []
- if params[:assignee_id].present?
- assignee = User.find_by_id(params[:assignee_id])
- @users.push(assignee) if assignee
- end
-
- if params[:author_id].present?
- author = User.find_by_id(params[:author_id])
- @users.push(author) if author
- end
+ @merge_requests = @issuables
respond_to do |format|
format.html
@@ -338,4 +317,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@target_project = @merge_request.target_project
@target_branches = @merge_request.target_project.repository.branch_names
end
+
+ def set_issuables_index
+ @finder_type = MergeRequestsFinder
+ super
+ end
end
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 2fd015df688..2376f469213 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -56,9 +56,12 @@ class Projects::RefsController < Projects::ApplicationController
contents[@offset, @limit].to_a.map do |content|
file = @path ? File.join(@path, content.name) : content.name
last_commit = @repo.last_commit_for_path(@commit.id, file)
+ commit_path = project_commit_path(@project, last_commit) if last_commit
{
file_name: content.name,
- commit: last_commit
+ commit: last_commit,
+ type: content.type,
+ commit_path: commit_path
}
end
end
@@ -70,6 +73,11 @@ class Projects::RefsController < Projects::ApplicationController
respond_to do |format|
format.html { render_404 }
+ format.json do
+ response.headers["More-Logs-Url"] = @more_log_url
+
+ render json: @logs
+ end
format.js
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 1688121e27e..2a473ec0cec 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -275,7 +275,8 @@ class ProjectsController < Projects::ApplicationController
@project_wiki = @project.wiki
@wiki_home = @project_wiki.find_page('home', params[:version_id])
elsif @project.feature_available?(:issues, current_user)
- @issues = issues_collection.page(params[:page])
+ @finder_type = IssuesFinder
+ @issues = issuables_collection.page(params[:page])
@collection_type = 'Issue'
@issuable_meta_data = issuable_meta_data(@issues, @collection_type)
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 85407e38532..a9840d19178 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -249,8 +249,6 @@ module IssuablesHelper
end
def issuables_count_for_state(issuable_type, state)
- finder = public_send("#{issuable_type}_finder") # rubocop:disable GitlabSecurity/PublicSend
-
Gitlab::IssuablesCountForState.new(finder)[state]
end
diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb
index c4391729dd7..ee2e43ee9dd 100644
--- a/app/models/clusters/providers/gcp.rb
+++ b/app/models/clusters/providers/gcp.rb
@@ -7,7 +7,7 @@ module Clusters
default_value_for :zone, 'us-central1-a'
default_value_for :num_nodes, 3
- default_value_for :machine_type, 'n1-standard-4'
+ default_value_for :machine_type, 'n1-standard-2'
attr_encrypted :access_token,
mode: :per_attribute_iv,
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index f3888528940..6b07dbdf3ea 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -14,7 +14,6 @@ class CommitStatus < ActiveRecord::Base
delegate :sha, :short_sha, to: :pipeline
validates :pipeline, presence: true, unless: :importing?
-
validates :name, presence: true, unless: :importing?
alias_attribute :author, :user
@@ -46,6 +45,17 @@ class CommitStatus < ActiveRecord::Base
runner_system_failure: 4
}
+ ##
+ # We still create some CommitStatuses outside of CreatePipelineService.
+ #
+ # These are pages deployments and external statuses.
+ #
+ before_create unless: :importing? do
+ Ci::EnsureStageService.new(project, user).execute(self) do |stage|
+ self.run_after_commit { StageUpdateWorker.perform_async(stage.id) }
+ end
+ end
+
state_machine :status do
event :process do
transition [:skipped, :manual] => :created
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index a928b9d6367..c008fb91a16 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -17,6 +17,8 @@ module Issuable
include Importable
include Editable
include AfterCommitQueue
+ include Sortable
+ include CreatedAtFilterable
# This object is used to gather issuable meta data for displaying
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests
diff --git a/app/models/issue.rb b/app/models/issue.rb
index fc590f9257e..3b3c7fb7f8b 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -5,11 +5,9 @@ class Issue < ActiveRecord::Base
include Issuable
include Noteable
include Referable
- include Sortable
include Spammable
include FasterCacheKeys
include RelativePositioning
- include CreatedAtFilterable
include TimeTrackable
DueDateStruct = Struct.new(:title, :name).freeze
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index f80601f3484..82d0ae90d77 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -3,9 +3,7 @@ class MergeRequest < ActiveRecord::Base
include Issuable
include Noteable
include Referable
- include Sortable
include IgnorableColumn
- include CreatedAtFilterable
include TimeTrackable
ignore_column :locked_at,
diff --git a/app/serializers/blob_entity.rb b/app/serializers/blob_entity.rb
index 56f173e5a27..ad039a2623d 100644
--- a/app/serializers/blob_entity.rb
+++ b/app/serializers/blob_entity.rb
@@ -3,10 +3,6 @@ class BlobEntity < Grape::Entity
expose :id, :path, :name, :mode
- expose :last_commit do |blob|
- request.project.repository.last_commit_for_path(blob.commit_id, blob.path)
- end
-
expose :icon do |blob|
IconsHelper.file_type_icon_class('file', blob.mode, blob.name)
end
diff --git a/app/serializers/tree_entity.rb b/app/serializers/tree_entity.rb
index 555e5cf83bd..9f1b485347f 100644
--- a/app/serializers/tree_entity.rb
+++ b/app/serializers/tree_entity.rb
@@ -3,10 +3,6 @@ class TreeEntity < Grape::Entity
expose :id, :path, :name, :mode
- expose :last_commit do |tree|
- request.project.repository.last_commit_for_path(tree.commit_id, tree.path)
- end
-
expose :icon do |tree|
IconsHelper.file_type_icon_class('folder', tree.mode, tree.name)
end
diff --git a/app/serializers/tree_root_entity.rb b/app/serializers/tree_root_entity.rb
index 69702ae1493..496f070ddbd 100644
--- a/app/serializers/tree_root_entity.rb
+++ b/app/serializers/tree_root_entity.rb
@@ -18,4 +18,8 @@ class TreeRootEntity < Grape::Entity
project_tree_path(request.project, File.join(request.ref, parent_tree_path))
end
+
+ expose :last_commit_path do |tree|
+ logs_file_project_ref_path(request.project, request.ref, tree.path)
+ end
end
diff --git a/app/services/ci/ensure_stage_service.rb b/app/services/ci/ensure_stage_service.rb
new file mode 100644
index 00000000000..dc2f49e8db1
--- /dev/null
+++ b/app/services/ci/ensure_stage_service.rb
@@ -0,0 +1,39 @@
+module Ci
+ ##
+ # We call this service everytime we persist a CI/CD job.
+ #
+ # In most cases a job should already have a stage assigned, but in cases it
+ # doesn't have we need to either find existing one or create a brand new
+ # stage.
+ #
+ class EnsureStageService < BaseService
+ def execute(build)
+ @build = build
+
+ return if build.stage_id.present?
+ return if build.invalid?
+
+ ensure_stage.tap do |stage|
+ build.stage_id = stage.id
+
+ yield stage if block_given?
+ end
+ end
+
+ private
+
+ def ensure_stage
+ find_stage || create_stage
+ end
+
+ def find_stage
+ @build.pipeline.stages.find_by(name: @build.stage)
+ end
+
+ def create_stage
+ Ci::Stage.create!(name: @build.stage,
+ pipeline: @build.pipeline,
+ project: @build.project)
+ end
+ end
+end
diff --git a/app/views/projects/clusters/_form.html.haml b/app/views/projects/clusters/_form.html.haml
index d8e5b55bb88..1f8ae463d0f 100644
--- a/app/views/projects/clusters/_form.html.haml
+++ b/app/views/projects/clusters/_form.html.haml
@@ -29,7 +29,7 @@
.form-group
= provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type')
= link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer')
- = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4'
+ = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-2'
.form-group
= field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-save'
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index ff17372fdd9..3f4415f9e5e 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -67,7 +67,7 @@
- if @commit.last_pipeline
- last_pipeline = @commit.last_pipeline
.well-segment.pipeline-info
- .status-icon-container{ class: "ci-status-icon-#{@commit.status}" }
+ .status-icon-container{ class: "ci-status-icon-#{last_pipeline.status}" }
= link_to project_pipeline_path(@project, last_pipeline.id) do
= ci_icon_for_status(last_pipeline.status)
#{ _('Pipeline') }
diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml
index 13809da6523..0d39edb7bfd 100644
--- a/app/views/projects/issues/_nav_btns.html.haml
+++ b/app/views/projects/issues/_nav_btns.html.haml
@@ -3,8 +3,8 @@
- if @can_bulk_update
= button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
= link_to "New issue", new_project_issue_path(@project,
- issue: { assignee_id: issues_finder.assignee.try(:id),
- milestone_id: issues_finder.milestones.first.try(:id) }),
+ issue: { assignee_id: finder.assignee.try(:id),
+ milestone_id: finder.milestones.first.try(:id) }),
class: "btn btn-new",
title: "New issue",
id: "new_issue_link"
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index d3f0aa2d339..8442d7ff4a2 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -1,4 +1,3 @@
-- finder = controller.controller_name == 'issues' ? issues_finder : merge_requests_finder
- boards_page = controller.controller_name == 'boards'
.issues-filters