Commit 4e9a4011 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-05-08

# Conflicts:
#	.flayignore
#	app/assets/javascripts/main.js
#	app/controllers/projects/mirrors_controller.rb
#	app/controllers/projects/settings/repository_controller.rb
#	app/models/project.rb
#	app/models/remote_mirror.rb
#	app/serializers/project_mirror_entity.rb
#	app/services/concerns/exclusive_lease_guard.rb
#	app/views/admin/application_settings/_repository_mirrors_form.html.haml
#	app/views/admin/application_settings/show.html.haml
#	app/views/projects/settings/repository/show.html.haml
#	app/workers/all_queues.yml
#	config/routes/group.rb
#	config/routes/project.rb
#	config/sidekiq_queues.yml
#	config/webpack.config.js
#	db/migrate/20180503131624_create_remote_mirrors.rb
#	db/migrate/20180503141722_add_remote_mirror_available_overridden_to_projects.rb
#	db/migrate/20180503193542_add_indexes_to_remote_mirror.rb
#	db/migrate/20180503193953_add_mirror_available_to_application_settings.rb
#	db/schema.rb
#	doc/workflow/repository_mirroring.md
#	lib/gitlab/ci/pipeline/chain/command.rb
#	lib/gitlab/import_export/import_export.yml
#	lib/gitlab/usage_data.rb
#	package.json
#	spec/factories/projects.rb
#	spec/javascripts/vue_mr_widget/mock_data.js
#	spec/lib/gitlab/import_export/all_models.yml
#	spec/lib/gitlab/usage_data_spec.rb
#	spec/models/project_spec.rb
#	spec/models/remote_mirror_spec.rb
#	spec/models/repository_spec.rb
#	spec/services/ci/retry_build_service_spec.rb
#	spec/spec_helper.rb

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