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`
- [ ] 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.
[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
- [ ] 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)
- [ ] 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)
......
......@@ -431,7 +431,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.97.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.99.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
......
......@@ -315,7 +315,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.97.0)
gitaly-proto (0.99.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
......@@ -1094,7 +1094,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.97.0)
gitaly-proto (~> 0.99.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
......
import Flash from '../flash';
import { s__ } from '../locale';
import setupToggleButtons from '../toggle_buttons';
import createFlash from '~/flash';
import { __ } from '~/locale';
import setupToggleButtons from '~/toggle_buttons';
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
import ClustersService from './services/clusters_service';
export default () => {
const clusterList = document.querySelector('.js-clusters-list');
gcpSignupOffer();
// The empty state won't have a clusterList
if (clusterList) {
setupToggleButtons(
document.querySelector('.js-clusters-list'),
(value, toggle) =>
ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } })
.catch((err) => {
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
throw err;
}),
setupToggleButtons(document.querySelector('.js-clusters-list'), (value, toggle) =>
ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }).catch(
err => {
createFlash(__('Something went wrong on our end.'));
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>
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 {
components: {
loadingIcon,
export default {
components: {
loadingIcon,
},
props: {
deployKey: {
type: Object,
required: true,
},
props: {
deployKey: {
type: Object,
required: true,
},
type: {
type: String,
required: true,
},
btnCssClass: {
type: String,
required: false,
default: 'btn-default',
},
type: {
type: String,
required: true,
},
data() {
return {
isLoading: false,
};
btnCssClass: {
type: String,
required: false,
default: 'btn-default',
},
computed: {
text() {
return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`;
},
},
methods: {
doAction() {
this.isLoading = true;
},
data() {
return {
isLoading: false,
};
},
methods: {
doAction() {
this.isLoading = true;
eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
this.isLoading = false;
});
},
eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
this.isLoading = false;
});
},
};
},
};
</script>
<template>
<button
class="btn btn-sm prepend-left-10"
class="btn"
:class="[{ disabled: isLoading }, btnCssClass]"
:disabled="isLoading"
@click="doAction">
{{ text }}
<slot></slot>
<loading-icon
v-if="isLoading"
:inline="true"
......
<script>
import Flash from '../../flash';
import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import keysPanel from './keys_panel.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import { s__ } from '~/locale';
import Flash from '~/flash';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import KeysPanel from './keys_panel.vue';
export default {
components: {
keysPanel,
loadingIcon,
export default {
components: {
KeysPanel,
LoadingIcon,
NavigationTabs,
},
props: {
endpoint: {
type: String,
required: true,
},
props: {
endpoint: {
type: String,
required: true,
},
projectId: {
type: String,
required: true,
},
data() {
return {
isLoading: false,
store: new DeployKeysStore(),
};
},
data() {
return {
currentTab: 'enabled_keys',
isLoading: false,
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: {
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() {
return Object.keys(this.keys).length;
},
computed: {
hasKeys() {
return Object.keys(this.keys).length;
},
keys() {
return this.store.keys;
},
keys() {
return this.store.keys;
},
created() {
this.service = new DeployKeysService(this.endpoint);
},
created() {
this.service = new DeployKeysService(this.endpoint);
eventHub.$on('enable.key', this.enableKey);
eventHub.$on('remove.key', this.disableKey);
eventHub.$on('disable.key', this.disableKey);
eventHub.$on('enable.key', this.enableKey);
eventHub.$on('remove.key', this.disableKey);
eventHub.$on('disable.key', this.disableKey);
},
mounted() {
this.fetchKeys();
},
beforeDestroy() {
eventHub.$off('enable.key', this.enableKey);
eventHub.$off('remove.key', this.disableKey);
eventHub.$off('disable.key', this.disableKey);
},
methods: {
onChangeTab(tab) {
this.currentTab = tab;
},
mounted() {
this.fetchKeys();
fetchKeys() {
this.isLoading = true;
return this.service
.getKeys()
.then(data => {
this.isLoading = false;
this.store.keys = data;
})
.catch(() => {
this.isLoading = false;
this.store.keys = {};
return new Flash(s__('DeployKeys|Error getting deploy keys'));
});
},
beforeDestroy() {
eventHub.$off('enable.key', this.enableKey);
eventHub.$off('remove.key', this.disableKey);
eventHub.$off('disable.key', this.disableKey);
enableKey(deployKey) {
this.service
.enableKey(deployKey.id)
.then(this.fetchKeys)
.catch(() => new Flash(s__('DeployKeys|Error enabling deploy key')));
},
methods: {
fetchKeys() {
this.isLoading = true;
this.service.getKeys()
.then((data) => {
this.isLoading = false;
this.store.keys = data;
})
.catch(() => new Flash('Error getting deploy keys'));
},
enableKey(deployKey) {
this.service.enableKey(deployKey.id)
.then(() => this.fetchKeys())
.catch(() => new Flash('Error enabling deploy key'));
},
disableKey(deployKey, callback) {
// eslint-disable-next-line no-alert
if (confirm('You are going to remove this deploy key. Are you sure?')) {
this.service.disableKey(deployKey.id)
.then(() => this.fetchKeys())
.then(callback)
.catch(() => new Flash('Error removing deploy key'));
} else {
callback();
}
},
disableKey(deployKey, callback) {
// eslint-disable-next-line no-alert
if (confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
this.service
.disableKey(deployKey.id)
.then(this.fetchKeys)
.then(callback)
.catch(() => new Flash(s__('DeployKeys|Error removing deploy key')));
} else {
callback();
}
},
};
},
};
</script>
<template>
......@@ -82,29 +117,38 @@
<loading-icon
v-if="isLoading && !hasKeys"
size="2"
label="Loading deploy keys"
:label="s__('DeployKeys|Loading deploy keys')"
/>
<div v-else-if="hasKeys">
<template v-else-if="hasKeys">
<div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
<div class="fade-left">
<i
class="fa fa-angle-left"
aria-hidden="true"
>
</i>
</div>
<div class="fade-right">
<i
class="fa fa-angle-right"
aria-hidden="true"
>
</i>
</div>
<navigation-tabs
:tabs="tabs"
@onChangeTab="onChangeTab"
scope="deployKeys"
/>
</div>
<keys-panel
title="Enabled deploy keys for this project"
class="qa-project-deploy-keys"
:keys="keys.enabled_keys"
:store="store"
:endpoint="endpoint"
/>
<keys-panel
title="Deploy keys from projects you have access to"
:keys="keys.available_project_keys"
:store="store"
:endpoint="endpoint"
/>
<keys-panel
v-if="keys.public_keys.length"
title="Public deploy keys available to any project"
:keys="keys.public_keys"
:project-id="projectId"
:keys="keys[currentTab]"
:store="store"
:endpoint="endpoint"
/>
</div>
</template>
</div>
</template>
<script>
import actionBtn from './action_btn.vue';
import { getTimeago } from '../../lib/utils/datetime_utility';
import tooltip from '../../vue_shared/directives/tooltip';
import _ from 'underscore';
import { s__, sprintf } from '~/locale';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
components: {
actionBtn,
},
directives: {
tooltip,
},
props: {
deployKey: {
type: Object,
required: true,
},
store: {
type: Object,
required: true,
},
endpoint: {
type: String,
required: true,
},
},
computed: {
timeagoDate() {
return getTimeago().format(this.deployKey.created_at);
},
editDeployKeyPath() {
return `${this.endpoint}/${this.deployKey.id}/edit`;
},
},
methods: {
isEnabled(id) {
return this.store.findEnabledKey(id) !== undefined;
},
tooltipTitle(project) {
return project.can_push ? 'Write access allowed' : 'Read access only';
},
},
};
import actionBtn from './action_btn.vue';
export default {
components: {
actionBtn,
icon,
},
directives: {
tooltip,
},
mixins: [timeagoMixin],
props: {
deployKey: {
type: Object,
required: true,
},
store: {
type: Object,
required: true,
},
endpoint: {
type: String,
required: true,
},
projectId: {
type: String,
required: false,
default: null,
},
},
data() {
return {
projectsExpanded: false,
};
},
computed: {
editDeployKeyPath() {
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: {
projectTooltipTitle(project) {
return project.can_push
? s__('DeployKeys|Write access allowed')
: s__('DeployKeys|Read access only');
},
toggleExpanded() {
this.projectsExpanded = !this.projectsExpanded;
},
},
};
</script>
<template>
<div>
<div class="pull-left append-right-10 hidden-xs">
<i
aria-hidden="true"
class="fa fa-key key-icon"
>
</i>
<div class="gl-responsive-table-row deploy-key">
<div class="table-section section-40">
<div
role="rowheader"
class="table-mobile-header">
{{ s__('DeployKeys|Deploy key') }}
</div>
<div class="table-mobile-content">
<strong class="title qa-key-title">
{{ deployKey.title }}
</strong>
<div class="fingerprint qa-key-fingerprint">
{{ deployKey.fingerprint }}
</div>
</div>
</div>
<div class="deploy-key-content key-list-item-info">
<strong class="title qa-key-title">
{{ deployKey.title }}
</strong>
<div class="description qa-key-fingerprint">
{{ deployKey.fingerprint }}
<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
v-if="isExpandable"
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"
:href="deployKeysProject.project.full_path"
:title="projectTooltipTitle(deployKeysProject)"
v-tooltip
>
<span>
{{ deployKeysProject.project.full_name }}
</span>
<icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'"/>
</a>
</template>
<span
v-else
class="text-secondary">{{ __('None') }}</span>
</div>
</div>
<div class="deploy-key-content prepend-left-default deploy-key-projects">
<a
v-for="(deployKeysProject, i) in deployKey.deploy_keys_projects"
:key="i"
class="label deploy-project-label"
:href="deployKeysProject.project.full_path"
:title="tooltipTitle(deployKeysProject)"
v-tooltip
>
{{ deployKeysProject.project.full_name }}
<i
v-if="!deployKeysProject.can_push"
aria-hidden="true"
class="fa fa-lock"
>
</i>
</a>
<div class="table-section section-15 text-right">
<div
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>
</div>
</div>
<div class="deploy-key-content">
<span class="key-created-at">
created {{ timeagoDate }}
</span>
<a
v-if="deployKey.can_edit"
class="btn btn-sm"
:href="editDeployKeyPath"
>
Edit
</a>
<action-btn
v-if="!isEnabled(deployKey.id)"
:deploy-key="deployKey"
type="enable"
/>
<action-btn
v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned"
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="remove"
/>
<action-btn
v-else
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="disable"
/>
<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
v-if="deployKey.can_edit"
class="btn btn-default text-secondary"
:href="editDeployKeyPath"
:title="__('Edit')"
data-container="body"
v-tooltip
>
<icon name="pencil"/>
</a>
<action-btn
v-if="isRemovable"
:deploy-key="deployKey"
btn-css-class="btn-danger"
type="remove"
:title="__('Remove')"
data-container="body"
v-tooltip
>
<icon name="remove"/>
</action-btn>
<action-btn
v-else-if="isEnabled"
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="disable"
:title="__('Disable')"
data-container="body"
v-tooltip
>
<icon name="cancel"/>
</action-btn>
</div>
</div>
</div>
</template>
<script>
import key from './key.vue';
import deployKey from './key.vue';
export default {
components: {
key,
export default {
components: {
deployKey,
},
props: {
keys: {
type: Array,
required: true,
},
props: {
title: {
type: String,
required: true,
},
keys: {
type: Array,
required: true,
},
showHelpBox: {
type: Boolean,
required: false,
default: true,
},
store: {
type: Object,
required: true,
},
endpoint: {
type: String,
required: true,
},
store: {
type: Object,
required: true,
},
};
endpoint: {
type: String,
required: true,
},
projectId: {
type: String,
required: false,
default: null,
},
},
};
</script>
<template>
<div class="deploy-keys-panel">
<h5>
{{ title }}
({{ keys.length }})
</h5>
<ul
class="well-list"
v-if="keys.length"
>
<li
<div class="deploy-keys-panel table-holder">
<template v-if="keys.length > 0">
<div
role="row"
class="gl-responsive-table-row table-row-header">
<div
role="rowheader"
class="table-section section-40">
{{ s__('DeployKeys|Deploy key') }}
</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"
:key="deployKey.id"
>
<key
:deploy-key="deployKey"
:store="store"
:endpoint="endpoint"
/>
</li>
</ul>
:deploy-key="deployKey"
:store="store"
:endpoint="endpoint"
:project-id="projectId"
/>
</template>
<div
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>
</template>
import Vue from 'vue';
import deployKeysApp from './components/app.vue';
export default () => new Vue({
el: document.getElementById('js-deploy-keys'),
components: {
deployKeysApp,
},
data() {
return {
endpoint: this.$options.el.dataset.endpoint,
};
},
render(createElement) {
return createElement('deploy-keys-app', {
props: {
endpoint: this.endpoint,
},
});
},
});
export default () =>
new Vue({
el: document.getElementById('js-deploy-keys'),
components: {
deployKeysApp,
},
data() {
return {
endpoint: this.$options.el.dataset.endpoint,
projectId: this.$options.el.dataset.projectId,
};
},
render(createElement) {
return createElement('deploy-keys-app', {
props: {
endpoint: this.endpoint,
projectId: this.projectId,
},
});
},
});
......@@ -7,21 +7,24 @@ export default class DeployKeysService {
constructor(endpoint) {
this.endpoint = endpoint;
this.resource = Vue.resource(`${this.endpoint}{/id}`, {}, {
enable: {
method: 'PUT',
url: `${this.endpoint}{/id}/enable`,
this.resource = Vue.resource(
`${this.endpoint}{/id}`,
{},
{
enable: {
method: 'PUT',
url: `${this.endpoint}{/id}/enable`,
},
disable: {
method: 'PUT',
url: `${this.endpoint}{/id}/disable`,
},
},
disable: {
method: 'PUT',
url: `${this.endpoint}{/id}/disable`,
},
});
);
}
getKeys() {
return this.resource.get()
.then(response => response.json());
return this.resource.get().then(response => response.json());
}
enableKey(id) {
......
......@@ -3,7 +3,7 @@ export default class DeployKeysStore {
this.keys = {};
}
findEnabledKey(id) {
return this.keys.enabled_keys.find(key => key.id === id);
isEnabled(id) {
return this.keys.enabled_keys.some(key => key.id === id);
}
}
......@@ -7,12 +7,12 @@ import { __ } from '~/locale';
export default class GpgBadges {
static fetch() {
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>');
const params = parseQueryStringIntoObject(form.serialize());
return axios.get(form.data('signaturesPath'), { params })
const params = parseQueryStringIntoObject(tag.serialize());
return axios.get(tag.data('signaturesPath'), { params })
.then(({ data }) => {
data.signatures.forEach((signature) => {
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>
import { mapState } from 'vuex';
import { mapActions, mapState } from 'vuex';
import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
......@@ -9,7 +9,7 @@ export default {
RadioGroup,
},
computed: {
...mapState(['currentBranchId']),
...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
......@@ -17,6 +17,17 @@ export default {
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,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
......@@ -44,6 +55,7 @@ export default {
:value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')"
:show-input="true"
:disabled="disableMergeRequestRadio"
/>
</div>
</template>
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { mapState } from 'vuex';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState(['lastCommitMsg', 'rightPanelCollapsed', 'changedFiles', 'stagedFiles']),
...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
},
methods: {
...mapActions(['toggleRightPanelCollapsed']),
...mapState(['lastCommitMsg', 'noChangesStateSvgPath']),
},
};
</script>
......@@ -31,31 +13,8 @@ export default {
v-if="!lastCommitMsg"
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
class="ide-commit-empty-state-container"
v-if="!rightPanelCollapsed"
>
<div class="svg-content svg-80">
<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>
import { mapActions, mapState, mapGetters } from 'vuex';
import { mapActions } from 'vuex';
import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import ListItem from './list_item.vue';
import ListCollapsed from './list_collapsed.vue';
export default {
components: {
Icon,
ListItem,
ListCollapsed,
},
directives: {
tooltip,
......@@ -24,11 +22,6 @@ export default {
type: Array,
required: true,
},
showToggle: {
type: Boolean,
required: false,
default: true,
},
iconName: {
type: String,
required: true,
......@@ -51,9 +44,12 @@ export default {
default: false,
},
},
data() {
return {
showActionButton: false,
};
},
computed: {
...mapState(['rightPanelCollapsed']),
...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
titleText() {
return sprintf(__('%{title} changes'), {
title: this.title,
......@@ -61,10 +57,13 @@ export default {
},
},
methods: {
...mapActions(['toggleRightPanelCollapsed', 'stageAllChanges', 'unstageAllChanges']),
...mapActions(['stageAllChanges', 'unstageAllChanges']),
actionBtnClicked() {
this[this.action]();
},
setShowActionButton(show) {
this.showActionButton = show;
},
},
};
</script>
......@@ -72,19 +71,14 @@ export default {
<template>
<div
class="ide-commit-list-container"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<header
class="multi-file-commit-panel-header"
@mouseenter="setShowActionButton(true)"
@mouseleave="setShowActionButton(false)"
>
<div
v-if="!rightPanelCollapsed"
class="multi-file-commit-panel-header-title"
:class="{
'append-right-10': showToggle,
}"
>
<icon
v-once
......@@ -92,7 +86,14 @@ export default {
:size="18"
/>
{{ titleText }}
<span
v-show="!showActionButton"
class="ide-commit-file-count"
>
{{ fileList.length }}
</span>
<button
v-show="showActionButton"
type="button"
class="btn btn-blank btn-link ide-staged-action-btn"
@click="actionBtnClicked"
......@@ -100,52 +101,28 @@ export default {
{{ actionBtnText }}
</button>
</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>
<list-collapsed
v-if="rightPanelCollapsed"
:files="fileList"
:icon-name="iconName"
:title="title"
/>
<template v-else>
<ul
v-if="fileList.length"
class="multi-file-commit-list list-unstyled append-bottom-0"
>
<li
v-for="file in fileList"
:key="file.key"
>
<list-item
:file="file"
:action-component="itemActionComponent"
:key-prefix="title"
:staged-list="stagedList"
/>
</li>
</ul>
<p
v-else
class="multi-file-commit-list help-block"
<ul
v-if="fileList.length"
class="multi-file-commit-list list-unstyled append-bottom-0"
>
<li
v-for="file in fileList"
:key="file.key"
>
{{ __('No changes') }}
</p>
</template>
<list-item
:file="file"
:action-component="itemActionComponent"
:key-prefix="title"
:staged-list="stagedList"
/>
</li>
</ul>
<p
v-else
class="multi-file-commit-list help-block"
>
{{ __('No changes') }}
</p>
</div>
</template>
......@@ -3,6 +3,7 @@ import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue';
import { viewerTypes } from '../../constants';
export default {
components: {
......@@ -53,7 +54,7 @@ export default {
keyPrefix: this.keyPrefix.toLowerCase(),
}).then(changeViewer => {
if (changeViewer) {
this.updateViewer('diff');
this.updateViewer(viewerTypes.diff);
}
});
},
......
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
......@@ -26,10 +27,20 @@ export default {
required: false,
default: false,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState('commit', ['commitAction']),
...mapGetters('commit', ['newBranchName']),
tooltipTitle() {
return this.disabled
? __('This option is disabled while you still have unstaged changes')
: '';
},
},
methods: {
...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
......@@ -39,19 +50,28 @@ export default {
<template>
<fieldset>
<label>
<label
v-tooltip
:title="tooltipTitle"
:class="{
'is-disabled': disabled
}"
>
<input
type="radio"
name="commit-action"
:value="value"
@change="updateCommitAction($event.target.value)"
:checked="checked"
v-once
:checked="commitAction === value"
:disabled="disabled"
/>
<span class="prepend-left-10">
<template v-if="label">
<span
v-if="label"
class="ide-radio-label"
>
{{ label }}
</template>
</span>
<slot v-else></slot>
</span>
</label>
......
......@@ -2,14 +2,8 @@
import { mapState } from 'vuex';
export default {
props: {
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState(['lastCommitMsg']),
...mapState(['lastCommitMsg', 'committedStateSvgPath']),
},
};
</script>
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
import { __, sprintf } from '~/locale';
import { viewerTypes } from '../constants';
export default {
components: {
Icon,
},
props: {
hasChanges: {
type: Boolean,
required: false,
default: false,
},
mergeRequestId: {
type: String,
required: false,
default: '',
},
viewer: {
type: String,
required: true,
},
showShadow: {
type: Boolean,
mergeRequestId: {
type: Number,
required: true,
},
},
......@@ -38,84 +25,45 @@ export default {
this.$emit('click', mode);
},
},
viewerTypes,
};
</script>
<template>
<div
class="dropdown"
:class="{
shadow: showShadow,
}"
>
<button
type="button"
class="btn btn-primary btn-sm"
:class="{
'btn-inverted': hasChanges,
}"
class="btn btn-link"
data-toggle="dropdown"
>
<template v-if="viewer === 'mrdiff' && mergeRequestId">
{{ 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"
/>
{{ __('Edit') }}
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul>
<template v-if="mergeRequestId">
<li>
<a
href="#"
@click.prevent="changeMode('mrdiff')"
:class="{
'is-active': viewer === 'mrdiff',
}"
>
<strong class="dropdown-menu-inner-title">
{{ mergeReviewLine }}
</strong>
<span class="dropdown-menu-inner-content">
{{ __('Compare changes with the merge request target branch') }}
</span>
</a>
</li>
<li
role="separator"
class="divider"
>
</li>
</template>
<li>
<a
href="#"
@click.prevent="changeMode('editor')"
@click.prevent="changeMode($options.viewerTypes.mr)"
:class="{
'is-active': viewer === 'editor',
'is-active': viewer === $options.viewerTypes.mr,
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong>
<strong class="dropdown-menu-inner-title">
{{ mergeReviewLine }}
</strong>
<span class="dropdown-menu-inner-content">
{{ __('View and edit lines') }}
{{ __('Compare changes with the merge request target branch') }}
</span>
</a>
</li>
<li>
<a
href="#"
@click.prevent="changeMode('diff')"
@click.prevent="changeMode($options.viewerTypes.diff)"
:class="{
'is-active': viewer === 'diff',
'is-active': viewer === $options.viewerTypes.diff,
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
......
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import Mousetrap from 'mousetrap';
import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.vue';
import Mousetrap from 'mousetrap';
import { mapActions, mapState, mapGetters } from 'vuex';
import IdeSidebar from './ide_side_bar.vue';
import RepoTabs from './repo_tabs.vue';
import IdeStatusBar from './ide_status_bar.vue';
import RepoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.vue';
const originalStopCallback = Mousetrap.stopCallback;
const originalStopCallback = Mousetrap.stopCallback;
export default {
components: {
ideSidebar,
ideContextbar,
repoTabs,
ideStatusBar,
repoEditor,
FindFile,
},
props: {
emptyStateSvgPath: {
type: String,
required: true,
},
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState([
'changedFiles',
'openFiles',
'viewer',
'currentMergeRequestId',
'fileFindVisible',
]),
...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined;
export default {
components: {
IdeSidebar,
RepoTabs,
IdeStatusBar,
RepoEditor,
FindFile,
},
computed: {
...mapState([
'changedFiles',
'openFiles',
'viewer',
'currentMergeRequestId',
'fileFindVisible',
'emptyStateSvgPath',
]),
...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined;
Object.assign(e, {
returnValue,
});
return returnValue;
};
Object.assign(e, {
returnValue,
});
return returnValue;
};
Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
if (e.preventDefault) {
e.preventDefault();
}
Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
if (e.preventDefault) {
e.preventDefault();
}
this.toggleFileFinder(!this.fileFindVisible);
});
this.toggleFileFinder(!this.fileFindVisible);
});
Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
},
methods: {
...mapActions(['toggleFileFinder']),
mousetrapStopCallback(e, el, combo) {
if (combo === 't' && el.classList.contains('dropdown-input-field')) {
return true;
} else if (combo === 'command+p' || combo === 'ctrl+p') {
return false;
}
Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
},
methods: {
...mapActions(['toggleFileFinder']),
mousetrapStopCallback(e, el, combo) {
if (combo === 't' && el.classList.contains('dropdown-input-field')) {
return true;
} else if (combo === 'command+p' || combo === 'ctrl+p') {
return false;
}
return originalStopCallback(e, el, combo);
},
return originalStopCallback(e, el, combo);
},
};
},
};
</script>
<template>
<div
class="ide-view"
>
<find-file
v-show="fileFindVisible"
/>
<ide-sidebar />
<article class="ide">
<div
class="multi-file-edit-pane"
class="ide-view"
>
<template
v-if="activeFile"
>
<repo-tabs
:active-file="activeFile"
:files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
:merge-request-id="currentMergeRequestId"
/>
<repo-editor
class="multi-file-edit-pane-content"
:file="activeFile"
/>
<ide-status-bar
:file="activeFile"
/>
</template>
<template
v-else
<find-file
v-show="fileFindVisible"
/>
<ide-sidebar />
<div
class="multi-file-edit-pane"
>
<div
v-once
class="ide-empty-state"
<template
v-if="activeFile"
>
<div class="row js-empty-state">
<div class="col-xs-12">
<div class="svg-content svg-250">
<img :src="emptyStateSvgPath" />
<repo-tabs
:active-file="activeFile"
:files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
:merge-request-id="currentMergeRequestId"
/>
<repo-editor
class="multi-file-edit-pane-content"
:file="activeFile"
/>
</template>
<template
v-else
>
<div
v-once
class="ide-empty-state"
>
<div class="row js-empty-state">
<div class="col-xs-12">
<div class="svg-content svg-250">
<img :src="emptyStateSvgPath" />
</div>
</div>
</div>
<div class="col-xs-12">
<div class="text-content text-center">
<h4>
Welcome to the GitLab IDE
</h4>
<p>
You can select a file in the left sidebar to begin
editing and use the right sidebar to commit your changes.
</p>
<div class="col-xs-12">
<div class="text-content text-center">
<h4>
Welcome to the GitLab IDE
</h4>
<p>
You can select a file in the left sidebar to begin
editing and use the right sidebar to commit your changes.
</p>
</div>
</div>
</div>
</div>
</div>
</template>
</template>
</div>
</div>
<ide-contextbar
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
<ide-status-bar
:file="activeFile"
/>
</div>
</article>
</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>
import { mapState, mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import projectTree from './ide_project_tree.vue';
import ResizablePanel from './resizable_panel.vue';
import { mapState, mapGetters } from 'vuex';
import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import PanelResizer from '~/vue_shared/components/panel_resizer.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 {
components: {
projectTree,
icon,
panelResizer,
skeletonLoadingContainer,
ResizablePanel,
export default {
directives: {
tooltip,
},
components: {
Icon,
PanelResizer,
SkeletonLoadingContainer,
ResizablePanel,
ActivityBar,
ProjectAvatarImage,
Identicon,
CommitSection,
IdeTree,
CommitForm,
IdeReview,
SuccessMessage,
},
data() {
return {
showTooltip: false,
};
},
computed: {
...mapState([
'loading',
'currentBranchId',
'currentActivityView',
'changedFiles',
'stagedFiles',
'lastCommitMsg',
]),
...mapGetters(['currentProject', 'someUncommitedChanges']),
showSuccessMessage() {
return (
this.currentActivityView === activityBarViews.edit &&
(this.lastCommitMsg && !this.someUncommitedChanges)
);
},
computed: {
...mapState([
'loading',
]),
...mapGetters([
'projectsWithTrees',
]),
branchTooltipTitle() {
return this.showTooltip ? this.currentBranchId : undefined;
},
};
},
watch: {
currentBranchId() {
this.$nextTick(() => {
this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth;
});
},
},
};
</script>
<template>
<resizable-panel
:collapsible="false"
:initial-width="290"
:initial-width="340"
side="left"
>
<activity-bar
v-if="!loading"
/>
<div class="multi-file-commit-panel-inner">
<template v-if="loading">
<div
......@@ -41,11 +87,54 @@
<skeleton-loading-container />
</div>
</template>
<project-tree
v-for="project in projectsWithTrees"
:key="project.id"
:project="project"
/>
<template v-else>
<div class="context-header ide-context-header">
<a
: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>
<commit-form />
</template>
</div>
</resizable-panel>
</template>
<script>
import { mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
components: {
icon,
userAvatarImage,
},
directives: {
tooltip,
......@@ -14,40 +17,93 @@ export default {
props: {
file: {
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>
<template>
<div class="ide-status-bar">
<div>
<div v-if="file.lastCommit && file.lastCommit.id">
Last commit:
<a
v-tooltip
:title="file.lastCommit.message"
:href="file.lastCommit.url"
>
{{ timeFormated(file.lastCommit.updatedAt) }} by
{{ file.lastCommit.author }}
</a>
</div>
<footer class="ide-status-bar">
<div
class="ide-status-branch"
v-if="lastCommit && lastCommitFormatedAge"
>
<icon
name="commit"
/>
<a
v-tooltip
class="commit-sha"
: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)"
>
{{ lastCommitFormatedAge }}
</time>
</div>
<div class="text-right">
<div
v-if="file"
class="ide-status-file"
>
{{ file.name }}
</div>
<div class="text-right">
<div
v-if="file"
class="ide-status-file"
>
{{ file.eol }}
</div>
<div
class="text-right"
v-if="!file.binary">
class="ide-status-file"
v-if="file && !file.binary">
{{ file.editorRow }}:{{ file.editorColumn }}
</div>
<div class="text-right">
<div
v-if="file"
class="ide-status-file"
>
{{ file.fileLanguage }}
</div>
</div>
</footer>
</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>
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 RepoFile from './repo_file.vue';
import NewDropdown from './new_dropdown/index.vue';
export default {
components: {
Icon,
RepoFile,
SkeletonLoadingContainer,
NewDropdown,
},
props: {
tree: {
type: Object,
viewerType: {
type: String,
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>
......@@ -20,7 +48,7 @@ export default {
<div
class="ide-file-list"
>
<template v-if="tree.loading">
<template v-if="showLoading">
<div
class="multi-file-loading-container"
v-for="n in 3"
......@@ -30,11 +58,18 @@ export default {
</div>
</template>
<template v-else>
<header
class="ide-tree-header"
:class="headerClass"
>
<slot name="header"></slot>
</header>
<repo-file
v-for="file in tree.tree"
v-for="file in currentTree.tree"
:key="file.key"
:file="file"
:level="0"
:disable-action-dropdown="disableActionDropdown"
/>
</template>
</div>
......
......@@ -17,7 +17,8 @@ export default {
},
path: {
type: String,
required: true,
required: false,
default: '',
},
},
data() {
......
......@@ -3,13 +3,10 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.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 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 Actions from './commit_sidebar/actions.vue';
import { activityBarViews } from '../constants';
export default {
components: {
......@@ -17,42 +14,50 @@ export default {
Icon,
CommitFilesList,
EmptyState,
SuccessMessage,
Actions,
LoadingButton,
CommitMessageField,
},
directives: {
tooltip,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState([
'changedFiles',
'stagedFiles',
'rightPanelCollapsed',
'lastCommitMsg',
'unusedSeal',
]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
showStageUnstageArea() {
return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
},
someUncommitedChanges() {
return !!(this.changedFiles.length || this.stagedFiles.length);
},
watch: {
hasChanges() {
if (!this.hasChanges) {
this.updateActivityBarView(activityBarViews.edit);
}
},
...mapState(['changedFiles', 'stagedFiles', 'rightPanelCollapsed', 'lastCommitMsg', 'unusedSeal']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
},
mounted() {
if (this.lastOpenedFile) {
this.openPendingTab({
file: this.lastOpenedFile,
})
.then(changeViewer => {
if (changeViewer) {
this.updateViewer('diff');
}
})
.catch(e => {
throw e;
});
}
},
methods: {
...mapActions('commit', [
'updateCommitMessage',
'discardDraft',
'commitChanges',
'updateCommitAction',
]),
...mapActions(['openPendingTab', 'updateViewer', 'updateActivityBarView']),
...mapActions('commit', ['commitChanges', 'updateCommitAction']),
forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
},
......@@ -80,6 +85,7 @@ export default {
v-if="showStageUnstageArea"
>
<commit-files-list
class="is-first"
icon-name="unstaged"
:title="__('Unstaged')"
:file-list="changedFiles"
......@@ -94,49 +100,11 @@ export default {
action="unstageAllChanges"
:action-btn-text="__('Unstage all')"
item-action-component="unstage-button"
:show-toggle="false"
:staged-list="true"
/>
</template>
<empty-state
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>
</template>
......@@ -3,6 +3,7 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import { activityBarViews, viewerTypes } from '../constants';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
import IdeFileButtons from './ide_file_buttons.vue';
......@@ -19,8 +20,14 @@ export default {
},
},
computed: {
...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
...mapGetters(['currentMergeRequest', 'getStagedFile']),
...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']),
...mapGetters([
'currentMergeRequest',
'getStagedFile',
'isEditModeActive',
'isCommitModeActive',
'isReviewModeActive',
]),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.content;
},
......@@ -40,6 +47,21 @@ export default {
// Compare key to allow for files opened in review mode to be cached differently
if (newVal.key !== this.file.key) {
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() {
......@@ -77,7 +99,6 @@ export default {
'setFileViewMode',
'setFileEOL',
'updateViewer',
'updateDelayViewerUpdated',
]),
initMonaco() {
if (this.shouldHideEditor) return;
......@@ -89,14 +110,6 @@ export default {
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
})
.then(() => {
const viewerPromise = this.delayViewerUpdated
? this.updateViewer(this.file.pending ? 'diff' : 'editor')
: Promise.resolve();
return viewerPromise;
})
.then(() => {
this.updateDelayViewerUpdated(false);
this.createEditorInstance();
})
.catch(err => {
......@@ -108,10 +121,10 @@ export default {
this.editor.dispose();
this.$nextTick(() => {
if (this.viewer === 'editor') {
if (this.viewer === viewerTypes.edit) {
this.editor.createInstance(this.$refs.editor);
} else {
this.editor.createDiffInstance(this.$refs.editor);
this.editor.createDiffInstance(this.$refs.editor, !this.isReviewModeActive);
}
this.setupEditor();
......@@ -127,7 +140,7 @@ export default {
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);
} else {
this.editor.attachModel(this.model);
......@@ -168,6 +181,7 @@ export default {
});
},
},
viewerTypes,
};
</script>
......@@ -176,16 +190,17 @@ export default {
id="ide"
class="blob-viewer-container blob-editor-container"
>
<div class="ide-mode-tabs clearfix">
<div class="ide-mode-tabs clearfix" >
<ul
class="nav-links pull-left"
v-if="!shouldHideEditor">
v-if="!shouldHideEditor && isEditModeActive"
>
<li :class="editTabCSS">
<a
href="javascript:void(0);"
role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
<template v-if="viewer === 'editor'">
<template v-if="viewer === $options.viewerTypes.edit">
{{ __('Edit') }}
</template>
<template v-else>
......@@ -212,6 +227,9 @@ export default {
v-show="!shouldHideEditor && file.viewMode === 'edit'"
ref="editor"
class="multi-file-editor-holder"
:class="{
'is-readonly': isCommitModeActive,
}"
>
</div>
<content-viewer
......
......@@ -34,6 +34,11 @@ export default {
type: Number,
required: true,
},
disableActionDropdown: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapGetters([
......@@ -99,16 +104,14 @@ export default {
}
},
methods: {
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
...mapActions(['toggleTreeOpen']),
clickFile() {
// Manual Action if a tree is selected/opened
if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
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 {
/>
</span>
<new-dropdown
v-if="isTree"
v-if="isTree && !disableActionDropdown"
:project-id="file.projectId"
:branch="file.branchId"
:path="file.path"
......
......@@ -32,6 +32,8 @@ export default {
return `Close ${this.tab.name}`;
},
showChangedIcon() {
if (this.tab.pending) return true;
return this.fileHasChanged ? !this.tabMouseOver : false;
},
fileHasChanged() {
......@@ -66,15 +68,32 @@ export default {
<template>
<li
:class="{
active: tab.active
}"
@click="clickFile(tab)"
@mouseover="mouseOverTab"
@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
type="button"
class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)"
:aria-label="closeLabel"
:disabled="tab.pending"
>
<icon
v-if="!showChangedIcon"
......@@ -87,22 +106,5 @@ export default {
:force-modified-icon="true"
/>
</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>
</template>
......@@ -32,16 +32,6 @@ export default {
default: '',
},
},
data() {
return {
showShadow: false,
};
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions(['updateViewer', 'removePendingTab']),
openFileViewer(viewer) {
......@@ -71,12 +61,5 @@ export default {
:tab="tab"
/>
</ul>
<editor-mode
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
:merge-request-id="mergeRequestId"
@click="openFileViewer"
/>
</div>
</template>
......@@ -3,6 +3,22 @@ export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55;
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
export const MAX_WINDOW_HEIGHT_COMPACT = 750;
export const COMMIT_ITEM_PADDING = 32;
// Commit message textarea
export const MAX_TITLE_LENGTH = 50;
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';
import VueRouter from 'vue-router';
import flash from '~/flash';
import store from './stores';
import { activityBarViews } from './constants';
Vue.use(VueRouter);
......@@ -63,6 +64,8 @@ router.beforeEach((to, from, next) => {
const fullProjectId = `${to.params.namespace}/${to.params.project}`;
if (to.params.branch) {
store.dispatch('setCurrentBranchId', to.params.branch);
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: to.params.branch,
......@@ -99,14 +102,14 @@ router.beforeEach((to, from, next) => {
throw e;
});
} else if (to.params.mrid) {
store.dispatch('updateViewer', 'mrdiff');
store
.dispatch('getMergeRequestData', {
projectId: fullProjectId,
mergeRequestId: to.params.mrid,
})
.then(mr => {
store.dispatch('updateActivityBarView', activityBarViews.review);
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: mr.source_branch,
......
......@@ -16,15 +16,16 @@ export function initIde(el) {
components: {
ide,
},
render(createElement) {
return createElement('ide', {
props: {
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
committedStateSvgPath: el.dataset.committedStateSvgPath,
},
created() {
this.$store.dispatch('setEmptyStateSvgs', {
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
committedStateSvgPath: el.dataset.committedStateSvgPath,
});
},
render(createElement) {
return createElement('ide');
},
});
}
......
......@@ -61,19 +61,19 @@ export default class Editor {
}
}
createDiffInstance(domElement) {
createDiffInstance(domElement, readOnly = true) {
if (!this.instance) {
clearDomElement(domElement);
this.disposable.add(
(this.instance = this.monaco.editor.createDiffEditor(domElement, {
...defaultEditorOptions,
readOnly: true,
quickSuggestions: false,
occurrencesHighlight: false,
renderLineHighlight: 'none',
hideCursorInOverviewRuler: true,
renderSideBySide: Editor.renderSideBySide(domElement),
readOnly,
renderLineHighlight: readOnly ? 'all' : 'none',
hideCursorInOverviewRuler: !readOnly,
})),
);
......
......@@ -123,6 +123,8 @@ export const scrollToTab = () => {
};
export const stageAllChanges = ({ state, commit }) => {
commit(types.SET_LAST_COMMIT_MSG, '');
state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
};
......@@ -138,6 +140,18 @@ export const updateDelayViewerUpdated = ({ commit }, 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 }) => {
commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile });
......
......@@ -5,6 +5,7 @@ import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
import { setPageTitle } from '../utils';
import { viewerTypes } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => {
const path = file.path;
......@@ -23,13 +24,12 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
const nextFileToOpen = state.openFiles[nextIndexToOpen];
if (nextFileToOpen.pending) {
dispatch('updateViewer', 'diff');
dispatch('updateViewer', viewerTypes.diff);
dispatch('openPendingTab', {
file: nextFileToOpen,
keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
});
} else {
dispatch('updateDelayViewerUpdated', true);
router.push(`/project${nextFileToOpen.url}`);
}
} else if (!state.openFiles.length) {
......@@ -184,6 +184,7 @@ export const stageChange = ({ commit, state }, path) => {
const stagedFile = state.stagedFiles.find(f => f.path === path);
commit(types.STAGE_CHANGE, path);
commit(types.SET_LAST_COMMIT_MSG, '');
if (stagedFile) {
eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
......@@ -195,9 +196,7 @@ export const unstageChange = ({ commit }, path) => {
};
export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => {
if (getters.activeFile && getters.activeFile === file && state.viewer === 'diff') {
return false;
}
state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`));
commit(types.ADD_PENDING_TAB, { file, keyPrefix });
......
......@@ -55,7 +55,6 @@ export const getBranchData = (
branch: data,
});
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
commit(types.SET_CURRENT_BRANCH, branchId);
resolve(data);
})
.catch(() => {
......@@ -73,3 +72,26 @@ export const getBranchData = (
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 { activityBarViews } from '../constants';
export const activeFile = state => state.openFiles.find(file => file.active) || null;
......@@ -31,15 +31,12 @@ export const currentMergeRequest = state => {
return null;
};
// eslint-disable-next-line no-confusing-arrow
export const collapseButtonIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const currentProject = state => state.projects[state.currentProjectId];
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 collapseButtonTooltip = state =>
state.rightPanelCollapsed ? __('Expand sidebar') : __('Collapse sidebar');
export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
export const hasMergeRequest = state => !!state.currentMergeRequestId;
......@@ -59,6 +56,16 @@ export const allBlobs = state =>
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 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 => {
const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f, path)).length;
const stagedFilesCount = state.stagedFiles.filter(
......@@ -74,5 +81,11 @@ export const getUnstagedFilesCountForPath = state => path =>
export const getStagedFilesCountForPath = state => 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
export default () => {};
......@@ -8,6 +8,7 @@ import router from '../../../ide_router';
import service from '../../../services';
import * as types from './mutation_types';
import * as consts from './constants';
import { activityBarViews } from '../../../constants';
import eventHub from '../../../eventhub';
export const updateCommitMessage = ({ commit }, message) => {
......@@ -75,7 +76,7 @@ export const checkCommitStatus = ({ rootState }) =>
export const updateFilesAfterCommit = (
{ commit, dispatch, state, rootState, rootGetters },
{ data, branch },
{ data },
) => {
const selectedProject = rootState.projects[rootState.currentProjectId];
const lastCommit = {
......@@ -126,15 +127,9 @@ export const updateFilesAfterCommit = (
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 payload = createCommitPayload(getters.branchName, newBranch, state, rootState);
const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus');
......@@ -187,7 +182,39 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 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 => {
let errMsg = __('Error committing changes. Please try again.');
......
......@@ -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_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS';
// Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT';
......@@ -19,6 +20,7 @@ export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
// Branch Mutation Types
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 TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
......@@ -59,6 +61,7 @@ export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_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 TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
......@@ -107,6 +107,21 @@ export default {
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) {
Object.assign(state, {
fileFindVisible,
......
......@@ -23,4 +23,9 @@ export default {
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';
export default {
......@@ -169,32 +170,24 @@ export default {
});
},
[types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
const key = `${keyPrefix}-${file.key}`;
const pendingTab = state.openFiles.find(f => f.key === key && f.pending);
let openFiles = state.openFiles.map(f => Object.assign(f, { active: false, opened: false }));
if (!pendingTab) {
const openFile = openFiles.find(f => f.path === file.path);
openFiles = openFiles.concat(openFile ? null : file).reduce((acc, f) => {
if (!f) return acc;
if (f.path === file.path) {
return acc.concat({
...f,
content: file.content,
active: true,
pending: true,
opened: true,
key,
});
}
return acc.concat(f);
}, []);
}
Object.assign(state, { openFiles });
state.entries[file.path].opened = false;
state.entries[file.path].active = false;
state.entries[file.path].lastOpenedAt = new Date().getTime();
state.openFiles.forEach(f =>
Object.assign(f, {
opened: false,
active: false,
}),
);
state.openFiles = [
{
...file,
key: `${keyPrefix}-${file.key}`,
pending: true,
opened: true,
active: true,
},
];
},
[types.REMOVE_PENDING_TAB](state, file) {
Object.assign(state, {
......
import { activityBarViews, viewerTypes } from '../constants';
export default () => ({
currentProjectId: '',
currentBranchId: '',
......@@ -16,8 +18,9 @@ export default () => ({
rightPanelCollapsed: false,
panelResizing: false,
entries: {},
viewer: 'editor',
viewer: viewerTypes.edit,
delayViewerUpdated: false,
currentActivityView: activityBarViews.edit,
unusedSeal: true,
fileFindVisible: false,
});
/* eslint-disable no-new */
import $ from 'jquery';
import flash from './flash';
import axios from './lib/utils/axios_utils';
......@@ -62,7 +60,7 @@ export default class MiniPipelineGraph {
*/
renderBuildsList(stageContainer, data) {
const dropdownContainer = stageContainer.parentElement.querySelector(
`${this.dropdownListSelector} .js-builds-dropdown-list`,
`${this.dropdownListSelector} .js-builds-dropdown-list ul`,
);
dropdownContainer.innerHTML = data;
......
......@@ -81,9 +81,8 @@ export default {
time: new Date(),
value: 0,
},
currentDataIndex: 0,
currentXCoordinate: 0,
currentFlagPosition: 0,
currentCoordinates: [],
showFlag: false,
showFlagContent: false,
timeSeries: [],
......@@ -273,6 +272,9 @@ export default {
:line-style="path.lineStyle"
:line-color="path.lineColor"
:area-color="path.areaColor"
:current-coordinates="currentCoordinates[index]"
:current-time-series-index="index"
:show-dot="showFlagContent"
/>
<graph-deployment
:deployment-data="reducedDeploymentData"
......@@ -298,9 +300,9 @@ export default {
:show-flag-content="showFlagContent"
:time-series="timeSeries"
:unit-of-display="unitOfDisplay"
:current-data-index="currentDataIndex"
:legend-title="legendTitle"
:deployment-flag-data="deploymentFlagData"
:current-coordinates="currentCoordinates"
/>
</div>
<graph-legend
......
......@@ -47,14 +47,14 @@ export default {
type: String,
required: true,
},
currentDataIndex: {
type: Number,
required: true,
},
legendTitle: {
type: String,
required: true,
},
currentCoordinates: {
type: Array,
required: true,
},
},
computed: {
formatTime() {
......@@ -90,10 +90,12 @@ export default {
},
},
methods: {
seriesMetricValue(series) {
seriesMetricValue(seriesIndex, series) {
const indexFromCoordinates = this.currentCoordinates[seriesIndex]
? this.currentCoordinates[seriesIndex].currentDataIndex : 0;
const index = this.deploymentFlagData
? this.deploymentFlagData.seriesIndex
: this.currentDataIndex;
: indexFromCoordinates;
const value = series.values[index] && series.values[index].value;
if (isNaN(value)) {
return '-';
......@@ -128,7 +130,7 @@ export default {
<h5 v-if="deploymentFlagData">
Deployed
</h5>
{{ formatDate }} at
{{ formatDate }}
<strong>{{ formatTime }}</strong>
</div>
<div
......@@ -163,9 +165,11 @@ export default {
:key="index"
>
<track-line :track="series"/>
<td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td>
<td>
<strong>{{ seriesMetricValue(series) }}</strong>
{{ series.track }} {{ seriesMetricLabel(index, series) }}
</td>
<td>
<strong>{{ seriesMetricValue(index, series) }}</strong>
</td>
</tr>
</table>
......
......@@ -22,6 +22,15 @@ export default {
type: String,
required: true,
},
currentCoordinates: {
type: Object,
required: false,
default: () => ({ currentX: 0, currentY: 0 }),
},
showDot: {
type: Boolean,
required: true,
},
},
computed: {
strokeDashArray() {
......@@ -33,12 +42,20 @@ export default {
};
</script>
<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
class="metric-area"
:d="generatedAreaPath"
:fill="areaColor"
transform="translate(-5, 20)"
/>
<path
class="metric-line"
......@@ -47,7 +64,6 @@ export default {
fill="none"
stroke-width="1"
:stroke-dasharray="strokeDashArray"
transform="translate(-5, 20)"
/>
</g>
</template>
......@@ -19,16 +19,16 @@ export default {
<template>
<td>
<svg
width="15"
height="6">
width="16"
height="8">
<line
:stroke-dasharray="stylizedLine"
:stroke="track.lineColor"
stroke-width="4"
:x1="0"
:x2="15"
:y1="2"
:y2="2"
:x2="16"
:y1="4"
:y2="4"
/>
</svg>
</td>
......
......@@ -52,14 +52,22 @@ const mixins = {
positionFlag() {
const timeSeries = this.timeSeries[0];
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1);
this.currentData = timeSeries.values[hoveredDataIndex];
this.currentDataIndex = hoveredDataIndex;
this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
if (this.currentXCoordinate > (this.graphWidth - 200)) {
this.currentFlagPosition = this.currentXCoordinate - 103;
} else {
this.currentFlagPosition = this.currentXCoordinate;
}
this.currentCoordinates = this.timeSeries.map((series) => {
const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate, 1);
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) {
this.showFlag = false;
......
......@@ -14,7 +14,7 @@ const d3 = {
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 dateFormatWithName = d3.time('%a, %b %-d');
export const bisectDate = d3.bisector(d => d.time).left;
......
......@@ -123,6 +123,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
linePath: lineFunction(timeSeries.values),
areaPath: areaFunction(timeSeries.values),
timeSeriesScaleX,
timeSeriesScaleY,
values: timeSeries.values,
max: maximumValue,
average: accum / timeSeries.values.length,
......
import initSettingsPanels from '~/settings_panels';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
const variableListEl = document.querySelector('.js-ci-variable-list-section');
// eslint-disable-next-line no-new
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 initChangesDropdown from '~/init_changes_dropdown';
import GpgBadges from '~/gpg_badges';
document.addEventListener('DOMContentLoaded', () => {
new Diff(); // eslint-disable-line no-new
const paddingTop = 16;
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
GpgBadges.fetch();
});
......@@ -61,7 +61,7 @@ export default {
methods: {
onClickAction() {
$(this.$el).tooltip('hide');
eventHub.$emit('graphAction', this.link);
eventHub.$emit('postAction', this.link);
this.linkRequested = this.link;
this.isDisabled = true;
},
......
......@@ -87,7 +87,8 @@ export default {
data-toggle="dropdown"
data-container="body"
class="dropdown-menu-toggle build-content"
:title="tooltipText">
:title="tooltipText"
>
<job-name-component
:name="job.name"
......@@ -104,7 +105,8 @@ export default {
<ul>
<li
v-for="(item, i) in job.jobs"
:key="i">
:key="i"
>
<job-component
:job="item"
css-class-job-name="mini-pipeline-graph-dropdown-item"
......
......@@ -108,7 +108,7 @@ export default {
<div
v-else
v-tooltip
class="js-job-component-tooltip"
class="js-job-component-tooltip non-details-job-component"
:title="tooltipText"
:class="cssClassJobName"
data-html="true"
......
<script>
/**
* Renders each stage of the pipeline mini graph.
*
* Given the provided endpoint will make a request to
* fetch the dropdown data when the stage is clicked.
*
* Request is made inside this component to make it reusable between:
* 1. Pipelines main table
* 2. Pipelines table in commit and Merge request views
* 3. Merge request widget
* 4. Commit widget
*/
import $ from 'jquery';
import Flash from '../../flash';
import axios from '../../lib/utils/axios_utils';
import eventHub from '../event_hub';
import Icon from '../../vue_shared/components/icon.vue';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
LoadingIcon,
Icon,
/**
* Renders each stage of the pipeline mini graph.
*
* Given the provided endpoint will make a request to
* fetch the dropdown data when the stage is clicked.
*
* Request is made inside this component to make it reusable between:
* 1. Pipelines main table
* 2. Pipelines table in commit and Merge request views
* 3. Merge request widget
* 4. Commit widget
*/
import $ from 'jquery';
import { __ } from '../../locale';
import Flash from '../../flash';
import axios from '../../lib/utils/axios_utils';
import eventHub from '../event_hub';
import Icon from '../../vue_shared/components/icon.vue';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import JobComponent from './graph/job_component.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
LoadingIcon,
Icon,
JobComponent,
},
directives: {
tooltip,
},
props: {
stage: {
type: Object,
required: true,
},
directives: {
tooltip,
updateDropdown: {
type: Boolean,
required: false,
default: false,
},
props: {
stage: {
type: Object,
required: true,
},
updateDropdown: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isLoading: false,
dropdownContent: '',
};
},
computed: {
dropdownClass() {
return this.dropdownContent.length > 0
? 'js-builds-dropdown-container'
: 'js-builds-dropdown-loading';
},
data() {
return {
isLoading: false,
dropdownContent: '',
};
triggerButtonClass() {
return `ci-status-icon-${this.stage.status.group}`;
},
computed: {
dropdownClass() {
return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading';
},
borderlessIcon() {
return `${this.stage.status.icon}_borderless`;
},
},
triggerButtonClass() {
return `ci-status-icon-${this.stage.status.group}`;
},
watch: {
updateDropdown() {
if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) {
this.fetchJobs();
}
},
},
updated() {
if (this.dropdownContent.length > 0) {
this.stopDropdownClickPropagation();
}
},
methods: {
onClickStage() {
if (!this.isDropdownOpen()) {
eventHub.$emit('clickedDropdown');
this.isLoading = true;
this.fetchJobs();
}
},
borderlessIcon() {
return `${this.stage.status.icon}_borderless`;
},
fetchJobs() {
axios
.get(this.stage.dropdown_path)
.then(({ data }) => {
this.dropdownContent = data.latest_statuses;
this.isLoading = false;
})
.catch(() => {
this.closeDropdown();
this.isLoading = false;
Flash(__('Something went wrong on our end.'));
});
},
watch: {
updateDropdown() {
if (this.updateDropdown &&
this.isDropdownOpen() &&
!this.isLoading) {
this.fetchJobs();
}
},
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(
'.js-builds-dropdown-list button, .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item',
this.$el,
).on('click', e => {
e.stopPropagation();
});
},
updated() {
if (this.dropdownContent.length > 0) {
this.stopDropdownClickPropagation();
closeDropdown() {
if (this.isDropdownOpen()) {
$(this.$refs.dropdown).dropdown('toggle');
}
},
methods: {
onClickStage() {
if (!this.isDropdownOpen()) {
eventHub.$emit('clickedDropdown');
this.isLoading = true;
this.fetchJobs();
}
},
fetchJobs() {
axios.get(this.stage.dropdown_path)
.then(({ data }) => {
this.dropdownContent = data.html;
this.isLoading = false;
})
.catch(() => {
this.closeDropdown();
this.isLoading = false;
Flash('Something went wrong on our end.');
});
},
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item'))
.on('click', (e) => {
e.stopPropagation();
});
},
closeDropdown() {
if (this.isDropdownOpen()) {
$(this.$refs.dropdown).dropdown('toggle');
}
},
isDropdownOpen() {
return this.$el.classList.contains('open');
},
isDropdownOpen() {
return this.$el.classList.contains('open');
},
};
},
};
</script>
<template>
......@@ -168,7 +173,6 @@
>
<li
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu"
>
......@@ -176,8 +180,16 @@
<ul
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>
</li>
</ul>
......
......@@ -33,10 +33,10 @@ export default () => {
};
},
created() {
eventHub.$on('graphAction', this.postAction);
eventHub.$on('postAction', this.postAction);
},
beforeDestroy() {
eventHub.$off('graphAction', this.postAction);
eventHub.$off('postAction', this.postAction);
},
methods: {
postAction(action) {
......
......@@ -100,9 +100,10 @@ export default {
fetchSearchedProjects(searchQuery) {
this.searchQuery = searchQuery;
this.toggleLoader(true);
this.service.getSearchedProjects(this.searchQuery)
this.service
.getSearchedProjects(this.searchQuery)
.then(res => res.json())
.then((results) => {
.then(results => {
this.toggleSearchProjectsList(true);
this.store.setSearchedProjects(results);
})
......
......@@ -50,7 +50,7 @@ export default class ProjectsService {
} else {
// Check if project is already present in frequents list
// When found, update metadata of it.
storedFrequentProjects = JSON.parse(storedRawProjects).map((projectItem) => {
storedFrequentProjects = JSON.parse(storedRawProjects).map(projectItem => {
if (projectItem.id === project.id) {
matchFound = true;
const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS;
......@@ -104,13 +104,17 @@ export default class ProjectsService {
return [];
}
if (bp.getBreakpointSize() === 'sm' ||
bp.getBreakpointSize() === 'xs') {
if (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'xs') {
frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE;
}
const frequentProjects = storedFrequentProjects
.filter(project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY);
const frequentProjects = storedFrequentProjects.filter(
project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY,
);
if (!frequentProjects || frequentProjects.length === 0) {
return [];
}
// Sort all frequent projects in decending order of frequency
// and then by lastAccessedOn with recent most first
......
......@@ -85,6 +85,7 @@ export default class Shortcuts {
if ($modal.length) {
$modal.modal('toggle');
return null;
}
return axios.get(gon.shortcuts_path, {
......
......@@ -3,6 +3,7 @@
import tooltip from '~/vue_shared/directives/tooltip';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import { s__, __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
......@@ -16,6 +17,7 @@
mrWidgetAuthorTime,
loadingIcon,
statusIcon,
ClipboardButton,
},
props: {
mr: {
......@@ -162,6 +164,18 @@
<span class="label-branch">
<a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a>
</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 v-if="mr.sourceBranchRemoved">
{{ s__("mrWidget|The source branch has been removed") }}
......
......@@ -23,6 +23,7 @@ export default class MergeRequestStore {
this.sourceBranch = data.source_branch;
this.mergeStatus = data.merge_status;
this.commitMessage = data.merge_commit_message;
this.shortMergeCommitSha = data.short_merge_commit_sha;
this.commitMessageWithDescription = data.merge_commit_message_with_description;
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
......@@ -68,6 +69,7 @@ export default class MergeRequestStore {
this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
this.mergeCheckPath = data.merge_check_path;
this.mergeActionsContentPath = data.commit_change_content_path;
this.mergeCommitPath = data.merge_commit_path;
this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
this.isOpen = data.state === 'opened';
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
......
......@@ -65,6 +65,9 @@ export default {
spriteHref() {
return `${gon.sprite_icons}#${this.name}`;
},
iconTestClass() {
return `ic-${this.name}`;
},
iconSizeClass() {
return this.size ? `s${this.size}` : '';
},
......@@ -74,7 +77,7 @@ export default {
<template>
<svg
:class="[iconSizeClass, cssClasses]"
:class="[iconSizeClass, iconTestClass, cssClasses]"
:width="width"
:height="height"
:x="x"
......
<script>
import $ from 'jquery';
import $ from 'jquery';
/**
* 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.
*
* This component is used in apps that handle the API call.
* If you only need to change the URL this component should not be used.
*
* @example
* <navigation-tabs
* :tabs="[
* {
* name: String,
* scope: String,
* count: Number || Undefined,
* isActive: Boolean,
* },
* ]"
* @onChangeTab="onChangeTab"
* />
*/
export default {
name: 'NavigationTabs',
props: {
tabs: {
type: Array,
required: true,
},
scope: {
type: String,
required: false,
default: '',
},
/**
* 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.
*
* This component is used in apps that handle the API call.
* If you only need to change the URL this component should not be used.
*
* @example
* <navigation-tabs
* :tabs="[
* {
* name: String,
* scope: String,
* count: Number || Undefined || Null,
* isActive: Boolean,
* },
* ]"
* @onChangeTab="onChangeTab"
* />
*/
export default {
name: 'NavigationTabs',
props: {
tabs: {
type: Array,
required: true,
},
mounted() {
$(document).trigger('init.scrolling-tabs');
scope: {
type: String,
required: false,
default: '',
},
},
mounted() {
$(document).trigger('init.scrolling-tabs');
},
methods: {
shouldRenderBadge(count) {
// 0 is valid in a badge, but evaluates to false, we need to check for undefined or null
return !(count === undefined || count === null);
},
methods: {
shouldRenderBadge(count) {
// 0 is valid in a badge, but evaluates to false, we need to check for undefined
return count !== undefined;
},
onTabClick(tab) {
this.$emit('onChangeTab', tab.scope);
},
onTabClick(tab) {
this.$emit('onChangeTab', tab.scope);
},
};
},
};
</script>
<template>
<ul class="nav-links scrolling-tabs separator">
......
......@@ -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 {
color: $color-700;
}
......@@ -203,6 +184,13 @@
.ide-file-list .file.file-active {
color: $color-700;
}
.ide-sidebar-link {
&.active {
color: $color-700;
box-shadow: inset 3px 0 $color-700;
}
}
}
body {
......@@ -343,9 +331,5 @@ body {
.sidebar-top-level-items > li.active .badge {
color: $theme-gray-900;
}
.ide-sidebar-link {
color: $white-light;
}
}
}
......@@ -231,6 +231,7 @@ $row-hover: $blue-50;
$row-hover-border: $blue-200;
$progress-color: #c0392b;
$header-height: 40px;
$ide-statusbar-height: 27px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$limited-layout-width-sm: 790px;
......@@ -268,6 +269,7 @@ $system-footer-height: $system-header-height;
$flash-height: 52px;
$context-header-height: 60px;
$breadcrumb-min-height: 48px;
$gcp-signup-offer-icon-max-width: 125px;
$issue-box-upcoming-bg: #8f8f8f;
$pages-group-name-color: #4c4e54;
......@@ -342,11 +344,10 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
/*
* Fonts
*/
$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas',
'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
$regular_font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
'Courier New', 'andale mono', 'lucida console', monospace;
$regular_font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
/*
* Dropdowns
......@@ -474,11 +475,9 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
*/
$issue-boards-filter-height: 68px;
$issue-boards-breadcrumbs-height-xs: 63px;
$issue-board-list-difference-xs: $header-height +
$issue-boards-breadcrumbs-height-xs;
$issue-board-list-difference-xs: $header-height + $issue-boards-breadcrumbs-height-xs;
$issue-board-list-difference-sm: $header-height + $breadcrumb-min-height;
$issue-board-list-difference-md: $issue-board-list-difference-sm +
$issue-boards-filter-height;
$issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-boards-filter-height;
/*
* Avatar
......@@ -699,6 +698,8 @@ $stage-hover-bg: $gray-darker;
$ci-action-icon-size: 22px;
$pipeline-dropdown-line-height: 20px;
$pipeline-dropdown-status-icon-size: 18px;
$ci-action-dropdown-button-size: 24px;
$ci-action-dropdown-svg-size: 12px;
/*
CI variable lists
......
......@@ -26,3 +26,51 @@
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 @@
background: none;
border: 0;
padding: 0;
margin-top: 10px;
word-break: normal;
white-space: pre-wrap;
}
......@@ -21,10 +20,6 @@
margin: 0;
color: $gl-text-color;
}
.commit-description {
margin-top: 15px;
}
}
.commit-hash-full {
......@@ -178,7 +173,7 @@
.commit-detail {
display: flex;
justify-content: space-between;
align-items: center;
align-items: start;
flex-grow: 1;
.project_namespace {
......@@ -272,20 +267,16 @@
.commit-row-description {
font-size: 14px;
padding: 10px 15px;
margin: 10px 0;
background: $gray-light;
padding: 0 0 0 $gl-padding-8;
border: 0;
display: none;
white-space: pre-wrap;
word-break: normal;
pre {
border: 0;
background: inherit;
padding: 0;
margin: 0;
white-space: pre-wrap;
}
color: $gl-text-color-secondary;
background: none;
font-family: inherit;
border-left: 2px solid $theme-gray-300;
border-radius: unset;
a {
color: $gl-text-color;
......
......@@ -438,28 +438,59 @@
}
&.popover {
padding: 0;
border: 1px solid $border-color;
&.left {
left: auto;
right: 0;
margin-right: 10px;
> .arrow {
right: -16px;
border-left-color: $border-color;
}
> .arrow::after {
border-left-color: $theme-gray-50;
}
}
&.right {
left: 0;
right: auto;
margin-left: 10px;
> .arrow {
left: -16px;
border-right-color: $border-color;
}
> .arrow::after {
border-right-color: $theme-gray-50;
}
}
> .arrow {
top: 40px;
top: 16px;
margin-top: -8px;
border-width: 8px;
}
> .popover-title,
> .popover-content {
padding: 5px 8px;
padding: 8px;
font-size: 12px;
white-space: nowrap;
}
> .popover-title {
background-color: $theme-gray-50;
}
}
strong {
font-weight: 600;
}
}
......@@ -472,7 +503,7 @@
vertical-align: middle;
+ td {
padding-left: 5px;
padding-left: 8px;
vertical-align: top;
}
}
......
......@@ -159,10 +159,6 @@
.dropdown-menu {
z-index: 300;
}
.ci-action-icon-wrapper {
line-height: 16px;
}
}
.mini-pipeline-graph-dropdown-toggle {
......
......@@ -49,7 +49,6 @@
}
.ci-table {
.label {
margin-bottom: 3px;
}
......@@ -150,7 +149,6 @@
}
.branch-commit {
.ref-name {
font-weight: $gl-font-weight-bold;
max-width: 100px;
......@@ -510,43 +508,6 @@
@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 {
height: 20px;
width: 20px;
......@@ -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
......@@ -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,
.mini-pipeline-graph-dropdown-menu {
width: 240px;
max-width: 240px;
.scrollable-menu {
// override dropdown.scss
&.dropdown-menu li button,
&.dropdown-menu li a.ci-action-icon-container {
padding: 0;
max-height: 245px;
overflow: auto;
text-align: center;
}
li {
position: relative;
.ci-action-icon-container {
position: absolute;
right: 8px;
top: 8px;
// 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;
}
&.ci-action-icon-wrapper {
height: $ci-action-dropdown-button-size;
width: $ci-action-dropdown-button-size;
// Action icon on the right
a.ci-action-icon-wrapper {
border-radius: 50%;
background: $white-light;
border: 1px solid $border-color;
width: $ci-action-icon-size;
height: $ci-action-icon-size;
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};
border-radius: 50%;
display: block;
&:hover,
&:focus {
&:hover {
background-color: $stage-hover-bg;
border: 1px solid $dropdown-toggle-active-border-color;
svg {
fill: $gl-text-color;
}
}
svg {
width: $ci-action-dropdown-svg-size;
height: $ci-action-dropdown-svg-size;
fill: $gl-text-color-secondary;
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 {
width: 16px;
height: 16px;
top: 0;
left: -3px;
}
top: 0;
vertical-align: initial;
}
}
}
&:hover svg,
&:focus svg {
fill: $gl-text-color;
}
// SVGs in the commit widget and mr widget
a.ci-action-icon-container.ci-action-icon-wrapper svg {
top: 2px;
}
&.icon-action-retry,
&.icon-action-play {
svg {
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
......@@ -838,6 +820,11 @@ a.linked-pipeline-mini-item {
line-height: $line-height-base;
white-space: nowrap;
// Match dropdown.scss for all `a` tags
&.non-details-job-component {
padding: 8px 16px;
}
.ci-job-name-component {
align-items: center;
display: flex;
......@@ -969,7 +956,7 @@ a.linked-pipeline-mini-item {
&.dropdown-menu {
transform: translate(-80%, 0);
@media(min-width: $screen-md-min) {
@media (min-width: $screen-md-min) {
transform: translate(-50%, 0);
right: auto;
left: 50%;
......
......@@ -354,30 +354,48 @@
min-width: 200px;
}
.deploy-key-content {
@media (min-width: $screen-sm-min) {
float: left;
.deploy-keys {
.scrolling-tabs-container {
position: relative;
}
}
&:last-child {
float: right;
.deploy-key {
// 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 {
@media (min-width: $screen-sm-min) {
line-height: 42px;
.btn svg {
vertical-align: top;
}
.key-created-at {
line-height: unset;
}
}
a.deploy-project-label {
padding: 5px;
margin-right: 5px;
color: $gl-text-color;
background-color: $row-hover;
.deploy-project-list {
margin-bottom: -$gl-padding-4;
&:hover {
color: $gl-link-color;
a.deploy-project-label {
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 {
color: $gl-link-color;
}
}
}
......
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
......@@ -106,9 +106,18 @@ class Projects::PipelinesController < Projects::ApplicationController
@stage = pipeline.legacy_stage(params[:stage])
return not_found unless @stage
respond_to do |format|
format.json { render json: { html: view_to_html_string('projects/pipelines/_stage') } }
end
render json: StageSerializer
.new(project: @project, current_user: @current_user)
.represent(@stage, details: true)
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
def retry
......
......@@ -8,7 +8,7 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
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)
if runner_project.persisted?
......@@ -22,6 +22,6 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
runner_project = project.runner_projects.find(params[:id])
runner_project.destroy
redirect_to runners_path(project), status: 302
redirect_to project_runners_path(project), status: 302
end
end
class Projects::RunnersController < Projects::ApplicationController
before_action :authorize_admin_build!
before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
layout 'project_settings'
......@@ -13,7 +13,7 @@ class Projects::RunnersController < Projects::ApplicationController
def update
if Ci::UpdateRunnerService.new(@runner).update(runner_params)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
redirect_to project_runner_path(@project, @runner), notice: 'Runner was successfully updated.'
else
render 'edit'
end
......@@ -24,26 +24,27 @@ class Projects::RunnersController < Projects::ApplicationController
@runner.destroy
end
redirect_to runners_path(@project), status: 302
redirect_to project_runners_path(@project), status: 302
end
def resume
if Ci::UpdateRunnerService.new(@runner).update(active: true)
redirect_to runners_path(@project), notice: 'Runner was successfully updated.'
redirect_to project_runners_path(@project), notice: 'Runner was successfully updated.'
else
redirect_to runners_path(@project), alert: 'Runner was not updated.'
redirect_to project_runners_path(@project), alert: 'Runner was not updated.'
end
end
def pause
if Ci::UpdateRunnerService.new(@runner).update(active: false)
redirect_to runners_path(@project), notice: 'Runner was successfully updated.'
redirect_to project_runners_path(@project), notice: 'Runner was successfully updated.'
else
redirect_to runners_path(@project), alert: 'Runner was not updated.'
redirect_to project_runners_path(@project), alert: 'Runner was not updated.'
end
end
def show
render 'shared/runners/show'
end
def toggle_shared_runners
......@@ -60,7 +61,7 @@ class Projects::RunnersController < Projects::ApplicationController
protected
def set_runner
def runner
@runner ||= project.runners.find(params[:id])
end
......
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.
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