Commit 1494f31b authored by Stan Hu's avatar Stan Hu

Merge branch 'ce-to-ee-2018-05-08' into 'master'

CE upstream - 2018-05-08 00:16 UTC

Closes #5904, #5573, and #5857

See merge request gitlab-org/gitlab-ee!5608
parents 6a6a8cec e2606758
...@@ -28,11 +28,11 @@ Set the title to: `[Security] Description of the original issue` ...@@ -28,11 +28,11 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR - [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager. - [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager.
[seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/process.md#secpick-script [seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script
#### Documentation and final details #### Documentation and final details
- [ ] Check the topic on #security to see when the next release is going ot happen and add a link to the [links section](#links) - [ ] Check the topic on #security to see when the next release is going to happen and add a link to the [links section](#links)
- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details) - [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details) - [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details) - [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
......
...@@ -431,7 +431,7 @@ group :ed25519 do ...@@ -431,7 +431,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.97.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.99.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0' gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed # Locked until https://github.com/google/protobuf/issues/4210 is closed
......
...@@ -315,7 +315,7 @@ GEM ...@@ -315,7 +315,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.97.0) gitaly-proto (0.99.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.10) grpc (~> 1.10)
github-linguist (5.3.3) github-linguist (5.3.3)
...@@ -1094,7 +1094,7 @@ DEPENDENCIES ...@@ -1094,7 +1094,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.97.0) gitaly-proto (~> 0.99.0)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2) gitlab-gollum-lib (~> 4.2)
......
import Flash from '../flash'; import createFlash from '~/flash';
import { s__ } from '../locale'; import { __ } from '~/locale';
import setupToggleButtons from '../toggle_buttons'; import setupToggleButtons from '~/toggle_buttons';
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
import ClustersService from './services/clusters_service'; import ClustersService from './services/clusters_service';
export default () => { export default () => {
const clusterList = document.querySelector('.js-clusters-list'); const clusterList = document.querySelector('.js-clusters-list');
gcpSignupOffer();
// The empty state won't have a clusterList // The empty state won't have a clusterList
if (clusterList) { if (clusterList) {
setupToggleButtons( setupToggleButtons(document.querySelector('.js-clusters-list'), (value, toggle) =>
document.querySelector('.js-clusters-list'), ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }).catch(
(value, toggle) => err => {
ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }) createFlash(__('Something went wrong on our end.'));
.catch((err) => {
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
throw err; throw err;
}), },
),
); );
} }
}; };
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import Flash from '~/flash';
export default function gcpSignupOffer() {
const alertEl = document.querySelector('.gcp-signup-offer');
if (!alertEl) {
return;
}
const closeButtonEl = alertEl.getElementsByClassName('close')[0];
const { dismissEndpoint, featureId } = closeButtonEl.dataset;
closeButtonEl.addEventListener('click', () => {
axios
.post(dismissEndpoint, {
feature_name: featureId,
})
.then(() => {
$(alertEl).alert('close');
})
.catch(() => {
Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
});
});
}
<script> <script>
import eventHub from '../eventhub'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import eventHub from '../eventhub';
export default { export default {
components: { components: {
loadingIcon, loadingIcon,
}, },
...@@ -26,11 +26,6 @@ ...@@ -26,11 +26,6 @@
isLoading: false, isLoading: false,
}; };
}, },
computed: {
text() {
return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`;
},
},
methods: { methods: {
doAction() { doAction() {
this.isLoading = true; this.isLoading = true;
...@@ -40,16 +35,16 @@ ...@@ -40,16 +35,16 @@
}); });
}, },
}, },
}; };
</script> </script>
<template> <template>
<button <button
class="btn btn-sm prepend-left-10" class="btn"
:class="[{ disabled: isLoading }, btnCssClass]" :class="[{ disabled: isLoading }, btnCssClass]"
:disabled="isLoading" :disabled="isLoading"
@click="doAction"> @click="doAction">
{{ text }} <slot></slot>
<loading-icon <loading-icon
v-if="isLoading" v-if="isLoading"
:inline="true" :inline="true"
......
<script> <script>
import Flash from '../../flash'; import { s__ } from '~/locale';
import eventHub from '../eventhub'; import Flash from '~/flash';
import DeployKeysService from '../service'; import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import DeployKeysStore from '../store'; import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import keysPanel from './keys_panel.vue'; import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import KeysPanel from './keys_panel.vue';
export default { export default {
components: { components: {
keysPanel, KeysPanel,
loadingIcon, LoadingIcon,
NavigationTabs,
}, },
props: { props: {
endpoint: { endpoint: {
type: String, type: String,
required: true, required: true,
}, },
projectId: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
currentTab: 'enabled_keys',
isLoading: false, isLoading: false,
store: new DeployKeysStore(), store: new DeployKeysStore(),
}; };
}, },
scopes: {
enabled_keys: s__('DeployKeys|Enabled deploy keys'),
available_project_keys: s__('DeployKeys|Privately accessible deploy keys'),
public_keys: s__('DeployKeys|Publicly accessible deploy keys'),
},
computed: { computed: {
tabs() {
return Object.keys(this.$options.scopes).map(scope => {
const count = Array.isArray(this.keys[scope]) ? this.keys[scope].length : null;
return {
name: this.$options.scopes[scope],
scope,
isActive: scope === this.currentTab,
count,
};
});
},
hasKeys() { hasKeys() {
return Object.keys(this.keys).length; return Object.keys(this.keys).length;
}, },
...@@ -47,34 +72,44 @@ ...@@ -47,34 +72,44 @@
eventHub.$off('disable.key', this.disableKey); eventHub.$off('disable.key', this.disableKey);
}, },
methods: { methods: {
onChangeTab(tab) {
this.currentTab = tab;
},
fetchKeys() { fetchKeys() {
this.isLoading = true; this.isLoading = true;
this.service.getKeys() return this.service
.then((data) => { .getKeys()
.then(data => {
this.isLoading = false; this.isLoading = false;
this.store.keys = data; this.store.keys = data;
}) })
.catch(() => new Flash('Error getting deploy keys')); .catch(() => {
this.isLoading = false;
this.store.keys = {};
return new Flash(s__('DeployKeys|Error getting deploy keys'));
});
}, },
enableKey(deployKey) { enableKey(deployKey) {
this.service.enableKey(deployKey.id) this.service
.then(() => this.fetchKeys()) .enableKey(deployKey.id)
.catch(() => new Flash('Error enabling deploy key')); .then(this.fetchKeys)
.catch(() => new Flash(s__('DeployKeys|Error enabling deploy key')));
}, },
disableKey(deployKey, callback) { disableKey(deployKey, callback) {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if (confirm('You are going to remove this deploy key. Are you sure?')) { if (confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
this.service.disableKey(deployKey.id) this.service
.then(() => this.fetchKeys()) .disableKey(deployKey.id)
.then(this.fetchKeys)
.then(callback) .then(callback)
.catch(() => new Flash('Error removing deploy key')); .catch(() => new Flash(s__('DeployKeys|Error removing deploy key')));
} else { } else {
callback(); callback();
} }
}, },
}, },
}; };
</script> </script>
<template> <template>
...@@ -82,29 +117,38 @@ ...@@ -82,29 +117,38 @@
<loading-icon <loading-icon
v-if="isLoading && !hasKeys" v-if="isLoading && !hasKeys"
size="2" size="2"
label="Loading deploy keys" :label="s__('DeployKeys|Loading deploy keys')"
/> />
<div v-else-if="hasKeys"> <template v-else-if="hasKeys">
<keys-panel <div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
title="Enabled deploy keys for this project" <div class="fade-left">
class="qa-project-deploy-keys" <i
:keys="keys.enabled_keys" class="fa fa-angle-left"
:store="store" aria-hidden="true"
:endpoint="endpoint" >
/> </i>
<keys-panel </div>
title="Deploy keys from projects you have access to" <div class="fade-right">
:keys="keys.available_project_keys" <i
:store="store" class="fa fa-angle-right"
:endpoint="endpoint" aria-hidden="true"
>
</i>
</div>
<navigation-tabs
:tabs="tabs"
@onChangeTab="onChangeTab"
scope="deployKeys"
/> />
</div>
<keys-panel <keys-panel
v-if="keys.public_keys.length" class="qa-project-deploy-keys"
title="Public deploy keys available to any project" :project-id="projectId"
:keys="keys.public_keys" :keys="keys[currentTab]"
:store="store" :store="store"
:endpoint="endpoint" :endpoint="endpoint"
/> />
</div> </template>
</div> </div>
</template> </template>
<script> <script>
import actionBtn from './action_btn.vue'; import _ from 'underscore';
import { getTimeago } from '../../lib/utils/datetime_utility'; import { s__, sprintf } from '~/locale';
import tooltip from '../../vue_shared/directives/tooltip'; import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default { import actionBtn from './action_btn.vue';
export default {
components: { components: {
actionBtn, actionBtn,
icon,
}, },
directives: { directives: {
tooltip, tooltip,
}, },
mixins: [timeagoMixin],
props: { props: {
deployKey: { deployKey: {
type: Object, type: Object,
...@@ -23,89 +29,207 @@ ...@@ -23,89 +29,207 @@
type: String, type: String,
required: true, required: true,
}, },
projectId: {
type: String,
required: false,
default: null,
}, },
computed: {
timeagoDate() {
return getTimeago().format(this.deployKey.created_at);
}, },
data() {
return {
projectsExpanded: false,
};
},
computed: {
editDeployKeyPath() { editDeployKeyPath() {
return `${this.endpoint}/${this.deployKey.id}/edit`; return `${this.endpoint}/${this.deployKey.id}/edit`;
}, },
projects() {
const projects = [...this.deployKey.deploy_keys_projects];
if (this.projectId !== null) {
const indexOfCurrentProject = _.findIndex(
projects,
project =>
project &&
project.project &&
project.project.id &&
project.project.id.toString() === this.projectId,
);
if (indexOfCurrentProject > -1) {
const currentProject = projects.splice(indexOfCurrentProject, 1);
currentProject[0].project.full_name = s__('DeployKeys|Current project');
return currentProject.concat(projects);
}
}
return projects;
},
firstProject() {
return _.head(this.projects);
},
restProjects() {
return _.tail(this.projects);
},
restProjectsTooltip() {
return sprintf(s__('DeployKeys|Expand %{count} other projects'), {
count: this.restProjects.length,
});
},
restProjectsLabel() {
return sprintf(s__('DeployKeys|+%{count} others'), { count: this.restProjects.length });
},
isEnabled() {
return this.store.isEnabled(this.deployKey.id);
},
isRemovable() {
return (
this.store.isEnabled(this.deployKey.id) &&
this.deployKey.destroyed_when_orphaned &&
this.deployKey.almost_orphaned
);
},
isExpandable() {
return !this.projectsExpanded && this.restProjects.length > 1;
},
isExpanded() {
return this.projectsExpanded || this.restProjects.length === 1;
},
}, },
methods: { methods: {
isEnabled(id) { projectTooltipTitle(project) {
return this.store.findEnabledKey(id) !== undefined; return project.can_push
? s__('DeployKeys|Write access allowed')
: s__('DeployKeys|Read access only');
}, },
tooltipTitle(project) { toggleExpanded() {
return project.can_push ? 'Write access allowed' : 'Read access only'; this.projectsExpanded = !this.projectsExpanded;
}, },
}, },
}; };
</script> </script>
<template> <template>
<div> <div class="gl-responsive-table-row deploy-key">
<div class="pull-left append-right-10 hidden-xs"> <div class="table-section section-40">
<i <div
aria-hidden="true" role="rowheader"
class="fa fa-key key-icon" class="table-mobile-header">
> {{ s__('DeployKeys|Deploy key') }}
</i>
</div> </div>
<div class="deploy-key-content key-list-item-info"> <div class="table-mobile-content">
<strong class="title qa-key-title"> <strong class="title qa-key-title">
{{ deployKey.title }} {{ deployKey.title }}
</strong> </strong>
<div class="description qa-key-fingerprint"> <div class="fingerprint qa-key-fingerprint">
{{ deployKey.fingerprint }} {{ deployKey.fingerprint }}
</div> </div>
</div> </div>
<div class="deploy-key-content prepend-left-default deploy-key-projects"> </div>
<div class="table-section section-30 section-wrap">
<div
role="rowheader"
class="table-mobile-header">
{{ s__('DeployKeys|Project usage') }}
</div>
<div class="table-mobile-content deploy-project-list">
<template v-if="projects.length > 0">
<a
class="label deploy-project-label"
:title="projectTooltipTitle(firstProject)"
v-tooltip
>
<span>
{{ firstProject.project.full_name }}
</span>
<icon :name="firstProject.can_push ? 'lock-open' : 'lock'"/>
</a>
<a <a
v-for="(deployKeysProject, i) in deployKey.deploy_keys_projects" v-if="isExpandable"
:key="i" class="label deploy-project-label"
@click="toggleExpanded"
:title="restProjectsTooltip"
v-tooltip
>
<span>{{ restProjectsLabel }}</span>
</a>
<a
v-else-if="isExpanded"
v-for="deployKeysProject in restProjects"
:key="deployKeysProject.project.full_path"
class="label deploy-project-label" class="label deploy-project-label"
:href="deployKeysProject.project.full_path" :href="deployKeysProject.project.full_path"
:title="tooltipTitle(deployKeysProject)" :title="projectTooltipTitle(deployKeysProject)"
v-tooltip v-tooltip
> >
<span>
{{ deployKeysProject.project.full_name }} {{ deployKeysProject.project.full_name }}
<i </span>
v-if="!deployKeysProject.can_push" <icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'"/>
aria-hidden="true"
class="fa fa-lock"
>
</i>
</a> </a>
</template>
<span
v-else
class="text-secondary">{{ __('None') }}</span>
</div>
</div> </div>
<div class="deploy-key-content"> <div class="table-section section-15 text-right">
<span class="key-created-at"> <div
created {{ timeagoDate }} role="rowheader"
class="table-mobile-header">
{{ __('Created') }}
</div>
<div class="table-mobile-content text-secondary key-created-at">
<span
:title="tooltipTitle(deployKey.created_at)"
v-tooltip>
<icon name="calendar"/>
<span>{{ timeFormated(deployKey.created_at) }}</span>
</span> </span>
</div>
</div>
<div class="table-section section-15 table-button-footer deploy-key-actions">
<div class="btn-group table-action-buttons">
<action-btn
v-if="!isEnabled"
:deploy-key="deployKey"
type="enable"
>
{{ __('Enable') }}
</action-btn>
<a <a
v-if="deployKey.can_edit" v-if="deployKey.can_edit"
class="btn btn-sm" class="btn btn-default text-secondary"
:href="editDeployKeyPath" :href="editDeployKeyPath"
:title="__('Edit')"
data-container="body"
v-tooltip
> >
Edit <icon name="pencil"/>
</a> </a>
<action-btn <action-btn
v-if="!isEnabled(deployKey.id)" v-if="isRemovable"
:deploy-key="deployKey" :deploy-key="deployKey"
type="enable" btn-css-class="btn-danger"
/>
<action-btn
v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned"
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="remove" type="remove"
/> :title="__('Remove')"
data-container="body"
v-tooltip
>
<icon name="remove"/>
</action-btn>
<action-btn <action-btn
v-else v-else-if="isEnabled"
:deploy-key="deployKey" :deploy-key="deployKey"
btn-css-class="btn-warning" btn-css-class="btn-warning"
type="disable" type="disable"
/> :title="__('Disable')"
data-container="body"
v-tooltip
>
<icon name="cancel"/>
</action-btn>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import key from './key.vue'; import deployKey from './key.vue';
export default { export default {
components: { components: {
key, deployKey,
}, },
props: { props: {
title: {
type: String,
required: true,
},
keys: { keys: {
type: Array, type: Array,
required: true, required: true,
}, },
showHelpBox: {
type: Boolean,
required: false,
default: true,
},
store: { store: {
type: Object, type: Object,
required: true, required: true,
...@@ -27,36 +18,51 @@ ...@@ -27,36 +18,51 @@
type: String, type: String,
required: true, required: true,
}, },
projectId: {
type: String,
required: false,
default: null,
}, },
}; },
};
</script> </script>
<template> <template>
<div class="deploy-keys-panel"> <div class="deploy-keys-panel table-holder">
<h5> <template v-if="keys.length > 0">
{{ title }} <div
({{ keys.length }}) role="row"
</h5> class="gl-responsive-table-row table-row-header">
<ul <div
class="well-list" role="rowheader"
v-if="keys.length" class="table-section section-40">
> {{ s__('DeployKeys|Deploy key') }}
<li </div>
<div
role="rowheader"
class="table-section section-30">
{{ s__('DeployKeys|Project usage') }}
</div>
<div
role="rowheader"
class="table-section section-15 text-right">
{{ __('Created') }}
</div>
</div>
<deploy-key
v-for="deployKey in keys" v-for="deployKey in keys"
:key="deployKey.id" :key="deployKey.id"
>
<key
:deploy-key="deployKey" :deploy-key="deployKey"
:store="store" :store="store"
:endpoint="endpoint" :endpoint="endpoint"
:project-id="projectId"
/> />
</li> </template>
</ul>
<div <div
class="settings-message text-center" class="settings-message text-center"
v-else-if="showHelpBox" v-else
> >
No deploy keys found. Create one with the form above. {{ s__('DeployKeys|No deploy keys found. Create one with the form above.') }}
</div> </div>
</div> </div>
</template> </template>
import Vue from 'vue'; import Vue from 'vue';
import deployKeysApp from './components/app.vue'; import deployKeysApp from './components/app.vue';
export default () => new Vue({ export default () =>
new Vue({
el: document.getElementById('js-deploy-keys'), el: document.getElementById('js-deploy-keys'),
components: { components: {
deployKeysApp, deployKeysApp,
...@@ -9,13 +10,15 @@ export default () => new Vue({ ...@@ -9,13 +10,15 @@ export default () => new Vue({
data() { data() {
return { return {
endpoint: this.$options.el.dataset.endpoint, endpoint: this.$options.el.dataset.endpoint,
projectId: this.$options.el.dataset.projectId,
}; };
}, },
render(createElement) { render(createElement) {
return createElement('deploy-keys-app', { return createElement('deploy-keys-app', {
props: { props: {
endpoint: this.endpoint, endpoint: this.endpoint,
projectId: this.projectId,
}, },
}); });
}, },
}); });
...@@ -7,7 +7,10 @@ export default class DeployKeysService { ...@@ -7,7 +7,10 @@ export default class DeployKeysService {
constructor(endpoint) { constructor(endpoint) {
this.endpoint = endpoint; this.endpoint = endpoint;
this.resource = Vue.resource(`${this.endpoint}{/id}`, {}, { this.resource = Vue.resource(
`${this.endpoint}{/id}`,
{},
{
enable: { enable: {
method: 'PUT', method: 'PUT',
url: `${this.endpoint}{/id}/enable`, url: `${this.endpoint}{/id}/enable`,
...@@ -16,12 +19,12 @@ export default class DeployKeysService { ...@@ -16,12 +19,12 @@ export default class DeployKeysService {
method: 'PUT', method: 'PUT',
url: `${this.endpoint}{/id}/disable`, url: `${this.endpoint}{/id}/disable`,
}, },
}); },
);
} }
getKeys() { getKeys() {
return this.resource.get() return this.resource.get().then(response => response.json());
.then(response => response.json());
} }
enableKey(id) { enableKey(id) {
......
...@@ -3,7 +3,7 @@ export default class DeployKeysStore { ...@@ -3,7 +3,7 @@ export default class DeployKeysStore {
this.keys = {}; this.keys = {};
} }
findEnabledKey(id) { isEnabled(id) {
return this.keys.enabled_keys.find(key => key.id === id); return this.keys.enabled_keys.some(key => key.id === id);
} }
} }
...@@ -7,12 +7,12 @@ import { __ } from '~/locale'; ...@@ -7,12 +7,12 @@ import { __ } from '~/locale';
export default class GpgBadges { export default class GpgBadges {
static fetch() { static fetch() {
const badges = $('.js-loading-gpg-badge'); const badges = $('.js-loading-gpg-badge');
const form = $('.commits-search-form'); const tag = $('.js-signature-container');
badges.html('<i class="fa fa-spinner fa-spin"></i>'); badges.html('<i class="fa fa-spinner fa-spin"></i>');
const params = parseQueryStringIntoObject(form.serialize()); const params = parseQueryStringIntoObject(tag.serialize());
return axios.get(form.data('signaturesPath'), { params }) return axios.get(tag.data('signaturesPath'), { params })
.then(({ data }) => { .then(({ data }) => {
data.signatures.forEach((signature) => { data.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html); badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
......
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { activityBarViews } from '../constants';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
computed: {
...mapGetters(['currentProject', 'hasChanges']),
...mapState(['currentActivityView']),
goBackUrl() {
return document.referrer || this.currentProject.web_url;
},
},
methods: {
...mapActions(['updateActivityBarView']),
},
activityBarViews,
};
</script>
<template>
<nav class="ide-activity-bar">
<ul class="list-unstyled">
<li v-once>
<a
v-tooltip
data-container="body"
data-placement="right"
:href="goBackUrl"
class="ide-sidebar-link"
:title="s__('IDE|Go back')"
:aria-label="s__('IDE|Go back')"
>
<icon
:size="16"
name="go-back"
/>
</a>
</li>
<li>
<button
v-tooltip
data-container="body"
data-placement="right"
type="button"
class="ide-sidebar-link js-ide-edit-mode"
:class="{
active: currentActivityView === $options.activityBarViews.edit
}"
@click.prevent="updateActivityBarView($options.activityBarViews.edit)"
:title="s__('IDE|Edit')"
:aria-label="s__('IDE|Edit')"
>
<icon
name="code"
/>
</button>
</li>
<li>
<button
v-tooltip
data-container="body"
data-placement="right"
type="button"
class="ide-sidebar-link js-ide-review-mode"
:class="{
active: currentActivityView === $options.activityBarViews.review
}"
@click.prevent="updateActivityBarView($options.activityBarViews.review)"
:title="s__('IDE|Review')"
:aria-label="s__('IDE|Review')"
>
<icon
name="file-modified"
/>
</button>
</li>
<li v-show="hasChanges">
<button
v-tooltip
data-container="body"
data-placement="right"
type="button"
class="ide-sidebar-link js-ide-commit-mode"
:class="{
active: currentActivityView === $options.activityBarViews.commit
}"
@click.prevent="updateActivityBarView($options.activityBarViews.commit)"
:title="s__('IDE|Commit')"
:aria-label="s__('IDE|Commit')"
>
<icon
name="commit"
/>
</button>
</li>
</ul>
</nav>
</template>
<script> <script>
import { mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants'; import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue'; import RadioGroup from './radio_group.vue';
...@@ -9,7 +9,7 @@ export default { ...@@ -9,7 +9,7 @@ export default {
RadioGroup, RadioGroup,
}, },
computed: { computed: {
...mapState(['currentBranchId']), ...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
commitToCurrentBranchText() { commitToCurrentBranchText() {
return sprintf( return sprintf(
__('Commit to %{branchName} branch'), __('Commit to %{branchName} branch'),
...@@ -17,6 +17,17 @@ export default { ...@@ -17,6 +17,17 @@ export default {
false, false,
); );
}, },
disableMergeRequestRadio() {
return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
},
},
mounted() {
if (this.disableMergeRequestRadio) {
this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
}
},
methods: {
...mapActions('commit', ['updateCommitAction']),
}, },
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH, commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH, commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
...@@ -44,6 +55,7 @@ export default { ...@@ -44,6 +55,7 @@ export default {
:value="$options.commitToNewBranchMR" :value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')" :label="__('Create a new branch and merge request')"
:show-input="true" :show-input="true"
:disabled="disableMergeRequestRadio"
/> />
</div> </div>
</template> </template>
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
},
computed: { computed: {
...mapState(['lastCommitMsg', 'rightPanelCollapsed', 'changedFiles', 'stagedFiles']), ...mapState(['lastCommitMsg', 'noChangesStateSvgPath']),
...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
},
methods: {
...mapActions(['toggleRightPanelCollapsed']),
}, },
}; };
</script> </script>
...@@ -31,31 +13,8 @@ export default { ...@@ -31,31 +13,8 @@ export default {
v-if="!lastCommitMsg" v-if="!lastCommitMsg"
class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state" class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state"
> >
<header
class="multi-file-commit-panel-header"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<button
v-tooltip
:title="collapseButtonTooltip"
data-container="body"
data-placement="left"
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
:aria-label="__('Toggle sidebar')"
@click.stop="toggleRightPanelCollapsed"
>
<icon
:name="collapseButtonIcon"
:size="18"
/>
</button>
</header>
<div <div
class="ide-commit-empty-state-container" class="ide-commit-empty-state-container"
v-if="!rightPanelCollapsed"
> >
<div class="svg-content svg-80"> <div class="svg-content svg-80">
<img :src="noChangesStateSvgPath" /> <img :src="noChangesStateSvgPath" />
......
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import { sprintf, __ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitMessageField from './message_field.vue';
import Actions from './actions.vue';
import SuccessMessage from './success_message.vue';
import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT, COMMIT_ITEM_PADDING } from '../../constants';
export default {
components: {
Actions,
LoadingButton,
CommitMessageField,
SuccessMessage,
},
data() {
return {
isCompact: true,
componentHeight: null,
};
},
computed: {
...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['hasChanges']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
overviewText() {
return sprintf(
__(
'<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes',
),
{
stagedFilesLength: this.stagedFiles.length,
changedFilesLength: this.changedFiles.length,
},
);
},
},
watch: {
currentActivityView() {
if (this.lastCommitMsg) {
this.isCompact = false;
} else {
this.isCompact = !(
this.currentActivityView === activityBarViews.commit &&
window.innerHeight >= MAX_WINDOW_HEIGHT_COMPACT
);
}
},
lastCommitMsg() {
this.isCompact =
this.currentActivityView !== activityBarViews.commit && this.lastCommitMsg === '';
},
},
methods: {
...mapActions(['updateActivityBarView']),
...mapActions('commit', ['updateCommitMessage', 'discardDraft', 'commitChanges']),
toggleIsSmall() {
this.updateActivityBarView(activityBarViews.commit)
.then(() => {
this.isCompact = !this.isCompact;
})
.catch(e => {
throw e;
});
},
beforeEnterTransition() {
const elHeight = this.isCompact
? this.$refs.formEl && this.$refs.formEl.offsetHeight
: this.$refs.compactEl && this.$refs.compactEl.offsetHeight;
this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
},
enterTransition() {
this.$nextTick(() => {
const elHeight = this.isCompact
? this.$refs.compactEl && this.$refs.compactEl.offsetHeight
: this.$refs.formEl && this.$refs.formEl.offsetHeight;
this.componentHeight = elHeight + COMMIT_ITEM_PADDING;
});
},
afterEndTransition() {
this.componentHeight = null;
},
},
activityBarViews,
};
</script>
<template>
<div
class="multi-file-commit-form"
:class="{
'is-compact': isCompact,
'is-full': !isCompact
}"
:style="{
height: componentHeight ? `${componentHeight}px` : null,
}"
>
<transition
name="commit-form-slide-up"
@before-enter="beforeEnterTransition"
@enter="enterTransition"
@after-enter="afterEndTransition"
>
<div
v-if="isCompact"
class="commit-form-compact"
ref="compactEl"
>
<button
type="button"
:disabled="!hasChanges"
class="btn btn-primary btn-sm btn-block"
@click="toggleIsSmall"
>
{{ __('Commit') }}
</button>
<p
class="text-center"
v-html="overviewText"
></p>
</div>
<form
v-if="!isCompact"
class="form-horizontal"
@submit.prevent.stop="commitChanges"
ref="formEl"
>
<transition name="fade">
<success-message
v-show="lastCommitMsg"
/>
</transition>
<commit-message-field
:text="commitMessage"
@input="updateCommitMessage"
/>
<div class="clearfix prepend-top-15">
<actions />
<loading-button
:loading="submitCommitLoading"
:disabled="commitButtonDisabled"
container-class="btn btn-success btn-sm pull-left"
:label="__('Commit')"
@click="commitChanges"
/>
<button
v-if="!discardDraftButtonDisabled"
type="button"
class="btn btn-default btn-sm pull-right"
@click="discardDraft"
>
{{ __('Discard draft') }}
</button>
<button
v-else
type="button"
class="btn btn-default btn-sm pull-right"
@click="toggleIsSmall"
>
{{ __('Collapse') }}
</button>
</div>
</form>
</transition>
</div>
</template>
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions } from 'vuex';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import ListItem from './list_item.vue'; import ListItem from './list_item.vue';
import ListCollapsed from './list_collapsed.vue';
export default { export default {
components: { components: {
Icon, Icon,
ListItem, ListItem,
ListCollapsed,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -24,11 +22,6 @@ export default { ...@@ -24,11 +22,6 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
showToggle: {
type: Boolean,
required: false,
default: true,
},
iconName: { iconName: {
type: String, type: String,
required: true, required: true,
...@@ -51,9 +44,12 @@ export default { ...@@ -51,9 +44,12 @@ export default {
default: false, default: false,
}, },
}, },
data() {
return {
showActionButton: false,
};
},
computed: { computed: {
...mapState(['rightPanelCollapsed']),
...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
titleText() { titleText() {
return sprintf(__('%{title} changes'), { return sprintf(__('%{title} changes'), {
title: this.title, title: this.title,
...@@ -61,10 +57,13 @@ export default { ...@@ -61,10 +57,13 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['toggleRightPanelCollapsed', 'stageAllChanges', 'unstageAllChanges']), ...mapActions(['stageAllChanges', 'unstageAllChanges']),
actionBtnClicked() { actionBtnClicked() {
this[this.action](); this[this.action]();
}, },
setShowActionButton(show) {
this.showActionButton = show;
},
}, },
}; };
</script> </script>
...@@ -72,19 +71,14 @@ export default { ...@@ -72,19 +71,14 @@ export default {
<template> <template>
<div <div
class="ide-commit-list-container" class="ide-commit-list-container"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
> >
<header <header
class="multi-file-commit-panel-header" class="multi-file-commit-panel-header"
@mouseenter="setShowActionButton(true)"
@mouseleave="setShowActionButton(false)"
> >
<div <div
v-if="!rightPanelCollapsed"
class="multi-file-commit-panel-header-title" class="multi-file-commit-panel-header-title"
:class="{
'append-right-10': showToggle,
}"
> >
<icon <icon
v-once v-once
...@@ -92,7 +86,14 @@ export default { ...@@ -92,7 +86,14 @@ export default {
:size="18" :size="18"
/> />
{{ titleText }} {{ titleText }}
<span
v-show="!showActionButton"
class="ide-commit-file-count"
>
{{ fileList.length }}
</span>
<button <button
v-show="showActionButton"
type="button" type="button"
class="btn btn-blank btn-link ide-staged-action-btn" class="btn btn-blank btn-link ide-staged-action-btn"
@click="actionBtnClicked" @click="actionBtnClicked"
...@@ -100,30 +101,7 @@ export default { ...@@ -100,30 +101,7 @@ export default {
{{ actionBtnText }} {{ actionBtnText }}
</button> </button>
</div> </div>
<button
v-if="showToggle"
v-tooltip
:title="collapseButtonTooltip"
data-container="body"
data-placement="left"
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
:aria-label="__('Toggle sidebar')"
@click.stop="toggleRightPanelCollapsed"
>
<icon
:name="collapseButtonIcon"
:size="18"
/>
</button>
</header> </header>
<list-collapsed
v-if="rightPanelCollapsed"
:files="fileList"
:icon-name="iconName"
:title="title"
/>
<template v-else>
<ul <ul
v-if="fileList.length" v-if="fileList.length"
class="multi-file-commit-list list-unstyled append-bottom-0" class="multi-file-commit-list list-unstyled append-bottom-0"
...@@ -146,6 +124,5 @@ export default { ...@@ -146,6 +124,5 @@ export default {
> >
{{ __('No changes') }} {{ __('No changes') }}
</p> </p>
</template>
</div> </div>
</template> </template>
...@@ -3,6 +3,7 @@ import { mapActions } from 'vuex'; ...@@ -3,6 +3,7 @@ import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue'; import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue'; import UnstageButton from './unstage_button.vue';
import { viewerTypes } from '../../constants';
export default { export default {
components: { components: {
...@@ -53,7 +54,7 @@ export default { ...@@ -53,7 +54,7 @@ export default {
keyPrefix: this.keyPrefix.toLowerCase(), keyPrefix: this.keyPrefix.toLowerCase(),
}).then(changeViewer => { }).then(changeViewer => {
if (changeViewer) { if (changeViewer) {
this.updateViewer('diff'); this.updateViewer(viewerTypes.diff);
} }
}); });
}, },
......
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
...@@ -26,10 +27,20 @@ export default { ...@@ -26,10 +27,20 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
disabled: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
...mapState('commit', ['commitAction']), ...mapState('commit', ['commitAction']),
...mapGetters('commit', ['newBranchName']), ...mapGetters('commit', ['newBranchName']),
tooltipTitle() {
return this.disabled
? __('This option is disabled while you still have unstaged changes')
: '';
},
}, },
methods: { methods: {
...mapActions('commit', ['updateCommitAction', 'updateBranchName']), ...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
...@@ -39,19 +50,28 @@ export default { ...@@ -39,19 +50,28 @@ export default {
<template> <template>
<fieldset> <fieldset>
<label> <label
v-tooltip
:title="tooltipTitle"
:class="{
'is-disabled': disabled
}"
>
<input <input
type="radio" type="radio"
name="commit-action" name="commit-action"
:value="value" :value="value"
@change="updateCommitAction($event.target.value)" @change="updateCommitAction($event.target.value)"
:checked="checked" :checked="commitAction === value"
v-once :disabled="disabled"
/> />
<span class="prepend-left-10"> <span class="prepend-left-10">
<template v-if="label"> <span
v-if="label"
class="ide-radio-label"
>
{{ label }} {{ label }}
</template> </span>
<slot v-else></slot> <slot v-else></slot>
</span> </span>
</label> </label>
......
...@@ -2,14 +2,8 @@ ...@@ -2,14 +2,8 @@
import { mapState } from 'vuex'; import { mapState } from 'vuex';
export default { export default {
props: {
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: { computed: {
...mapState(['lastCommitMsg']), ...mapState(['lastCommitMsg', 'committedStateSvgPath']),
}, },
}; };
</script> </script>
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import { viewerTypes } from '../constants';
export default { export default {
components: {
Icon,
},
props: { props: {
hasChanges: {
type: Boolean,
required: false,
default: false,
},
mergeRequestId: {
type: String,
required: false,
default: '',
},
viewer: { viewer: {
type: String, type: String,
required: true, required: true,
}, },
showShadow: { mergeRequestId: {
type: Boolean, type: Number,
required: true, required: true,
}, },
}, },
...@@ -38,48 +25,29 @@ export default { ...@@ -38,48 +25,29 @@ export default {
this.$emit('click', mode); this.$emit('click', mode);
}, },
}, },
viewerTypes,
}; };
</script> </script>
<template> <template>
<div <div
class="dropdown" class="dropdown"
:class="{
shadow: showShadow,
}"
> >
<button <button
type="button" type="button"
class="btn btn-primary btn-sm" class="btn btn-link"
:class="{
'btn-inverted': hasChanges,
}"
data-toggle="dropdown" data-toggle="dropdown"
> >
<template v-if="viewer === 'mrdiff' && mergeRequestId"> {{ __('Edit') }}
{{ mergeReviewLine }}
</template>
<template v-else-if="viewer === 'editor'">
{{ __('Editing') }}
</template>
<template v-else>
{{ __('Reviewing') }}
</template>
<icon
name="angle-down"
:size="12"
css-classes="caret-down"
/>
</button> </button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left"> <div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul> <ul>
<template v-if="mergeRequestId">
<li> <li>
<a <a
href="#" href="#"
@click.prevent="changeMode('mrdiff')" @click.prevent="changeMode($options.viewerTypes.mr)"
:class="{ :class="{
'is-active': viewer === 'mrdiff', 'is-active': viewer === $options.viewerTypes.mr,
}" }"
> >
<strong class="dropdown-menu-inner-title"> <strong class="dropdown-menu-inner-title">
...@@ -90,32 +58,12 @@ export default { ...@@ -90,32 +58,12 @@ export default {
</span> </span>
</a> </a>
</li> </li>
<li
role="separator"
class="divider"
>
</li>
</template>
<li>
<a
href="#"
@click.prevent="changeMode('editor')"
:class="{
'is-active': viewer === 'editor',
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong>
<span class="dropdown-menu-inner-content">
{{ __('View and edit lines') }}
</span>
</a>
</li>
<li> <li>
<a <a
href="#" href="#"
@click.prevent="changeMode('diff')" @click.prevent="changeMode($options.viewerTypes.diff)"
:class="{ :class="{
'is-active': viewer === 'diff', 'is-active': viewer === $options.viewerTypes.diff,
}" }"
> >
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong> <strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
......
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import Mousetrap from 'mousetrap';
import Mousetrap from 'mousetrap'; import { mapActions, mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue'; import IdeSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue'; import RepoTabs from './repo_tabs.vue';
import repoTabs from './repo_tabs.vue'; import IdeStatusBar from './ide_status_bar.vue';
import ideStatusBar from './ide_status_bar.vue'; import RepoEditor from './repo_editor.vue';
import repoEditor from './repo_editor.vue'; import FindFile from './file_finder/index.vue';
import FindFile from './file_finder/index.vue';
const originalStopCallback = Mousetrap.stopCallback; const originalStopCallback = Mousetrap.stopCallback;
export default { export default {
components: { components: {
ideSidebar, IdeSidebar,
ideContextbar, RepoTabs,
repoTabs, IdeStatusBar,
ideStatusBar, RepoEditor,
repoEditor,
FindFile, FindFile,
}, },
props: {
emptyStateSvgPath: {
type: String,
required: true,
},
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: { computed: {
...mapState([ ...mapState([
'changedFiles', 'changedFiles',
...@@ -40,6 +24,7 @@ ...@@ -40,6 +24,7 @@
'viewer', 'viewer',
'currentMergeRequestId', 'currentMergeRequestId',
'fileFindVisible', 'fileFindVisible',
'emptyStateSvgPath',
]), ]),
...mapGetters(['activeFile', 'hasChanges']), ...mapGetters(['activeFile', 'hasChanges']),
}, },
...@@ -76,10 +61,11 @@ ...@@ -76,10 +61,11 @@
return originalStopCallback(e, el, combo); return originalStopCallback(e, el, combo);
}, },
}, },
}; };
</script> </script>
<template> <template>
<article class="ide">
<div <div
class="ide-view" class="ide-view"
> >
...@@ -104,9 +90,6 @@ ...@@ -104,9 +90,6 @@
class="multi-file-edit-pane-content" class="multi-file-edit-pane-content"
:file="activeFile" :file="activeFile"
/> />
<ide-status-bar
:file="activeFile"
/>
</template> </template>
<template <template
v-else v-else
...@@ -136,9 +119,9 @@ ...@@ -136,9 +119,9 @@
</div> </div>
</template> </template>
</div> </div>
<ide-contextbar
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
/>
</div> </div>
<ide-status-bar
:file="activeFile"
/>
</article>
</template> </template>
<script>
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import repoCommitSection from './repo_commit_section.vue';
import ResizablePanel from './resizable_panel.vue';
export default {
components: {
repoCommitSection,
icon,
panelResizer,
ResizablePanel,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
};
</script>
<template>
<resizable-panel
:collapsible="true"
:initial-width="340"
side="right"
>
<div
class="multi-file-commit-panel-section"
>
<repo-commit-section
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
/>
</div>
</resizable-panel>
</template>
<script>
import icon from '~/vue_shared/components/icon.vue';
export default {
components: {
icon,
},
props: {
projectUrl: {
type: String,
required: true,
},
},
computed: {
goBackUrl() {
return document.referrer || this.projectUrl;
},
},
};
</script>
<template>
<nav
class="ide-external-links"
v-once
>
<p>
<a
:href="goBackUrl"
class="ide-sidebar-link"
>
<icon
:size="16"
class="append-right-8"
name="go-back"
/>
<span class="ide-external-links-text">
{{ s__('Go back') }}
</span>
</a>
</p>
</nav>
</template>
<script>
import icon from '~/vue_shared/components/icon.vue';
import repoTree from './ide_repo_tree.vue';
import newDropdown from './new_dropdown/index.vue';
export default {
components: {
repoTree,
icon,
newDropdown,
},
props: {
projectId: {
type: String,
required: true,
},
branch: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="branch-container">
<div class="branch-header">
<div class="branch-header-title str-truncated ref-name">
<icon
name="branch"
:size="12"
/>
{{ branch.name }}
</div>
<div class="branch-header-btns">
<new-dropdown
:project-id="projectId"
:branch="branch.name"
path=""
/>
</div>
</div>
<repo-tree
:tree="branch.tree"
/>
</div>
</template>
<script>
import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
import Identicon from '../../vue_shared/components/identicon.vue';
import BranchesTree from './ide_project_branches_tree.vue';
import ExternalLinks from './ide_external_links.vue';
export default {
components: {
BranchesTree,
ExternalLinks,
ProjectAvatarImage,
Identicon,
},
props: {
project: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="projects-sidebar">
<div class="context-header">
<a
:title="project.name"
:href="project.web_url"
>
<div
v-if="project.avatar_url"
class="avatar-container s40 project-avatar"
>
<project-avatar-image
class="avatar-container project-avatar"
:link-href="project.path"
:img-src="project.avatar_url"
:img-alt="project.name"
:img-size="40"
/>
</div>
<identicon
v-else
size-class="s40"
:entity-id="project.id"
:entity-name="project.name"
/>
<div class="sidebar-context-title">
{{ project.name }}
</div>
</a>
</div>
<external-links
:project-url="project.web_url"
/>
<div class="multi-file-commit-panel-inner-scroll">
<branches-tree
v-for="branch in project.branches"
:key="branch.name"
:project-id="project.path_with_namespace"
:branch="branch"
/>
</div>
</div>
</template>
<script>
import { mapGetters, mapState, mapActions } from 'vuex';
import IdeTreeList from './ide_tree_list.vue';
import EditorModeDropdown from './editor_mode_dropdown.vue';
import { viewerTypes } from '../constants';
export default {
components: {
IdeTreeList,
EditorModeDropdown,
},
computed: {
...mapGetters(['currentMergeRequest']),
...mapState(['viewer']),
showLatestChangesText() {
return !this.currentMergeRequest || this.viewer === viewerTypes.diff;
},
showMergeRequestText() {
return this.currentMergeRequest && this.viewer === viewerTypes.mr;
},
},
mounted() {
this.$nextTick(() => {
this.updateViewer(this.currentMergeRequest ? viewerTypes.mr : viewerTypes.diff);
});
},
methods: {
...mapActions(['updateViewer']),
},
};
</script>
<template>
<ide-tree-list
:viewer-type="viewer"
header-class="ide-review-header"
:disable-action-dropdown="true"
>
<template
slot="header"
>
<div class="ide-review-button-holder">
{{ __('Review') }}
<editor-mode-dropdown
v-if="currentMergeRequest"
:viewer="viewer"
:merge-request-id="currentMergeRequest.iid"
@click="updateViewer"
/>
</div>
<div class="prepend-top-5 ide-review-sub-header">
<template v-if="showLatestChangesText">
{{ __('Latest changes') }}
</template>
<template v-else-if="showMergeRequestText">
{{ __('Merge request') }}
(<a :href="currentMergeRequest.web_url">!{{ currentMergeRequest.iid }}</a>)
</template>
</div>
</template>
</ide-tree-list>
</template>
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue'; import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue'; import Icon from '~/vue_shared/components/icon.vue';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import tooltip from '~/vue_shared/directives/tooltip';
import projectTree from './ide_project_tree.vue'; import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import ResizablePanel from './resizable_panel.vue'; import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import Identicon from '../../vue_shared/components/identicon.vue';
import IdeTree from './ide_tree.vue';
import ResizablePanel from './resizable_panel.vue';
import ActivityBar from './activity_bar.vue';
import CommitSection from './repo_commit_section.vue';
import CommitForm from './commit_sidebar/form.vue';
import IdeReview from './ide_review.vue';
import SuccessMessage from './commit_sidebar/success_message.vue';
import { activityBarViews } from '../constants';
export default { export default {
directives: {
tooltip,
},
components: { components: {
projectTree, Icon,
icon, PanelResizer,
panelResizer, SkeletonLoadingContainer,
skeletonLoadingContainer,
ResizablePanel, ResizablePanel,
ActivityBar,
ProjectAvatarImage,
Identicon,
CommitSection,
IdeTree,
CommitForm,
IdeReview,
SuccessMessage,
},
data() {
return {
showTooltip: false,
};
}, },
computed: { computed: {
...mapState([ ...mapState([
'loading', 'loading',
'currentBranchId',
'currentActivityView',
'changedFiles',
'stagedFiles',
'lastCommitMsg',
]), ]),
...mapGetters([ ...mapGetters(['currentProject', 'someUncommitedChanges']),
'projectsWithTrees', showSuccessMessage() {
]), return (
this.currentActivityView === activityBarViews.edit &&
(this.lastCommitMsg && !this.someUncommitedChanges)
);
}, },
}; branchTooltipTitle() {
return this.showTooltip ? this.currentBranchId : undefined;
},
},
watch: {
currentBranchId() {
this.$nextTick(() => {
this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth;
});
},
},
};
</script> </script>
<template> <template>
<resizable-panel <resizable-panel
:collapsible="false" :collapsible="false"
:initial-width="290" :initial-width="340"
side="left" side="left"
> >
<activity-bar
v-if="!loading"
/>
<div class="multi-file-commit-panel-inner"> <div class="multi-file-commit-panel-inner">
<template v-if="loading"> <template v-if="loading">
<div <div
...@@ -41,11 +87,54 @@ ...@@ -41,11 +87,54 @@
<skeleton-loading-container /> <skeleton-loading-container />
</div> </div>
</template> </template>
<project-tree <template v-else>
v-for="project in projectsWithTrees" <div class="context-header ide-context-header">
:key="project.id" <a
:project="project" :href="currentProject.web_url"
>
<div
v-if="currentProject.avatar_url"
class="avatar-container s40 project-avatar"
>
<project-avatar-image
class="avatar-container project-avatar"
:link-href="currentProject.path"
:img-src="currentProject.avatar_url"
:img-alt="currentProject.name"
:img-size="40"
/>
</div>
<identicon
v-else
size-class="s40"
:entity-id="currentProject.id"
:entity-name="currentProject.name"
/>
<div class="ide-sidebar-project-title">
<div class="sidebar-context-title">
{{ currentProject.name }}
</div>
<div
class="sidebar-context-title ide-sidebar-branch-title"
ref="branchId"
v-tooltip
:title="branchTooltipTitle"
>
<icon
name="branch"
css-classes="append-right-5"
/>{{ currentBranchId }}
</div>
</div>
</a>
</div>
<div class="multi-file-commit-panel-inner-scroll">
<component
:is="currentActivityView"
/> />
</div> </div>
<commit-form />
</template>
</div>
</resizable-panel> </resizable-panel>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago'; import timeAgoMixin from '~/vue_shared/mixins/timeago';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default { export default {
components: { components: {
icon, icon,
userAvatarImage,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -14,40 +17,93 @@ export default { ...@@ -14,40 +17,93 @@ export default {
props: { props: {
file: { file: {
type: Object, type: Object,
required: true, required: false,
default: null,
},
},
data() {
return {
lastCommitFormatedAge: null,
};
},
computed: {
...mapGetters(['currentProject', 'lastCommit']),
},
mounted() {
this.startTimer();
},
beforeDestroy() {
if (this.intervalId) {
clearInterval(this.intervalId);
}
},
methods: {
startTimer() {
this.intervalId = setInterval(() => {
this.commitAgeUpdate();
}, 1000);
},
commitAgeUpdate() {
if (this.lastCommit) {
this.lastCommitFormatedAge = this.timeFormated(this.lastCommit.committed_date);
}
},
getCommitPath(shortSha) {
return `${this.currentProject.web_url}/commit/${shortSha}`;
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="ide-status-bar"> <footer class="ide-status-bar">
<div> <div
<div v-if="file.lastCommit && file.lastCommit.id"> class="ide-status-branch"
Last commit: v-if="lastCommit && lastCommitFormatedAge"
>
<icon
name="commit"
/>
<a <a
v-tooltip v-tooltip
:title="file.lastCommit.message" class="commit-sha"
:href="file.lastCommit.url" :title="lastCommit.message"
:href="getCommitPath(lastCommit.short_id)"
>{{ lastCommit.short_id }}</a>
by
{{ lastCommit.author_name }}
<time
v-tooltip
data-placement="top"
data-container="body"
:datetime="lastCommit.committed_date"
:title="tooltipTitle(lastCommit.committed_date)"
> >
{{ timeFormated(file.lastCommit.updatedAt) }} by {{ lastCommitFormatedAge }}
{{ file.lastCommit.author }} </time>
</a>
</div>
</div> </div>
<div class="text-right"> <div
v-if="file"
class="ide-status-file"
>
{{ file.name }} {{ file.name }}
</div> </div>
<div class="text-right"> <div
v-if="file"
class="ide-status-file"
>
{{ file.eol }} {{ file.eol }}
</div> </div>
<div <div
class="text-right" class="ide-status-file"
v-if="!file.binary"> v-if="file && !file.binary">
{{ file.editorRow }}:{{ file.editorColumn }} {{ file.editorRow }}:{{ file.editorColumn }}
</div> </div>
<div class="text-right"> <div
v-if="file"
class="ide-status-file"
>
{{ file.fileLanguage }} {{ file.fileLanguage }}
</div> </div>
</div> </footer>
</template> </template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import NewDropdown from './new_dropdown/index.vue';
import IdeTreeList from './ide_tree_list.vue';
export default {
components: {
NewDropdown,
IdeTreeList,
},
computed: {
...mapState(['currentBranchId']),
...mapGetters(['currentProject', 'currentTree', 'activeFile']),
},
mounted() {
if (this.activeFile && this.activeFile.pending) {
this.$router.push(`/project${this.activeFile.url}`, () => {
this.updateViewer('editor');
});
}
},
methods: {
...mapActions(['updateViewer']),
},
};
</script>
<template>
<ide-tree-list
viewer-type="editor"
>
<template
slot="header"
>
{{ __('Edit') }}
<new-dropdown
:project-id="currentProject.name_with_namespace"
:branch="currentBranchId"
/>
</template>
</ide-tree-list>
</template>
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import RepoFile from './repo_file.vue'; import RepoFile from './repo_file.vue';
import NewDropdown from './new_dropdown/index.vue';
export default { export default {
components: { components: {
Icon,
RepoFile, RepoFile,
SkeletonLoadingContainer, SkeletonLoadingContainer,
NewDropdown,
}, },
props: { props: {
tree: { viewerType: {
type: Object, type: String,
required: true, required: true,
}, },
headerClass: {
type: String,
required: false,
default: null,
},
disableActionDropdown: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState(['currentBranchId']),
...mapGetters(['currentProject', 'currentTree']),
showLoading() {
return !this.currentTree || this.currentTree.loading;
},
},
mounted() {
this.updateViewer(this.viewerType);
},
methods: {
...mapActions(['updateViewer']),
}, },
}; };
</script> </script>
...@@ -20,7 +48,7 @@ export default { ...@@ -20,7 +48,7 @@ export default {
<div <div
class="ide-file-list" class="ide-file-list"
> >
<template v-if="tree.loading"> <template v-if="showLoading">
<div <div
class="multi-file-loading-container" class="multi-file-loading-container"
v-for="n in 3" v-for="n in 3"
...@@ -30,11 +58,18 @@ export default { ...@@ -30,11 +58,18 @@ export default {
</div> </div>
</template> </template>
<template v-else> <template v-else>
<header
class="ide-tree-header"
:class="headerClass"
>
<slot name="header"></slot>
</header>
<repo-file <repo-file
v-for="file in tree.tree" v-for="file in currentTree.tree"
:key="file.key" :key="file.key"
:file="file" :file="file"
:level="0" :level="0"
:disable-action-dropdown="disableActionDropdown"
/> />
</template> </template>
</div> </div>
......
...@@ -17,7 +17,8 @@ export default { ...@@ -17,7 +17,8 @@ export default {
}, },
path: { path: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
}, },
data() { data() {
......
...@@ -3,13 +3,10 @@ import { mapState, mapActions, mapGetters } from 'vuex'; ...@@ -3,13 +3,10 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitFilesList from './commit_sidebar/list.vue'; import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue'; import EmptyState from './commit_sidebar/empty_state.vue';
import CommitMessageField from './commit_sidebar/message_field.vue';
import SuccessMessage from './commit_sidebar/success_message.vue';
import * as consts from '../stores/modules/commit/constants'; import * as consts from '../stores/modules/commit/constants';
import Actions from './commit_sidebar/actions.vue'; import { activityBarViews } from '../constants';
export default { export default {
components: { components: {
...@@ -17,42 +14,50 @@ export default { ...@@ -17,42 +14,50 @@ export default {
Icon, Icon,
CommitFilesList, CommitFilesList,
EmptyState, EmptyState,
SuccessMessage,
Actions,
LoadingButton,
CommitMessageField,
}, },
directives: { directives: {
tooltip, tooltip,
}, },
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: { computed: {
...mapState([
'changedFiles',
'stagedFiles',
'rightPanelCollapsed',
'lastCommitMsg',
'unusedSeal',
]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
showStageUnstageArea() { showStageUnstageArea() {
return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal); return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
}, },
someUncommitedChanges() {
return !!(this.changedFiles.length || this.stagedFiles.length);
}, },
...mapState(['changedFiles', 'stagedFiles', 'rightPanelCollapsed', 'lastCommitMsg', 'unusedSeal']), watch: {
...mapState('commit', ['commitMessage', 'submitCommitLoading']), hasChanges() {
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']), if (!this.hasChanges) {
this.updateActivityBarView(activityBarViews.edit);
}
},
},
mounted() {
if (this.lastOpenedFile) {
this.openPendingTab({
file: this.lastOpenedFile,
})
.then(changeViewer => {
if (changeViewer) {
this.updateViewer('diff');
}
})
.catch(e => {
throw e;
});
}
}, },
methods: { methods: {
...mapActions('commit', [ ...mapActions(['openPendingTab', 'updateViewer', 'updateActivityBarView']),
'updateCommitMessage', ...mapActions('commit', ['commitChanges', 'updateCommitAction']),
'discardDraft',
'commitChanges',
'updateCommitAction',
]),
forceCreateNewBranch() { forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges()); return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
}, },
...@@ -80,6 +85,7 @@ export default { ...@@ -80,6 +85,7 @@ export default {
v-if="showStageUnstageArea" v-if="showStageUnstageArea"
> >
<commit-files-list <commit-files-list
class="is-first"
icon-name="unstaged" icon-name="unstaged"
:title="__('Unstaged')" :title="__('Unstaged')"
:file-list="changedFiles" :file-list="changedFiles"
...@@ -94,49 +100,11 @@ export default { ...@@ -94,49 +100,11 @@ export default {
action="unstageAllChanges" action="unstageAllChanges"
:action-btn-text="__('Unstage all')" :action-btn-text="__('Unstage all')"
item-action-component="unstage-button" item-action-component="unstage-button"
:show-toggle="false"
:staged-list="true" :staged-list="true"
/> />
</template> </template>
<empty-state <empty-state
v-if="unusedSeal" v-if="unusedSeal"
:no-changes-state-svg-path="noChangesStateSvgPath"
/>
<div
class="multi-file-commit-panel-bottom"
>
<form
class="form-horizontal multi-file-commit-form"
@submit.prevent.stop="commitChanges"
v-if="!rightPanelCollapsed"
>
<success-message
v-if="lastCommitMsg && !someUncommitedChanges"
:committed-state-svg-path="committedStateSvgPath"
/> />
<commit-message-field
:text="commitMessage"
@input="updateCommitMessage"
/>
<div class="clearfix prepend-top-15">
<actions />
<loading-button
:loading="submitCommitLoading"
:disabled="commitButtonDisabled"
container-class="btn btn-success btn-sm pull-left"
:label="__('Commit')"
@click="commitChanges"
/>
<button
v-if="!discardDraftButtonDisabled"
type="button"
class="btn btn-default btn-sm pull-right"
@click="discardDraft"
>
{{ __('Discard draft') }}
</button>
</div>
</form>
</div>
</div> </div>
</template> </template>
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash'; import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import { activityBarViews, viewerTypes } from '../constants';
import monacoLoader from '../monaco_loader'; import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor'; import Editor from '../lib/editor';
import IdeFileButtons from './ide_file_buttons.vue'; import IdeFileButtons from './ide_file_buttons.vue';
...@@ -19,8 +20,14 @@ export default { ...@@ -19,8 +20,14 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']), ...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']),
...mapGetters(['currentMergeRequest', 'getStagedFile']), ...mapGetters([
'currentMergeRequest',
'getStagedFile',
'isEditModeActive',
'isCommitModeActive',
'isReviewModeActive',
]),
shouldHideEditor() { shouldHideEditor() {
return this.file && this.file.binary && !this.file.content; return this.file && this.file.binary && !this.file.content;
}, },
...@@ -40,6 +47,21 @@ export default { ...@@ -40,6 +47,21 @@ export default {
// Compare key to allow for files opened in review mode to be cached differently // Compare key to allow for files opened in review mode to be cached differently
if (newVal.key !== this.file.key) { if (newVal.key !== this.file.key) {
this.initMonaco(); this.initMonaco();
if (this.currentActivityView !== activityBarViews.edit) {
this.setFileViewMode({
file: this.file,
viewMode: 'edit',
});
}
}
},
currentActivityView() {
if (this.currentActivityView !== activityBarViews.edit) {
this.setFileViewMode({
file: this.file,
viewMode: 'edit',
});
} }
}, },
rightPanelCollapsed() { rightPanelCollapsed() {
...@@ -77,7 +99,6 @@ export default { ...@@ -77,7 +99,6 @@ export default {
'setFileViewMode', 'setFileViewMode',
'setFileEOL', 'setFileEOL',
'updateViewer', 'updateViewer',
'updateDelayViewerUpdated',
]), ]),
initMonaco() { initMonaco() {
if (this.shouldHideEditor) return; if (this.shouldHideEditor) return;
...@@ -89,14 +110,6 @@ export default { ...@@ -89,14 +110,6 @@ export default {
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '', baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
}) })
.then(() => { .then(() => {
const viewerPromise = this.delayViewerUpdated
? this.updateViewer(this.file.pending ? 'diff' : 'editor')
: Promise.resolve();
return viewerPromise;
})
.then(() => {
this.updateDelayViewerUpdated(false);
this.createEditorInstance(); this.createEditorInstance();
}) })
.catch(err => { .catch(err => {
...@@ -108,10 +121,10 @@ export default { ...@@ -108,10 +121,10 @@ export default {
this.editor.dispose(); this.editor.dispose();
this.$nextTick(() => { this.$nextTick(() => {
if (this.viewer === 'editor') { if (this.viewer === viewerTypes.edit) {
this.editor.createInstance(this.$refs.editor); this.editor.createInstance(this.$refs.editor);
} else { } else {
this.editor.createDiffInstance(this.$refs.editor); this.editor.createDiffInstance(this.$refs.editor, !this.isReviewModeActive);
} }
this.setupEditor(); this.setupEditor();
...@@ -127,7 +140,7 @@ export default { ...@@ -127,7 +140,7 @@ export default {
this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null, this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null,
); );
if (this.viewer === 'mrdiff') { if (this.viewer === viewerTypes.mr) {
this.editor.attachMergeRequestModel(this.model); this.editor.attachMergeRequestModel(this.model);
} else { } else {
this.editor.attachModel(this.model); this.editor.attachModel(this.model);
...@@ -168,6 +181,7 @@ export default { ...@@ -168,6 +181,7 @@ export default {
}); });
}, },
}, },
viewerTypes,
}; };
</script> </script>
...@@ -176,16 +190,17 @@ export default { ...@@ -176,16 +190,17 @@ export default {
id="ide" id="ide"
class="blob-viewer-container blob-editor-container" class="blob-viewer-container blob-editor-container"
> >
<div class="ide-mode-tabs clearfix"> <div class="ide-mode-tabs clearfix" >
<ul <ul
class="nav-links pull-left" class="nav-links pull-left"
v-if="!shouldHideEditor"> v-if="!shouldHideEditor && isEditModeActive"
>
<li :class="editTabCSS"> <li :class="editTabCSS">
<a <a
href="javascript:void(0);" href="javascript:void(0);"
role="button" role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'edit' })"> @click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
<template v-if="viewer === 'editor'"> <template v-if="viewer === $options.viewerTypes.edit">
{{ __('Edit') }} {{ __('Edit') }}
</template> </template>
<template v-else> <template v-else>
...@@ -212,6 +227,9 @@ export default { ...@@ -212,6 +227,9 @@ export default {
v-show="!shouldHideEditor && file.viewMode === 'edit'" v-show="!shouldHideEditor && file.viewMode === 'edit'"
ref="editor" ref="editor"
class="multi-file-editor-holder" class="multi-file-editor-holder"
:class="{
'is-readonly': isCommitModeActive,
}"
> >
</div> </div>
<content-viewer <content-viewer
......
...@@ -34,6 +34,11 @@ export default { ...@@ -34,6 +34,11 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
disableActionDropdown: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
...mapGetters([ ...mapGetters([
...@@ -99,16 +104,14 @@ export default { ...@@ -99,16 +104,14 @@ export default {
} }
}, },
methods: { methods: {
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']), ...mapActions(['toggleTreeOpen']),
clickFile() { clickFile() {
// Manual Action if a tree is selected/opened // Manual Action if a tree is selected/opened
if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) { if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.toggleTreeOpen(this.file.path); this.toggleTreeOpen(this.file.path);
} }
return this.updateDelayViewerUpdated(true).then(() => {
router.push(`/project${this.file.url}`); router.push(`/project${this.file.url}`);
});
}, },
}, },
}; };
...@@ -170,7 +173,7 @@ export default { ...@@ -170,7 +173,7 @@ export default {
/> />
</span> </span>
<new-dropdown <new-dropdown
v-if="isTree" v-if="isTree && !disableActionDropdown"
:project-id="file.projectId" :project-id="file.projectId"
:branch="file.branchId" :branch="file.branchId"
:path="file.path" :path="file.path"
......
...@@ -32,6 +32,8 @@ export default { ...@@ -32,6 +32,8 @@ export default {
return `Close ${this.tab.name}`; return `Close ${this.tab.name}`;
}, },
showChangedIcon() { showChangedIcon() {
if (this.tab.pending) return true;
return this.fileHasChanged ? !this.tabMouseOver : false; return this.fileHasChanged ? !this.tabMouseOver : false;
}, },
fileHasChanged() { fileHasChanged() {
...@@ -66,15 +68,32 @@ export default { ...@@ -66,15 +68,32 @@ export default {
<template> <template>
<li <li
:class="{
active: tab.active
}"
@click="clickFile(tab)" @click="clickFile(tab)"
@mouseover="mouseOverTab" @mouseover="mouseOverTab"
@mouseout="mouseOutTab" @mouseout="mouseOutTab"
> >
<div
class="multi-file-tab"
:title="tab.url"
>
<file-icon
:file-name="tab.name"
:size="16"
/>
{{ tab.name }}
<file-status-icon
:file="tab"
/>
</div>
<button <button
type="button" type="button"
class="multi-file-tab-close" class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)" @click.stop.prevent="closeFile(tab)"
:aria-label="closeLabel" :aria-label="closeLabel"
:disabled="tab.pending"
> >
<icon <icon
v-if="!showChangedIcon" v-if="!showChangedIcon"
...@@ -87,22 +106,5 @@ export default { ...@@ -87,22 +106,5 @@ export default {
:force-modified-icon="true" :force-modified-icon="true"
/> />
</button> </button>
<div
class="multi-file-tab"
:class="{
active: tab.active
}"
:title="tab.url"
>
<file-icon
:file-name="tab.name"
:size="16"
/>
{{ tab.name }}
<file-status-icon
:file="tab"
/>
</div>
</li> </li>
</template> </template>
...@@ -32,16 +32,6 @@ export default { ...@@ -32,16 +32,6 @@ export default {
default: '', default: '',
}, },
}, },
data() {
return {
showShadow: false,
};
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: { methods: {
...mapActions(['updateViewer', 'removePendingTab']), ...mapActions(['updateViewer', 'removePendingTab']),
openFileViewer(viewer) { openFileViewer(viewer) {
...@@ -71,12 +61,5 @@ export default { ...@@ -71,12 +61,5 @@ export default {
:tab="tab" :tab="tab"
/> />
</ul> </ul>
<editor-mode
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
:merge-request-id="mergeRequestId"
@click="openFileViewer"
/>
</div> </div>
</template> </template>
...@@ -3,6 +3,22 @@ export const MAX_FILE_FINDER_RESULTS = 40; ...@@ -3,6 +3,22 @@ export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55; export const FILE_FINDER_ROW_HEIGHT = 55;
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33; export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
export const MAX_WINDOW_HEIGHT_COMPACT = 750;
export const COMMIT_ITEM_PADDING = 32;
// Commit message textarea // Commit message textarea
export const MAX_TITLE_LENGTH = 50; export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72; export const MAX_BODY_LENGTH = 72;
export const activityBarViews = {
edit: 'ide-tree',
commit: 'commit-section',
review: 'ide-review',
};
export const viewerTypes = {
mr: 'mrdiff',
edit: 'editor',
diff: 'diff',
};
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import flash from '~/flash'; import flash from '~/flash';
import store from './stores'; import store from './stores';
import { activityBarViews } from './constants';
Vue.use(VueRouter); Vue.use(VueRouter);
...@@ -63,6 +64,8 @@ router.beforeEach((to, from, next) => { ...@@ -63,6 +64,8 @@ router.beforeEach((to, from, next) => {
const fullProjectId = `${to.params.namespace}/${to.params.project}`; const fullProjectId = `${to.params.namespace}/${to.params.project}`;
if (to.params.branch) { if (to.params.branch) {
store.dispatch('setCurrentBranchId', to.params.branch);
store.dispatch('getBranchData', { store.dispatch('getBranchData', {
projectId: fullProjectId, projectId: fullProjectId,
branchId: to.params.branch, branchId: to.params.branch,
...@@ -99,14 +102,14 @@ router.beforeEach((to, from, next) => { ...@@ -99,14 +102,14 @@ router.beforeEach((to, from, next) => {
throw e; throw e;
}); });
} else if (to.params.mrid) { } else if (to.params.mrid) {
store.dispatch('updateViewer', 'mrdiff');
store store
.dispatch('getMergeRequestData', { .dispatch('getMergeRequestData', {
projectId: fullProjectId, projectId: fullProjectId,
mergeRequestId: to.params.mrid, mergeRequestId: to.params.mrid,
}) })
.then(mr => { .then(mr => {
store.dispatch('updateActivityBarView', activityBarViews.review);
store.dispatch('getBranchData', { store.dispatch('getBranchData', {
projectId: fullProjectId, projectId: fullProjectId,
branchId: mr.source_branch, branchId: mr.source_branch,
......
...@@ -16,15 +16,16 @@ export function initIde(el) { ...@@ -16,15 +16,16 @@ export function initIde(el) {
components: { components: {
ide, ide,
}, },
render(createElement) { created() {
return createElement('ide', { this.$store.dispatch('setEmptyStateSvgs', {
props: {
emptyStateSvgPath: el.dataset.emptyStateSvgPath, emptyStateSvgPath: el.dataset.emptyStateSvgPath,
noChangesStateSvgPath: el.dataset.noChangesStateSvgPath, noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
committedStateSvgPath: el.dataset.committedStateSvgPath, committedStateSvgPath: el.dataset.committedStateSvgPath,
},
}); });
}, },
render(createElement) {
return createElement('ide');
},
}); });
} }
......
...@@ -61,19 +61,19 @@ export default class Editor { ...@@ -61,19 +61,19 @@ export default class Editor {
} }
} }
createDiffInstance(domElement) { createDiffInstance(domElement, readOnly = true) {
if (!this.instance) { if (!this.instance) {
clearDomElement(domElement); clearDomElement(domElement);
this.disposable.add( this.disposable.add(
(this.instance = this.monaco.editor.createDiffEditor(domElement, { (this.instance = this.monaco.editor.createDiffEditor(domElement, {
...defaultEditorOptions, ...defaultEditorOptions,
readOnly: true,
quickSuggestions: false, quickSuggestions: false,
occurrencesHighlight: false, occurrencesHighlight: false,
renderLineHighlight: 'none',
hideCursorInOverviewRuler: true,
renderSideBySide: Editor.renderSideBySide(domElement), renderSideBySide: Editor.renderSideBySide(domElement),
readOnly,
renderLineHighlight: readOnly ? 'all' : 'none',
hideCursorInOverviewRuler: !readOnly,
})), })),
); );
......
...@@ -123,6 +123,8 @@ export const scrollToTab = () => { ...@@ -123,6 +123,8 @@ export const scrollToTab = () => {
}; };
export const stageAllChanges = ({ state, commit }) => { export const stageAllChanges = ({ state, commit }) => {
commit(types.SET_LAST_COMMIT_MSG, '');
state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path)); state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
}; };
...@@ -138,6 +140,18 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => { ...@@ -138,6 +140,18 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay); commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
}; };
export const updateActivityBarView = ({ commit }, view) => {
commit(types.UPDATE_ACTIVITY_BAR_VIEW, view);
};
export const setEmptyStateSvgs = ({ commit }, svgs) => {
commit(types.SET_EMPTY_STATE_SVGS, svgs);
};
export const setCurrentBranchId = ({ commit }, currentBranchId) => {
commit(types.SET_CURRENT_BRANCH, currentBranchId);
};
export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => { export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => {
commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile }); commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile });
......
...@@ -5,6 +5,7 @@ import service from '../../services'; ...@@ -5,6 +5,7 @@ import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import router from '../../ide_router'; import router from '../../ide_router';
import { setPageTitle } from '../utils'; import { setPageTitle } from '../utils';
import { viewerTypes } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => { export const closeFile = ({ commit, state, dispatch }, file) => {
const path = file.path; const path = file.path;
...@@ -23,13 +24,12 @@ export const closeFile = ({ commit, state, dispatch }, file) => { ...@@ -23,13 +24,12 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
const nextFileToOpen = state.openFiles[nextIndexToOpen]; const nextFileToOpen = state.openFiles[nextIndexToOpen];
if (nextFileToOpen.pending) { if (nextFileToOpen.pending) {
dispatch('updateViewer', 'diff'); dispatch('updateViewer', viewerTypes.diff);
dispatch('openPendingTab', { dispatch('openPendingTab', {
file: nextFileToOpen, file: nextFileToOpen,
keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged', keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
}); });
} else { } else {
dispatch('updateDelayViewerUpdated', true);
router.push(`/project${nextFileToOpen.url}`); router.push(`/project${nextFileToOpen.url}`);
} }
} else if (!state.openFiles.length) { } else if (!state.openFiles.length) {
...@@ -184,6 +184,7 @@ export const stageChange = ({ commit, state }, path) => { ...@@ -184,6 +184,7 @@ export const stageChange = ({ commit, state }, path) => {
const stagedFile = state.stagedFiles.find(f => f.path === path); const stagedFile = state.stagedFiles.find(f => f.path === path);
commit(types.STAGE_CHANGE, path); commit(types.STAGE_CHANGE, path);
commit(types.SET_LAST_COMMIT_MSG, '');
if (stagedFile) { if (stagedFile) {
eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content); eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
...@@ -195,9 +196,7 @@ export const unstageChange = ({ commit }, path) => { ...@@ -195,9 +196,7 @@ export const unstageChange = ({ commit }, path) => {
}; };
export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => { export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => {
if (getters.activeFile && getters.activeFile === file && state.viewer === 'diff') { state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`));
return false;
}
commit(types.ADD_PENDING_TAB, { file, keyPrefix }); commit(types.ADD_PENDING_TAB, { file, keyPrefix });
......
...@@ -55,7 +55,6 @@ export const getBranchData = ( ...@@ -55,7 +55,6 @@ export const getBranchData = (
branch: data, branch: data,
}); });
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
commit(types.SET_CURRENT_BRANCH, branchId);
resolve(data); resolve(data);
}) })
.catch(() => { .catch(() => {
...@@ -73,3 +72,26 @@ export const getBranchData = ( ...@@ -73,3 +72,26 @@ export const getBranchData = (
resolve(state.projects[`${projectId}`].branches[branchId]); resolve(state.projects[`${projectId}`].branches[branchId]);
} }
}); });
export const refreshLastCommitData = (
{ commit, state, dispatch },
{ projectId, branchId } = {},
) => service
.getBranchData(projectId, branchId)
.then(({ data }) => {
commit(types.SET_BRANCH_COMMIT, {
projectId,
branchId,
commit: data.commit,
});
})
.catch(() => {
flash(
'Error loading last commit.',
'alert',
document,
null,
false,
true,
);
});
import { __ } from '~/locale';
import { getChangesCountForFiles, filePathMatches } from './utils'; import { getChangesCountForFiles, filePathMatches } from './utils';
import { activityBarViews } from '../constants';
export const activeFile = state => state.openFiles.find(file => file.active) || null; export const activeFile = state => state.openFiles.find(file => file.active) || null;
...@@ -31,15 +31,12 @@ export const currentMergeRequest = state => { ...@@ -31,15 +31,12 @@ export const currentMergeRequest = state => {
return null; return null;
}; };
// eslint-disable-next-line no-confusing-arrow export const currentProject = state => state.projects[state.currentProjectId];
export const collapseButtonIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length; export const currentTree = state =>
state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
// eslint-disable-next-line no-confusing-arrow export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
export const collapseButtonTooltip = state =>
state.rightPanelCollapsed ? __('Expand sidebar') : __('Collapse sidebar');
export const hasMergeRequest = state => !!state.currentMergeRequestId; export const hasMergeRequest = state => !!state.currentMergeRequestId;
...@@ -59,6 +56,16 @@ export const allBlobs = state => ...@@ -59,6 +56,16 @@ export const allBlobs = state =>
export const getChangedFile = state => path => state.changedFiles.find(f => f.path === path); export const getChangedFile = state => path => state.changedFiles.find(f => f.path === path);
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path); export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
export const lastOpenedFile = state =>
[...state.changedFiles, ...state.stagedFiles].sort((a, b) => b.lastOpenedAt - a.lastOpenedAt)[0];
export const isEditModeActive = state => state.currentActivityView === activityBarViews.edit;
export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit;
export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review;
export const someUncommitedChanges = state =>
!!(state.changedFiles.length || state.stagedFiles.length);
export const getChangesInFolder = state => path => { export const getChangesInFolder = state => path => {
const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f, path)).length; const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f, path)).length;
const stagedFilesCount = state.stagedFiles.filter( const stagedFilesCount = state.stagedFiles.filter(
...@@ -74,5 +81,11 @@ export const getUnstagedFilesCountForPath = state => path => ...@@ -74,5 +81,11 @@ export const getUnstagedFilesCountForPath = state => path =>
export const getStagedFilesCountForPath = state => path => export const getStagedFilesCountForPath = state => path =>
getChangesCountForFiles(state.stagedFiles, path); getChangesCountForFiles(state.stagedFiles, path);
export const lastCommit = (state, getters) => {
const branch = getters.currentProject && getters.currentProject.branches[state.currentBranchId];
return branch ? branch.commit : null;
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -8,6 +8,7 @@ import router from '../../../ide_router'; ...@@ -8,6 +8,7 @@ import router from '../../../ide_router';
import service from '../../../services'; import service from '../../../services';
import * as types from './mutation_types'; import * as types from './mutation_types';
import * as consts from './constants'; import * as consts from './constants';
import { activityBarViews } from '../../../constants';
import eventHub from '../../../eventhub'; import eventHub from '../../../eventhub';
export const updateCommitMessage = ({ commit }, message) => { export const updateCommitMessage = ({ commit }, message) => {
...@@ -75,7 +76,7 @@ export const checkCommitStatus = ({ rootState }) => ...@@ -75,7 +76,7 @@ export const checkCommitStatus = ({ rootState }) =>
export const updateFilesAfterCommit = ( export const updateFilesAfterCommit = (
{ commit, dispatch, state, rootState, rootGetters }, { commit, dispatch, state, rootState, rootGetters },
{ data, branch }, { data },
) => { ) => {
const selectedProject = rootState.projects[rootState.currentProjectId]; const selectedProject = rootState.projects[rootState.currentProjectId];
const lastCommit = { const lastCommit = {
...@@ -126,15 +127,9 @@ export const updateFilesAfterCommit = ( ...@@ -126,15 +127,9 @@ export const updateFilesAfterCommit = (
changed: !!changedFile, changed: !!changedFile,
}); });
}); });
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH && rootGetters.activeFile) {
router.push(
`/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`,
);
}
}; };
export const commitChanges = ({ commit, state, getters, dispatch, rootState }) => { export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => {
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH; const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
const payload = createCommitPayload(getters.branchName, newBranch, state, rootState); const payload = createCommitPayload(getters.branchName, newBranch, state, rootState);
const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus'); const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus');
...@@ -187,7 +182,39 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) = ...@@ -187,7 +182,39 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true }); commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 5000); }, 5000);
}) })
.then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH)); .then(() => {
if (rootGetters.lastOpenedFile) {
dispatch(
'openPendingTab',
{
file: rootGetters.lastOpenedFile,
},
{ root: true },
)
.then(changeViewer => {
if (changeViewer) {
dispatch('updateViewer', 'diff', { root: true });
}
})
.catch(e => {
throw e;
});
} else {
dispatch('updateActivityBarView', activityBarViews.edit, { root: true });
dispatch('updateViewer', 'editor', { root: true });
router.push(
`/project/${rootState.currentProjectId}/blob/${getters.branchName}/${
rootGetters.activeFile.path
}`,
);
}
})
.then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH))
.then(() => dispatch('refreshLastCommitData', {
projectId: rootState.currentProjectId,
branchId: rootState.currentBranchId,
}, { root: true }));
}) })
.catch(err => { .catch(err => {
let errMsg = __('Error committing changes. Please try again.'); let errMsg = __('Error committing changes. Please try again.');
......
...@@ -5,6 +5,7 @@ export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG'; ...@@ -5,6 +5,7 @@ export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG';
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED'; export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED'; export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS'; export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS';
// Project Mutation Types // Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT'; export const SET_PROJECT = 'SET_PROJECT';
...@@ -19,6 +20,7 @@ export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS'; ...@@ -19,6 +20,7 @@ export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
// Branch Mutation Types // Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH'; export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE'; export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN'; export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
...@@ -59,6 +61,7 @@ export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT'; ...@@ -59,6 +61,7 @@ export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB'; export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB'; export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW';
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG'; export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL'; export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
...@@ -107,6 +107,21 @@ export default { ...@@ -107,6 +107,21 @@ export default {
delayViewerUpdated, delayViewerUpdated,
}); });
}, },
[types.UPDATE_ACTIVITY_BAR_VIEW](state, currentActivityView) {
Object.assign(state, {
currentActivityView,
});
},
[types.SET_EMPTY_STATE_SVGS](
state,
{ emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath },
) {
Object.assign(state, {
emptyStateSvgPath,
noChangesStateSvgPath,
committedStateSvgPath,
});
},
[types.TOGGLE_FILE_FINDER](state, fileFindVisible) { [types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
Object.assign(state, { Object.assign(state, {
fileFindVisible, fileFindVisible,
......
...@@ -23,4 +23,9 @@ export default { ...@@ -23,4 +23,9 @@ export default {
workingReference: reference, workingReference: reference,
}); });
}, },
[types.SET_BRANCH_COMMIT](state, { projectId, branchId, commit }) {
Object.assign(state.projects[projectId].branches[branchId], {
commit,
});
},
}; };
/* eslint-disable no-param-reassign */
import * as types from '../mutation_types'; import * as types from '../mutation_types';
export default { export default {
...@@ -169,32 +170,24 @@ export default { ...@@ -169,32 +170,24 @@ export default {
}); });
}, },
[types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) { [types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
const key = `${keyPrefix}-${file.key}`; state.entries[file.path].opened = false;
const pendingTab = state.openFiles.find(f => f.key === key && f.pending); state.entries[file.path].active = false;
let openFiles = state.openFiles.map(f => Object.assign(f, { active: false, opened: false })); state.entries[file.path].lastOpenedAt = new Date().getTime();
state.openFiles.forEach(f =>
if (!pendingTab) { Object.assign(f, {
const openFile = openFiles.find(f => f.path === file.path); opened: false,
active: false,
openFiles = openFiles.concat(openFile ? null : file).reduce((acc, f) => { }),
if (!f) return acc; );
state.openFiles = [
if (f.path === file.path) { {
return acc.concat({ ...file,
...f, key: `${keyPrefix}-${file.key}`,
content: file.content,
active: true,
pending: true, pending: true,
opened: true, opened: true,
key, active: true,
}); },
} ];
return acc.concat(f);
}, []);
}
Object.assign(state, { openFiles });
}, },
[types.REMOVE_PENDING_TAB](state, file) { [types.REMOVE_PENDING_TAB](state, file) {
Object.assign(state, { Object.assign(state, {
......
import { activityBarViews, viewerTypes } from '../constants';
export default () => ({ export default () => ({
currentProjectId: '', currentProjectId: '',
currentBranchId: '', currentBranchId: '',
...@@ -16,8 +18,9 @@ export default () => ({ ...@@ -16,8 +18,9 @@ export default () => ({
rightPanelCollapsed: false, rightPanelCollapsed: false,
panelResizing: false, panelResizing: false,
entries: {}, entries: {},
viewer: 'editor', viewer: viewerTypes.edit,
delayViewerUpdated: false, delayViewerUpdated: false,
currentActivityView: activityBarViews.edit,
unusedSeal: true, unusedSeal: true,
fileFindVisible: false, fileFindVisible: false,
}); });
/* eslint-disable no-new */
import $ from 'jquery'; import $ from 'jquery';
import flash from './flash'; import flash from './flash';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
...@@ -62,7 +60,7 @@ export default class MiniPipelineGraph { ...@@ -62,7 +60,7 @@ export default class MiniPipelineGraph {
*/ */
renderBuildsList(stageContainer, data) { renderBuildsList(stageContainer, data) {
const dropdownContainer = stageContainer.parentElement.querySelector( const dropdownContainer = stageContainer.parentElement.querySelector(
`${this.dropdownListSelector} .js-builds-dropdown-list`, `${this.dropdownListSelector} .js-builds-dropdown-list ul`,
); );
dropdownContainer.innerHTML = data; dropdownContainer.innerHTML = data;
......
...@@ -81,9 +81,8 @@ export default { ...@@ -81,9 +81,8 @@ export default {
time: new Date(), time: new Date(),
value: 0, value: 0,
}, },
currentDataIndex: 0,
currentXCoordinate: 0, currentXCoordinate: 0,
currentFlagPosition: 0, currentCoordinates: [],
showFlag: false, showFlag: false,
showFlagContent: false, showFlagContent: false,
timeSeries: [], timeSeries: [],
...@@ -273,6 +272,9 @@ export default { ...@@ -273,6 +272,9 @@ export default {
:line-style="path.lineStyle" :line-style="path.lineStyle"
:line-color="path.lineColor" :line-color="path.lineColor"
:area-color="path.areaColor" :area-color="path.areaColor"
:current-coordinates="currentCoordinates[index]"
:current-time-series-index="index"
:show-dot="showFlagContent"
/> />
<graph-deployment <graph-deployment
:deployment-data="reducedDeploymentData" :deployment-data="reducedDeploymentData"
...@@ -298,9 +300,9 @@ export default { ...@@ -298,9 +300,9 @@ export default {
:show-flag-content="showFlagContent" :show-flag-content="showFlagContent"
:time-series="timeSeries" :time-series="timeSeries"
:unit-of-display="unitOfDisplay" :unit-of-display="unitOfDisplay"
:current-data-index="currentDataIndex"
:legend-title="legendTitle" :legend-title="legendTitle"
:deployment-flag-data="deploymentFlagData" :deployment-flag-data="deploymentFlagData"
:current-coordinates="currentCoordinates"
/> />
</div> </div>
<graph-legend <graph-legend
......
...@@ -47,14 +47,14 @@ export default { ...@@ -47,14 +47,14 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
currentDataIndex: {
type: Number,
required: true,
},
legendTitle: { legendTitle: {
type: String, type: String,
required: true, required: true,
}, },
currentCoordinates: {
type: Array,
required: true,
},
}, },
computed: { computed: {
formatTime() { formatTime() {
...@@ -90,10 +90,12 @@ export default { ...@@ -90,10 +90,12 @@ export default {
}, },
}, },
methods: { methods: {
seriesMetricValue(series) { seriesMetricValue(seriesIndex, series) {
const indexFromCoordinates = this.currentCoordinates[seriesIndex]
? this.currentCoordinates[seriesIndex].currentDataIndex : 0;
const index = this.deploymentFlagData const index = this.deploymentFlagData
? this.deploymentFlagData.seriesIndex ? this.deploymentFlagData.seriesIndex
: this.currentDataIndex; : indexFromCoordinates;
const value = series.values[index] && series.values[index].value; const value = series.values[index] && series.values[index].value;
if (isNaN(value)) { if (isNaN(value)) {
return '-'; return '-';
...@@ -128,7 +130,7 @@ export default { ...@@ -128,7 +130,7 @@ export default {
<h5 v-if="deploymentFlagData"> <h5 v-if="deploymentFlagData">
Deployed Deployed
</h5> </h5>
{{ formatDate }} at {{ formatDate }}
<strong>{{ formatTime }}</strong> <strong>{{ formatTime }}</strong>
</div> </div>
<div <div
...@@ -163,9 +165,11 @@ export default { ...@@ -163,9 +165,11 @@ export default {
:key="index" :key="index"
> >
<track-line :track="series"/> <track-line :track="series"/>
<td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td>
<td> <td>
<strong>{{ seriesMetricValue(series) }}</strong> {{ series.track }} {{ seriesMetricLabel(index, series) }}
</td>
<td>
<strong>{{ seriesMetricValue(index, series) }}</strong>
</td> </td>
</tr> </tr>
</table> </table>
......
...@@ -22,6 +22,15 @@ export default { ...@@ -22,6 +22,15 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
currentCoordinates: {
type: Object,
required: false,
default: () => ({ currentX: 0, currentY: 0 }),
},
showDot: {
type: Boolean,
required: true,
},
}, },
computed: { computed: {
strokeDashArray() { strokeDashArray() {
...@@ -33,12 +42,20 @@ export default { ...@@ -33,12 +42,20 @@ export default {
}; };
</script> </script>
<template> <template>
<g> <g transform="translate(-5, 20)">
<circle
class="circle-path"
:cx="currentCoordinates.currentX"
:cy="currentCoordinates.currentY"
:fill="lineColor"
:stroke="lineColor"
r="3"
v-if="showDot"
/>
<path <path
class="metric-area" class="metric-area"
:d="generatedAreaPath" :d="generatedAreaPath"
:fill="areaColor" :fill="areaColor"
transform="translate(-5, 20)"
/> />
<path <path
class="metric-line" class="metric-line"
...@@ -47,7 +64,6 @@ export default { ...@@ -47,7 +64,6 @@ export default {
fill="none" fill="none"
stroke-width="1" stroke-width="1"
:stroke-dasharray="strokeDashArray" :stroke-dasharray="strokeDashArray"
transform="translate(-5, 20)"
/> />
</g> </g>
</template> </template>
...@@ -19,16 +19,16 @@ export default { ...@@ -19,16 +19,16 @@ export default {
<template> <template>
<td> <td>
<svg <svg
width="15" width="16"
height="6"> height="8">
<line <line
:stroke-dasharray="stylizedLine" :stroke-dasharray="stylizedLine"
:stroke="track.lineColor" :stroke="track.lineColor"
stroke-width="4" stroke-width="4"
:x1="0" :x1="0"
:x2="15" :x2="16"
:y1="2" :y1="4"
:y2="2" :y2="4"
/> />
</svg> </svg>
</td> </td>
......
...@@ -52,14 +52,22 @@ const mixins = { ...@@ -52,14 +52,22 @@ const mixins = {
positionFlag() { positionFlag() {
const timeSeries = this.timeSeries[0]; const timeSeries = this.timeSeries[0];
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1); const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1);
this.currentData = timeSeries.values[hoveredDataIndex]; this.currentData = timeSeries.values[hoveredDataIndex];
this.currentDataIndex = hoveredDataIndex;
this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time)); this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
if (this.currentXCoordinate > (this.graphWidth - 200)) {
this.currentFlagPosition = this.currentXCoordinate - 103; this.currentCoordinates = this.timeSeries.map((series) => {
} else { const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate, 1);
this.currentFlagPosition = this.currentXCoordinate; const currentData = series.values[currentDataIndex];
} const currentX = Math.floor(series.timeSeriesScaleX(currentData.time));
const currentY = Math.floor(series.timeSeriesScaleY(currentData.value));
return {
currentX,
currentY,
currentDataIndex,
};
});
if (this.hoverData.currentDeployXPos) { if (this.hoverData.currentDeployXPos) {
this.showFlag = false; this.showFlag = false;
......
...@@ -14,7 +14,7 @@ const d3 = { ...@@ -14,7 +14,7 @@ const d3 = {
timeYear, timeYear,
}; };
export const dateFormat = d3.time('%a, %b %-d'); export const dateFormat = d3.time('%d %b %Y, ');
export const timeFormat = d3.time('%-I:%M%p'); export const timeFormat = d3.time('%-I:%M%p');
export const dateFormatWithName = d3.time('%a, %b %-d'); export const dateFormatWithName = d3.time('%a, %b %-d');
export const bisectDate = d3.bisector(d => d.time).left; export const bisectDate = d3.bisector(d => d.time).left;
......
...@@ -123,6 +123,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom ...@@ -123,6 +123,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
linePath: lineFunction(timeSeries.values), linePath: lineFunction(timeSeries.values),
areaPath: areaFunction(timeSeries.values), areaPath: areaFunction(timeSeries.values),
timeSeriesScaleX, timeSeriesScaleX,
timeSeriesScaleY,
values: timeSeries.values, values: timeSeries.values,
max: maximumValue, max: maximumValue,
average: accum / timeSeries.values.length, average: accum / timeSeries.values.length,
......
import initSettingsPanels from '~/settings_panels';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
const variableListEl = document.querySelector('.js-ci-variable-list-section'); const variableListEl = document.querySelector('.js-ci-variable-list-section');
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new AjaxVariableList({ new AjaxVariableList({
......
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
gcpSignupOffer();
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
gcpSignupOffer();
import Diff from '~/diff'; import Diff from '~/diff';
import initChangesDropdown from '~/init_changes_dropdown'; import initChangesDropdown from '~/init_changes_dropdown';
import GpgBadges from '~/gpg_badges';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new Diff(); // eslint-disable-line no-new new Diff(); // eslint-disable-line no-new
const paddingTop = 16; const paddingTop = 16;
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop); initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
GpgBadges.fetch();
}); });
...@@ -61,7 +61,7 @@ export default { ...@@ -61,7 +61,7 @@ export default {
methods: { methods: {
onClickAction() { onClickAction() {
$(this.$el).tooltip('hide'); $(this.$el).tooltip('hide');
eventHub.$emit('graphAction', this.link); eventHub.$emit('postAction', this.link);
this.linkRequested = this.link; this.linkRequested = this.link;
this.isDisabled = true; this.isDisabled = true;
}, },
......
...@@ -87,7 +87,8 @@ export default { ...@@ -87,7 +87,8 @@ export default {
data-toggle="dropdown" data-toggle="dropdown"
data-container="body" data-container="body"
class="dropdown-menu-toggle build-content" class="dropdown-menu-toggle build-content"
:title="tooltipText"> :title="tooltipText"
>
<job-name-component <job-name-component
:name="job.name" :name="job.name"
...@@ -104,7 +105,8 @@ export default { ...@@ -104,7 +105,8 @@ export default {
<ul> <ul>
<li <li
v-for="(item, i) in job.jobs" v-for="(item, i) in job.jobs"
:key="i"> :key="i"
>
<job-component <job-component
:job="item" :job="item"
css-class-job-name="mini-pipeline-graph-dropdown-item" css-class-job-name="mini-pipeline-graph-dropdown-item"
......
...@@ -108,7 +108,7 @@ export default { ...@@ -108,7 +108,7 @@ export default {
<div <div
v-else v-else
v-tooltip v-tooltip
class="js-job-component-tooltip" class="js-job-component-tooltip non-details-job-component"
:title="tooltipText" :title="tooltipText"
:class="cssClassJobName" :class="cssClassJobName"
data-html="true" data-html="true"
......
<script> <script>
/**
/**
* Renders each stage of the pipeline mini graph. * Renders each stage of the pipeline mini graph.
* *
* Given the provided endpoint will make a request to * Given the provided endpoint will make a request to
...@@ -13,18 +12,21 @@ ...@@ -13,18 +12,21 @@
* 4. Commit widget * 4. Commit widget
*/ */
import $ from 'jquery'; import $ from 'jquery';
import Flash from '../../flash'; import { __ } from '../../locale';
import axios from '../../lib/utils/axios_utils'; import Flash from '../../flash';
import eventHub from '../event_hub'; import axios from '../../lib/utils/axios_utils';
import Icon from '../../vue_shared/components/icon.vue'; import eventHub from '../event_hub';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import Icon from '../../vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import JobComponent from './graph/job_component.vue';
export default { import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: { components: {
LoadingIcon, LoadingIcon,
Icon, Icon,
JobComponent,
}, },
directives: { directives: {
...@@ -53,7 +55,9 @@ ...@@ -53,7 +55,9 @@
computed: { computed: {
dropdownClass() { dropdownClass() {
return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading'; return this.dropdownContent.length > 0
? 'js-builds-dropdown-container'
: 'js-builds-dropdown-loading';
}, },
triggerButtonClass() { triggerButtonClass() {
...@@ -67,9 +71,7 @@ ...@@ -67,9 +71,7 @@
watch: { watch: {
updateDropdown() { updateDropdown() {
if (this.updateDropdown && if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) {
this.isDropdownOpen() &&
!this.isLoading) {
this.fetchJobs(); this.fetchJobs();
} }
}, },
...@@ -91,16 +93,17 @@ ...@@ -91,16 +93,17 @@
}, },
fetchJobs() { fetchJobs() {
axios.get(this.stage.dropdown_path) axios
.get(this.stage.dropdown_path)
.then(({ data }) => { .then(({ data }) => {
this.dropdownContent = data.html; this.dropdownContent = data.latest_statuses;
this.isLoading = false; this.isLoading = false;
}) })
.catch(() => { .catch(() => {
this.closeDropdown(); this.closeDropdown();
this.isLoading = false; this.isLoading = false;
Flash('Something went wrong on our end.'); Flash(__('Something went wrong on our end.'));
}); });
}, },
...@@ -113,8 +116,10 @@ ...@@ -113,8 +116,10 @@
* target the click event of this component. * target the click event of this component.
*/ */
stopDropdownClickPropagation() { stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')) $(
.on('click', (e) => { '.js-builds-dropdown-list button, .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item',
this.$el,
).on('click', e => {
e.stopPropagation(); e.stopPropagation();
}); });
}, },
...@@ -129,7 +134,7 @@ ...@@ -129,7 +134,7 @@
return this.$el.classList.contains('open'); return this.$el.classList.contains('open');
}, },
}, },
}; };
</script> </script>
<template> <template>
...@@ -168,7 +173,6 @@ ...@@ -168,7 +173,6 @@
> >
<li <li
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu" class="js-builds-dropdown-list scrollable-menu"
> >
...@@ -176,8 +180,16 @@ ...@@ -176,8 +180,16 @@
<ul <ul
v-else v-else
v-html="dropdownContent"
> >
<li
v-for="job in dropdownContent"
:key="job.id"
>
<job-component
:job="job"
css-class-job-name="mini-pipeline-graph-dropdown-item"
/>
</li>
</ul> </ul>
</li> </li>
</ul> </ul>
......
...@@ -33,10 +33,10 @@ export default () => { ...@@ -33,10 +33,10 @@ export default () => {
}; };
}, },
created() { created() {
eventHub.$on('graphAction', this.postAction); eventHub.$on('postAction', this.postAction);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('graphAction', this.postAction); eventHub.$off('postAction', this.postAction);
}, },
methods: { methods: {
postAction(action) { postAction(action) {
......
...@@ -100,9 +100,10 @@ export default { ...@@ -100,9 +100,10 @@ export default {
fetchSearchedProjects(searchQuery) { fetchSearchedProjects(searchQuery) {
this.searchQuery = searchQuery; this.searchQuery = searchQuery;
this.toggleLoader(true); this.toggleLoader(true);
this.service.getSearchedProjects(this.searchQuery) this.service
.getSearchedProjects(this.searchQuery)
.then(res => res.json()) .then(res => res.json())
.then((results) => { .then(results => {
this.toggleSearchProjectsList(true); this.toggleSearchProjectsList(true);
this.store.setSearchedProjects(results); this.store.setSearchedProjects(results);
}) })
......
...@@ -50,7 +50,7 @@ export default class ProjectsService { ...@@ -50,7 +50,7 @@ export default class ProjectsService {
} else { } else {
// Check if project is already present in frequents list // Check if project is already present in frequents list
// When found, update metadata of it. // When found, update metadata of it.
storedFrequentProjects = JSON.parse(storedRawProjects).map((projectItem) => { storedFrequentProjects = JSON.parse(storedRawProjects).map(projectItem => {
if (projectItem.id === project.id) { if (projectItem.id === project.id) {
matchFound = true; matchFound = true;
const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS; const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS;
...@@ -104,13 +104,17 @@ export default class ProjectsService { ...@@ -104,13 +104,17 @@ export default class ProjectsService {
return []; return [];
} }
if (bp.getBreakpointSize() === 'sm' || if (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'xs') {
bp.getBreakpointSize() === 'xs') {
frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE; frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE;
} }
const frequentProjects = storedFrequentProjects const frequentProjects = storedFrequentProjects.filter(
.filter(project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY); project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY,
);
if (!frequentProjects || frequentProjects.length === 0) {
return [];
}
// Sort all frequent projects in decending order of frequency // Sort all frequent projects in decending order of frequency
// and then by lastAccessedOn with recent most first // and then by lastAccessedOn with recent most first
......
...@@ -85,6 +85,7 @@ export default class Shortcuts { ...@@ -85,6 +85,7 @@ export default class Shortcuts {
if ($modal.length) { if ($modal.length) {
$modal.modal('toggle'); $modal.modal('toggle');
return null;
} }
return axios.get(gon.shortcuts_path, { return axios.get(gon.shortcuts_path, {
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue'; import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
...@@ -16,6 +17,7 @@ ...@@ -16,6 +17,7 @@
mrWidgetAuthorTime, mrWidgetAuthorTime,
loadingIcon, loadingIcon,
statusIcon, statusIcon,
ClipboardButton,
}, },
props: { props: {
mr: { mr: {
...@@ -162,6 +164,18 @@ ...@@ -162,6 +164,18 @@
<span class="label-branch"> <span class="label-branch">
<a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a> <a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a>
</span> </span>
with
<a
:href="mr.mergeCommitPath"
class="commit-sha js-mr-merged-commit-sha"
>
{{ mr.shortMergeCommitSha }}
</a>
<clipboard-button
:title="__('Copy commit SHA to clipboard')"
:text="mr.shortMergeCommitSha"
css-class="btn-default btn-transparent btn-clipboard js-mr-merged-copy-sha"
/>
</p> </p>
<p v-if="mr.sourceBranchRemoved"> <p v-if="mr.sourceBranchRemoved">
{{ s__("mrWidget|The source branch has been removed") }} {{ s__("mrWidget|The source branch has been removed") }}
......
...@@ -23,6 +23,7 @@ export default class MergeRequestStore { ...@@ -23,6 +23,7 @@ export default class MergeRequestStore {
this.sourceBranch = data.source_branch; this.sourceBranch = data.source_branch;
this.mergeStatus = data.merge_status; this.mergeStatus = data.merge_status;
this.commitMessage = data.merge_commit_message; this.commitMessage = data.merge_commit_message;
this.shortMergeCommitSha = data.short_merge_commit_sha;
this.commitMessageWithDescription = data.merge_commit_message_with_description; this.commitMessageWithDescription = data.merge_commit_message_with_description;
this.commitsCount = data.commits_count; this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count; this.divergedCommitsCount = data.diverged_commits_count;
...@@ -68,6 +69,7 @@ export default class MergeRequestStore { ...@@ -68,6 +69,7 @@ export default class MergeRequestStore {
this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path; this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
this.mergeCheckPath = data.merge_check_path; this.mergeCheckPath = data.merge_check_path;
this.mergeActionsContentPath = data.commit_change_content_path; this.mergeActionsContentPath = data.commit_change_content_path;
this.mergeCommitPath = data.merge_commit_path;
this.isRemovingSourceBranch = this.isRemovingSourceBranch || false; this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
this.isOpen = data.state === 'opened'; this.isOpen = data.state === 'opened';
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false; this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
......
...@@ -65,6 +65,9 @@ export default { ...@@ -65,6 +65,9 @@ export default {
spriteHref() { spriteHref() {
return `${gon.sprite_icons}#${this.name}`; return `${gon.sprite_icons}#${this.name}`;
}, },
iconTestClass() {
return `ic-${this.name}`;
},
iconSizeClass() { iconSizeClass() {
return this.size ? `s${this.size}` : ''; return this.size ? `s${this.size}` : '';
}, },
...@@ -74,7 +77,7 @@ export default { ...@@ -74,7 +77,7 @@ export default {
<template> <template>
<svg <svg
:class="[iconSizeClass, cssClasses]" :class="[iconSizeClass, iconTestClass, cssClasses]"
:width="width" :width="width"
:height="height" :height="height"
:x="x" :x="x"
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
/** /**
* Given an array of tabs, renders non linked bootstrap tabs. * Given an array of tabs, renders non linked bootstrap tabs.
* When a tab is clicked it will trigger an event and provide the clicked scope. * When a tab is clicked it will trigger an event and provide the clicked scope.
* *
...@@ -14,14 +14,14 @@ ...@@ -14,14 +14,14 @@
* { * {
* name: String, * name: String,
* scope: String, * scope: String,
* count: Number || Undefined, * count: Number || Undefined || Null,
* isActive: Boolean, * isActive: Boolean,
* }, * },
* ]" * ]"
* @onChangeTab="onChangeTab" * @onChangeTab="onChangeTab"
* /> * />
*/ */
export default { export default {
name: 'NavigationTabs', name: 'NavigationTabs',
props: { props: {
tabs: { tabs: {
...@@ -39,15 +39,15 @@ ...@@ -39,15 +39,15 @@
}, },
methods: { methods: {
shouldRenderBadge(count) { shouldRenderBadge(count) {
// 0 is valid in a badge, but evaluates to false, we need to check for undefined // 0 is valid in a badge, but evaluates to false, we need to check for undefined or null
return count !== undefined; return !(count === undefined || count === null);
}, },
onTabClick(tab) { onTabClick(tab) {
this.$emit('onChangeTab', tab.scope); this.$emit('onChangeTab', tab.scope);
}, },
}, },
}; };
</script> </script>
<template> <template>
<ul class="nav-links scrolling-tabs separator"> <ul class="nav-links scrolling-tabs separator">
......
...@@ -177,25 +177,6 @@ ...@@ -177,25 +177,6 @@
} }
} }
// Web IDE
.ide-sidebar-link {
color: $color-200;
background-color: $color-700;
&:hover,
&:focus {
background-color: $color-500;
}
&:active {
background: $color-800;
}
}
.branch-container {
border-left-color: $color-700;
}
.branch-header-title { .branch-header-title {
color: $color-700; color: $color-700;
} }
...@@ -203,6 +184,13 @@ ...@@ -203,6 +184,13 @@
.ide-file-list .file.file-active { .ide-file-list .file.file-active {
color: $color-700; color: $color-700;
} }
.ide-sidebar-link {
&.active {
color: $color-700;
box-shadow: inset 3px 0 $color-700;
}
}
} }
body { body {
...@@ -343,9 +331,5 @@ body { ...@@ -343,9 +331,5 @@ body {
.sidebar-top-level-items > li.active .badge { .sidebar-top-level-items > li.active .badge {
color: $theme-gray-900; color: $theme-gray-900;
} }
.ide-sidebar-link {
color: $white-light;
}
} }
} }
...@@ -231,6 +231,7 @@ $row-hover: $blue-50; ...@@ -231,6 +231,7 @@ $row-hover: $blue-50;
$row-hover-border: $blue-200; $row-hover-border: $blue-200;
$progress-color: #c0392b; $progress-color: #c0392b;
$header-height: 40px; $header-height: 40px;
$ide-statusbar-height: 27px;
$fixed-layout-width: 1280px; $fixed-layout-width: 1280px;
$limited-layout-width: 990px; $limited-layout-width: 990px;
$limited-layout-width-sm: 790px; $limited-layout-width-sm: 790px;
...@@ -268,6 +269,7 @@ $system-footer-height: $system-header-height; ...@@ -268,6 +269,7 @@ $system-footer-height: $system-header-height;
$flash-height: 52px; $flash-height: 52px;
$context-header-height: 60px; $context-header-height: 60px;
$breadcrumb-min-height: 48px; $breadcrumb-min-height: 48px;
$gcp-signup-offer-icon-max-width: 125px;
$issue-box-upcoming-bg: #8f8f8f; $issue-box-upcoming-bg: #8f8f8f;
$pages-group-name-color: #4c4e54; $pages-group-name-color: #4c4e54;
...@@ -342,11 +344,10 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%); ...@@ -342,11 +344,10 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
/* /*
* Fonts * Fonts
*/ */
$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', $monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; 'Courier New', 'andale mono', 'lucida console', monospace;
$regular_font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, $regular_font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif, 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
/* /*
* Dropdowns * Dropdowns
...@@ -474,11 +475,9 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5); ...@@ -474,11 +475,9 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
*/ */
$issue-boards-filter-height: 68px; $issue-boards-filter-height: 68px;
$issue-boards-breadcrumbs-height-xs: 63px; $issue-boards-breadcrumbs-height-xs: 63px;
$issue-board-list-difference-xs: $header-height + $issue-board-list-difference-xs: $header-height + $issue-boards-breadcrumbs-height-xs;
$issue-boards-breadcrumbs-height-xs;
$issue-board-list-difference-sm: $header-height + $breadcrumb-min-height; $issue-board-list-difference-sm: $header-height + $breadcrumb-min-height;
$issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-boards-filter-height;
$issue-boards-filter-height;
/* /*
* Avatar * Avatar
...@@ -699,6 +698,8 @@ $stage-hover-bg: $gray-darker; ...@@ -699,6 +698,8 @@ $stage-hover-bg: $gray-darker;
$ci-action-icon-size: 22px; $ci-action-icon-size: 22px;
$pipeline-dropdown-line-height: 20px; $pipeline-dropdown-line-height: 20px;
$pipeline-dropdown-status-icon-size: 18px; $pipeline-dropdown-status-icon-size: 18px;
$ci-action-dropdown-button-size: 24px;
$ci-action-dropdown-svg-size: 12px;
/* /*
CI variable lists CI variable lists
......
...@@ -26,3 +26,51 @@ ...@@ -26,3 +26,51 @@
margin-right: 0; margin-right: 0;
} }
} }
.gcp-signup-offer {
background-color: $blue-50;
border: 1px solid $blue-300;
border-radius: $border-radius-default;
// TODO: To be superceded by cssLab
&.alert {
padding: 24px 16px;
&-dismissable {
padding-right: 32px;
.close {
top: -8px;
right: -16px;
color: $blue-500;
opacity: 1;
}
}
}
.gcp-logo {
margin-bottom: $gl-padding;
text-align: center;
}
img {
max-width: $gcp-signup-offer-icon-max-width;
}
a:not(.btn) {
color: $gl-link-color;
font-weight: normal;
text-decoration: none;
}
@media (min-width: $screen-sm-min) {
> div {
display: flex;
align-items: center;
}
.gcp-logo {
margin: 0;
}
}
}
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
background: none; background: none;
border: 0; border: 0;
padding: 0; padding: 0;
margin-top: 10px;
word-break: normal; word-break: normal;
white-space: pre-wrap; white-space: pre-wrap;
} }
...@@ -21,10 +20,6 @@ ...@@ -21,10 +20,6 @@
margin: 0; margin: 0;
color: $gl-text-color; color: $gl-text-color;
} }
.commit-description {
margin-top: 15px;
}
} }
.commit-hash-full { .commit-hash-full {
...@@ -178,7 +173,7 @@ ...@@ -178,7 +173,7 @@
.commit-detail { .commit-detail {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: start;
flex-grow: 1; flex-grow: 1;
.project_namespace { .project_namespace {
...@@ -272,20 +267,16 @@ ...@@ -272,20 +267,16 @@
.commit-row-description { .commit-row-description {
font-size: 14px; font-size: 14px;
padding: 10px 15px; padding: 0 0 0 $gl-padding-8;
margin: 10px 0; border: 0;
background: $gray-light;
display: none; display: none;
white-space: pre-wrap; white-space: pre-wrap;
word-break: normal; word-break: normal;
color: $gl-text-color-secondary;
pre { background: none;
border: 0; font-family: inherit;
background: inherit; border-left: 2px solid $theme-gray-300;
padding: 0; border-radius: unset;
margin: 0;
white-space: pre-wrap;
}
a { a {
color: $gl-text-color; color: $gl-text-color;
......
...@@ -438,28 +438,59 @@ ...@@ -438,28 +438,59 @@
} }
&.popover { &.popover {
padding: 0;
border: 1px solid $border-color;
&.left { &.left {
left: auto; left: auto;
right: 0; right: 0;
margin-right: 10px; margin-right: 10px;
> .arrow {
right: -16px;
border-left-color: $border-color;
}
> .arrow::after {
border-left-color: $theme-gray-50;
}
} }
&.right { &.right {
left: 0; left: 0;
right: auto; right: auto;
margin-left: 10px; margin-left: 10px;
> .arrow {
left: -16px;
border-right-color: $border-color;
}
> .arrow::after {
border-right-color: $theme-gray-50;
}
} }
> .arrow { > .arrow {
top: 40px; top: 16px;
margin-top: -8px;
border-width: 8px;
} }
> .popover-title, > .popover-title,
> .popover-content { > .popover-content {
padding: 5px 8px; padding: 8px;
font-size: 12px; font-size: 12px;
white-space: nowrap; white-space: nowrap;
} }
> .popover-title {
background-color: $theme-gray-50;
}
}
strong {
font-weight: 600;
} }
} }
...@@ -472,7 +503,7 @@ ...@@ -472,7 +503,7 @@
vertical-align: middle; vertical-align: middle;
+ td { + td {
padding-left: 5px; padding-left: 8px;
vertical-align: top; vertical-align: top;
} }
} }
......
...@@ -159,10 +159,6 @@ ...@@ -159,10 +159,6 @@
.dropdown-menu { .dropdown-menu {
z-index: 300; z-index: 300;
} }
.ci-action-icon-wrapper {
line-height: 16px;
}
} }
.mini-pipeline-graph-dropdown-toggle { .mini-pipeline-graph-dropdown-toggle {
......
...@@ -49,7 +49,6 @@ ...@@ -49,7 +49,6 @@
} }
.ci-table { .ci-table {
.label { .label {
margin-bottom: 3px; margin-bottom: 3px;
} }
...@@ -150,7 +149,6 @@ ...@@ -150,7 +149,6 @@
} }
.branch-commit { .branch-commit {
.ref-name { .ref-name {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
max-width: 100px; max-width: 100px;
...@@ -510,43 +508,6 @@ ...@@ -510,43 +508,6 @@
@extend .build-content:hover; @extend .build-content:hover;
} }
.ci-action-icon-container {
position: absolute;
right: 5px;
top: 5px;
// Action Icons in big pipeline-graph nodes
&.ci-action-icon-wrapper {
height: 30px;
width: 30px;
background: $white-light;
border: 1px solid $border-color;
border-radius: 100%;
display: block;
&:hover {
background-color: $stage-hover-bg;
border: 1px solid $dropdown-toggle-active-border-color;
svg {
fill: $gl-text-color;
}
}
svg {
fill: $gl-text-color-secondary;
position: relative;
top: -1px;
}
&.play {
svg {
left: 2px;
}
}
}
}
.ci-status-icon svg { .ci-status-icon svg {
height: 20px; height: 20px;
width: 20px; width: 20px;
...@@ -631,6 +592,43 @@ ...@@ -631,6 +592,43 @@
} }
} }
} }
.ci-action-icon-container {
position: absolute;
right: 5px;
top: 5px;
// Action Icons in big pipeline-graph nodes
&.ci-action-icon-wrapper {
height: 30px;
width: 30px;
background: $white-light;
border: 1px solid $border-color;
border-radius: 100%;
display: block;
&:hover {
background-color: $stage-hover-bg;
border: 1px solid $dropdown-toggle-active-border-color;
svg {
fill: $gl-text-color;
}
}
svg {
fill: $gl-text-color-secondary;
position: relative;
top: -1px;
}
&.play {
svg {
left: 2px;
}
}
}
}
} }
// Triggers the dropdown in the big pipeline graph // Triggers the dropdown in the big pipeline graph
...@@ -740,93 +738,77 @@ a.linked-pipeline-mini-item { ...@@ -740,93 +738,77 @@ a.linked-pipeline-mini-item {
} }
} }
// dropdown content for big and mini pipeline /**
Action icons inside dropdowns:
- mini graph in pipelines table
- dropdown in big graph
- mini graph in MR widget pipeline
- mini graph in Commit widget pipeline
*/
.big-pipeline-graph-dropdown-menu, .big-pipeline-graph-dropdown-menu,
.mini-pipeline-graph-dropdown-menu { .mini-pipeline-graph-dropdown-menu {
width: 240px; width: 240px;
max-width: 240px; max-width: 240px;
.scrollable-menu { // override dropdown.scss
&.dropdown-menu li button,
&.dropdown-menu li a.ci-action-icon-container {
padding: 0; padding: 0;
max-height: 245px; text-align: center;
overflow: auto;
} }
li { .ci-action-icon-container {
position: relative; position: absolute;
right: 8px;
top: 8px;
// ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered &.ci-action-icon-wrapper {
&:hover > .mini-pipeline-graph-dropdown-item, height: $ci-action-dropdown-button-size;
&:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item { width: $ci-action-dropdown-button-size;
@extend .mini-pipeline-graph-dropdown-item:hover;
}
// Action icon on the right background: $white-light;
a.ci-action-icon-wrapper {
border-radius: 50%;
border: 1px solid $border-color; border: 1px solid $border-color;
width: $ci-action-icon-size; border-radius: 50%;
height: $ci-action-icon-size; display: block;
padding: 2px 0 0 5px;
font-size: 12px;
background-color: $white-light;
position: absolute;
top: 50%;
right: $gl-padding;
margin-top: -#{$ci-action-icon-size / 2};
&:hover, &:hover {
&:focus {
background-color: $stage-hover-bg; background-color: $stage-hover-bg;
border: 1px solid $dropdown-toggle-active-border-color; border: 1px solid $dropdown-toggle-active-border-color;
}
svg { svg {
fill: $gl-text-color-secondary; fill: $gl-text-color;
width: #{$ci-action-icon-size - 6};
height: #{$ci-action-icon-size - 6};
left: -3px;
position: relative;
top: -1px;
&.icon-action-stop,
&.icon-action-cancel {
width: 12px;
height: 12px;
top: 1px;
left: -1px;
} }
&.icon-action-play {
width: 11px;
height: 11px;
top: 1px;
left: 1px;
} }
&.icon-action-retry { svg {
width: 16px; width: $ci-action-dropdown-svg-size;
height: 16px; height: $ci-action-dropdown-svg-size;
fill: $gl-text-color-secondary;
position: relative;
top: 0; top: 0;
left: -3px; vertical-align: initial;
} }
} }
&:hover svg,
&:focus svg {
fill: $gl-text-color;
} }
&.icon-action-retry, // SVGs in the commit widget and mr widget
&.icon-action-play { a.ci-action-icon-container.ci-action-icon-wrapper svg {
svg { top: 2px;
width: #{$ci-action-icon-size - 6};
height: #{$ci-action-icon-size - 6};
left: 8px;
} }
.scrollable-menu {
padding: 0;
max-height: 245px;
overflow: auto;
} }
li {
position: relative;
// ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered
&:hover > .mini-pipeline-graph-dropdown-item,
&:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item {
@extend .mini-pipeline-graph-dropdown-item:hover;
} }
// link to the build // link to the build
...@@ -838,6 +820,11 @@ a.linked-pipeline-mini-item { ...@@ -838,6 +820,11 @@ a.linked-pipeline-mini-item {
line-height: $line-height-base; line-height: $line-height-base;
white-space: nowrap; white-space: nowrap;
// Match dropdown.scss for all `a` tags
&.non-details-job-component {
padding: 8px 16px;
}
.ci-job-name-component { .ci-job-name-component {
align-items: center; align-items: center;
display: flex; display: flex;
...@@ -969,7 +956,7 @@ a.linked-pipeline-mini-item { ...@@ -969,7 +956,7 @@ a.linked-pipeline-mini-item {
&.dropdown-menu { &.dropdown-menu {
transform: translate(-80%, 0); transform: translate(-80%, 0);
@media(min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
transform: translate(-50%, 0); transform: translate(-50%, 0);
right: auto; right: auto;
left: 50%; left: 50%;
......
...@@ -354,31 +354,49 @@ ...@@ -354,31 +354,49 @@
min-width: 200px; min-width: 200px;
} }
.deploy-key-content { .deploy-keys {
@media (min-width: $screen-sm-min) { .scrolling-tabs-container {
float: left; position: relative;
}
}
&:last-child { .deploy-key {
float: right; // Ensure that the fingerprint does not overflow on small screens
.fingerprint {
word-break: break-all;
white-space: normal;
}
.deploy-project-label,
.key-created-at {
svg {
vertical-align: text-top;
} }
} }
}
.deploy-key-projects { .btn svg {
@media (min-width: $screen-sm-min) { vertical-align: top;
line-height: 42px; }
.key-created-at {
line-height: unset;
} }
} }
a.deploy-project-label { .deploy-project-list {
padding: 5px; margin-bottom: -$gl-padding-4;
margin-right: 5px;
color: $gl-text-color; a.deploy-project-label {
background-color: $row-hover; margin-right: $gl-padding-4;
margin-bottom: $gl-padding-4;
color: $gl-text-color-secondary;
background-color: $theme-gray-100;
line-height: $gl-btn-line-height;
&:hover { &:hover {
color: $gl-link-color; color: $gl-link-color;
} }
}
} }
.vs-public { .vs-public {
......
This diff is collapsed.
class Groups::RunnersController < Groups::ApplicationController
# Proper policies should be implemented per
# https://gitlab.com/gitlab-org/gitlab-ce/issues/45894
before_action :authorize_admin_pipeline!
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
def show
render 'shared/runners/show'
end
def edit
end
def update
if Ci::UpdateRunnerService.new(@runner).update(runner_params)
redirect_to group_runner_path(@group, @runner), notice: 'Runner was successfully updated.'
else
render 'edit'
end
end
def destroy
@runner.destroy
redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), status: 302
end
def resume
if Ci::UpdateRunnerService.new(@runner).update(active: true)
redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), notice: 'Runner was successfully updated.'
else
redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), alert: 'Runner was not updated.'
end
end
def pause
if Ci::UpdateRunnerService.new(@runner).update(active: false)
redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), notice: 'Runner was successfully updated.'
else
redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), alert: 'Runner was not updated.'
end
end
private
def runner
@runner ||= @group.runners.find(params[:id])
end
def authorize_admin_pipeline!
return render_404 unless can?(current_user, :admin_pipeline, group)
end
def runner_params
params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
end
end
...@@ -8,8 +8,11 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -8,8 +8,11 @@ class Projects::CompareController < Projects::ApplicationController
# Authorize # Authorize
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :authorize_download_code! before_action :authorize_download_code!
before_action :define_ref_vars, only: [:index, :show, :diff_for_path] # Defining ivars
before_action :define_diff_vars, only: [:show, :diff_for_path] before_action :define_diffs, only: [:show, :diff_for_path]
before_action :define_environment, only: [:show]
before_action :define_diff_notes_disabled, only: [:show, :diff_for_path]
before_action :define_commits, only: [:show, :diff_for_path, :signatures]
before_action :merge_request, only: [:index, :show] before_action :merge_request, only: [:index, :show]
def index def index
...@@ -22,9 +25,9 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -22,9 +25,9 @@ class Projects::CompareController < Projects::ApplicationController
end end
def diff_for_path def diff_for_path
return render_404 unless @compare return render_404 unless compare
render_diff_for_path(@compare.diffs(diff_options)) render_diff_for_path(compare.diffs(diff_options))
end end
def create def create
...@@ -41,30 +44,60 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -41,30 +44,60 @@ class Projects::CompareController < Projects::ApplicationController
end end
end end
def signatures
respond_to do |format|
format.json do
render json: {
signatures: @commits.select(&:has_signature?).map do |commit|
{
commit_sha: commit.sha,
html: view_to_html_string('projects/commit/_signature', signature: commit.signature)
}
end
}
end
end
end
private private
def define_ref_vars def compare
@start_ref = Addressable::URI.unescape(params[:from]) return @compare if defined?(@compare)
@compare = CompareService.new(@project, head_ref).execute(@project, start_ref)
end
def start_ref
@start_ref ||= Addressable::URI.unescape(params[:from])
end
def head_ref
return @ref if defined?(@ref)
@ref = @head_ref = Addressable::URI.unescape(params[:to]) @ref = @head_ref = Addressable::URI.unescape(params[:to])
end end
def define_diff_vars def define_commits
@compare = CompareService.new(@project, @head_ref) @commits = compare.present? ? prepare_commits_for_rendering(compare.commits) : []
.execute(@project, @start_ref) end
if @compare def define_diffs
@commits = prepare_commits_for_rendering(@compare.commits) @diffs = compare.present? ? compare.diffs(diff_options) : []
@diffs = @compare.diffs(diff_options) end
environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @compare.commit } def define_environment
if compare
environment_params = @repository.branch_exists?(head_ref) ? { ref: head_ref } : { commit: compare.commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@diff_notes_disabled = true
end end
end end
def define_diff_notes_disabled
@diff_notes_disabled = compare.present?
end
def merge_request def merge_request
@merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
.find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) .find_by(source_project: @project, source_branch: head_ref, target_branch: start_ref)
end end
end end
...@@ -106,9 +106,18 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -106,9 +106,18 @@ class Projects::PipelinesController < Projects::ApplicationController
@stage = pipeline.legacy_stage(params[:stage]) @stage = pipeline.legacy_stage(params[:stage])
return not_found unless @stage return not_found unless @stage
respond_to do |format| render json: StageSerializer
format.json { render json: { html: view_to_html_string('projects/pipelines/_stage') } } .new(project: @project, current_user: @current_user)
.represent(@stage, details: true)
end end
# TODO: This endpoint is used by mini-pipeline-graph
# TODO: This endpoint should be migrated to `stage.json`
def stage_ajax
@stage = pipeline.legacy_stage(params[:stage])
return not_found unless @stage
render json: { html: view_to_html_string('projects/pipelines/_stage') }
end end
def retry def retry
......
...@@ -8,7 +8,7 @@ class Projects::RunnerProjectsController < Projects::ApplicationController ...@@ -8,7 +8,7 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
return head(403) unless can?(current_user, :assign_runner, @runner) return head(403) unless can?(current_user, :assign_runner, @runner)
path = runners_path(project) path = project_runners_path(project)
runner_project = @runner.assign_to(project, current_user) runner_project = @runner.assign_to(project, current_user)
if runner_project.persisted? if runner_project.persisted?
...@@ -22,6 +22,6 @@ class Projects::RunnerProjectsController < Projects::ApplicationController ...@@ -22,6 +22,6 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
runner_project = project.runner_projects.find(params[:id]) runner_project = project.runner_projects.find(params[:id])
runner_project.destroy runner_project.destroy
redirect_to runners_path(project), status: 302 redirect_to project_runners_path(project), status: 302
end end
end end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -1034,6 +1034,10 @@ class MergeRequest < ActiveRecord::Base ...@@ -1034,6 +1034,10 @@ class MergeRequest < ActiveRecord::Base
@merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
end end
def short_merge_commit_sha
Commit.truncate_sha(merge_commit_sha) if merge_commit_sha
end
def can_be_reverted?(current_user) def can_be_reverted?(current_user)
return false unless merge_commit return false unless merge_commit
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment