summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/images/favicon-yellow.pngbin1667 -> 1481 bytes
-rw-r--r--app/assets/javascripts/batch_comments/mixins/resolved_status.js6
-rw-r--r--app/assets/javascripts/boards/stores/actions.js4
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js3
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js48
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js6
-rw-r--r--app/assets/javascripts/dirty_submit/dirty_submit_form.js11
-rw-r--r--app/assets/javascripts/dropzone_input.js15
-rw-r--r--app/assets/javascripts/error_tracking_settings/index.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js14
-rw-r--r--app/assets/javascripts/gl_dropdown.js8
-rw-r--r--app/assets/javascripts/ide/components/ide.vue40
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue19
-rw-r--r--app/assets/javascripts/ide/stores/actions.js52
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js118
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js18
-rw-r--r--app/assets/javascripts/ide/stores/getters.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js11
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutations/branch.js6
-rw-r--r--app/assets/javascripts/ide/stores/mutations/project.js5
-rw-r--r--app/assets/javascripts/import_projects/store/getters.js6
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue12
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue3
-rw-r--r--app/assets/javascripts/jobs/store/actions.js26
-rw-r--r--app/assets/javascripts/jobs/store/mutation_types.js4
-rw-r--r--app/assets/javascripts/jobs/store/mutations.js19
-rw-r--r--app/assets/javascripts/jobs/store/state.js1
-rw-r--r--app/assets/javascripts/label_manager.js3
-rw-r--r--app/assets/javascripts/labels_select.js16
-rw-r--r--app/assets/javascripts/lib/graphql.js8
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js35
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js9
-rw-r--r--app/assets/javascripts/locale/index.js2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/area.vue16
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue74
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js2
-rw-r--r--app/assets/javascripts/monitoring/services/monitoring_service.js75
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js117
-rw-r--r--app/assets/javascripts/monitoring/stores/index.js21
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js15
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js45
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js12
-rw-r--r--app/assets/javascripts/monitoring/stores/utils.js (renamed from app/assets/javascripts/monitoring/stores/monitoring_store.js)44
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue9
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue6
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue8
-rw-r--r--app/assets/javascripts/pdf/index.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue4
-rw-r--r--app/assets/javascripts/project_label_subscription.js4
-rw-r--r--app/assets/javascripts/project_select.js5
-rw-r--r--app/assets/javascripts/reports/components/report_section.vue2
-rw-r--r--app/assets/javascripts/repository/components/breadcrumbs.vue61
-rw-r--r--app/assets/javascripts/repository/components/table/header.vue9
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue144
-rw-r--r--app/assets/javascripts/repository/components/table/parent_row.vue37
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue72
-rw-r--r--app/assets/javascripts/repository/fragmentTypes.json1
-rw-r--r--app/assets/javascripts/repository/graphql.js34
-rw-r--r--app/assets/javascripts/repository/index.js38
-rw-r--r--app/assets/javascripts/repository/mixins/get_ref.js14
-rw-r--r--app/assets/javascripts/repository/pages/index.vue14
-rw-r--r--app/assets/javascripts/repository/pages/tree.vue7
-rw-r--r--app/assets/javascripts/repository/queries/getFiles.graphql55
-rw-r--r--app/assets/javascripts/repository/queries/getProjectPath.graphql3
-rw-r--r--app/assets/javascripts/repository/queries/getProjectShortPath.graphql3
-rw-r--r--app/assets/javascripts/repository/router.js19
-rw-r--r--app/assets/javascripts/repository/utils/icon.js99
-rw-r--r--app/assets/javascripts/repository/utils/title.js9
-rw-r--r--app/assets/javascripts/right_sidebar.js7
-rw-r--r--app/assets/javascripts/search_autocomplete.js5
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue6
-rw-r--r--app/assets/javascripts/star.js2
-rw-r--r--app/assets/javascripts/subscription_select.js3
-rw-r--r--app/assets/javascripts/usage_ping_consent.js3
-rw-r--r--app/assets/javascripts/users_select.js38
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue66
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue10
-rw-r--r--app/assets/stylesheets/components/avatar.scss232
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/avatar.scss194
-rw-r--r--app/assets/stylesheets/framework/awards.scss5
-rw-r--r--app/assets/stylesheets/framework/buttons.scss31
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss4
-rw-r--r--app/assets/stylesheets/framework/files.scss1
-rw-r--r--app/assets/stylesheets/framework/filters.scss2
-rw-r--r--app/assets/stylesheets/framework/forms.scss4
-rw-r--r--app/assets/stylesheets/framework/highlight.scss3
-rw-r--r--app/assets/stylesheets/framework/mixins.scss13
-rw-r--r--app/assets/stylesheets/framework/modal.scss6
-rw-r--r--app/assets/stylesheets/framework/timeline.scss1
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/framework/variables_overrides.scss1
-rw-r--r--app/assets/stylesheets/pages/clusters.scss2
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss1
-rw-r--r--app/assets/stylesheets/pages/diff.scss6
-rw-r--r--app/assets/stylesheets/pages/events.scss4
-rw-r--r--app/assets/stylesheets/pages/login.scss13
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss14
-rw-r--r--app/assets/stylesheets/pages/notes.scss3
-rw-r--r--app/assets/stylesheets/pages/search.scss10
105 files changed, 1612 insertions, 703 deletions
diff --git a/app/assets/images/favicon-yellow.png b/app/assets/images/favicon-yellow.png
index 2d5289818b4..a80827808fc 100644
--- a/app/assets/images/favicon-yellow.png
+++ b/app/assets/images/favicon-yellow.png
Binary files differ
diff --git a/app/assets/javascripts/batch_comments/mixins/resolved_status.js b/app/assets/javascripts/batch_comments/mixins/resolved_status.js
index 20c31d9f8a4..3bbbaa86b51 100644
--- a/app/assets/javascripts/batch_comments/mixins/resolved_status.js
+++ b/app/assets/javascripts/batch_comments/mixins/resolved_status.js
@@ -1,10 +1,12 @@
+import { sprintf, __ } from '~/locale';
+
export default {
computed: {
resolveButtonTitle() {
- let title = 'Mark as resolved';
+ let title = __('Mark comment as resolved');
if (this.resolvedBy) {
- title = `Resolved by ${this.resolvedBy.name}`;
+ title = sprintf(__('Resolved by %{name}'), { name: this.resolvedBy.name });
}
return title;
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index da82b52330a..51565c597e6 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,5 +1,7 @@
+import { __ } from '~/locale';
+
const notImplemented = () => {
- throw new Error('Not implemented!');
+ throw new Error(__('Not implemented!'));
};
export default {
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 77ba68be07e..8e61b93e824 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -1,7 +1,8 @@
import * as mutationTypes from './mutation_types';
+import { __ } from '~/locale';
const notImplemented = () => {
- throw new Error('Not implemented!');
+ throw new Error(__('Not implemented!'));
};
export default {
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 561b6bdd9f1..70af333a0dd 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -1,5 +1,6 @@
import Visibility from 'visibilityjs';
import Vue from 'vue';
+import AccessorUtilities from '~/lib/utils/accessor';
import { GlToast } from '@gitlab/ui';
import PersistentUserCallout from '../persistent_user_callout';
import { s__, sprintf } from '../locale';
@@ -43,8 +44,10 @@ export default class Clusters {
helpPath,
ingressHelpPath,
ingressDnsHelpPath,
+ clusterId,
} = document.querySelector('.js-edit-cluster-form').dataset;
+ this.clusterId = clusterId;
this.store = new ClustersStore();
this.store.setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath);
this.store.setManagePrometheusPath(managePrometheusPath);
@@ -69,6 +72,10 @@ export default class Clusters {
this.errorContainer = document.querySelector('.js-cluster-error');
this.successContainer = document.querySelector('.js-cluster-success');
this.creatingContainer = document.querySelector('.js-cluster-creating');
+ this.unreachableContainer = document.querySelector('.js-cluster-api-unreachable');
+ this.authenticationFailureContainer = document.querySelector(
+ '.js-cluster-authentication-failure',
+ );
this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
this.successApplicationContainer = document.querySelector('.js-cluster-application-notice');
this.showTokenButton = document.querySelector('.js-show-cluster-token');
@@ -125,6 +132,13 @@ export default class Clusters {
PersistentUserCallout.factory(callout);
}
+ addBannerCloseHandler(el, status) {
+ el.querySelector('.js-close-banner').addEventListener('click', () => {
+ el.classList.add('hidden');
+ this.setBannerDismissedState(status, true);
+ });
+ }
+
addListeners() {
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication);
@@ -133,6 +147,9 @@ export default class Clusters {
eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data));
eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data));
eventHub.$on('uninstallApplication', data => this.uninstallApplication(data));
+ // Add event listener to all the banner close buttons
+ this.addBannerCloseHandler(this.unreachableContainer, 'unreachable');
+ this.addBannerCloseHandler(this.authenticationFailureContainer, 'authentication_failure');
}
removeListeners() {
@@ -205,6 +222,8 @@ export default class Clusters {
this.errorContainer.classList.add('hidden');
this.successContainer.classList.add('hidden');
this.creatingContainer.classList.add('hidden');
+ this.unreachableContainer.classList.add('hidden');
+ this.authenticationFailureContainer.classList.add('hidden');
}
checkForNewInstalls(prevApplicationMap, newApplicationMap) {
@@ -228,9 +247,32 @@ export default class Clusters {
}
}
+ setBannerDismissedState(status, isDismissed) {
+ if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ window.localStorage.setItem(
+ `cluster_${this.clusterId}_banner_dismissed`,
+ `${status}_${isDismissed}`,
+ );
+ }
+ }
+
+ isBannerDismissed(status) {
+ let bannerState;
+ if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ bannerState = window.localStorage.getItem(`cluster_${this.clusterId}_banner_dismissed`);
+ }
+
+ return bannerState === `${status}_true`;
+ }
+
updateContainer(prevStatus, status, error) {
this.hideAll();
+ if (this.isBannerDismissed(status)) {
+ return;
+ }
+ this.setBannerDismissedState(status, false);
+
// We poll all the time but only want the `created` banner to show when newly created
if (this.store.state.status !== 'created' || prevStatus !== this.store.state.status) {
switch (status) {
@@ -241,6 +283,12 @@ export default class Clusters {
this.errorContainer.classList.remove('hidden');
this.errorReasonContainer.textContent = error;
break;
+ case 'unreachable':
+ this.unreachableContainer.classList.remove('hidden');
+ break;
+ case 'authentication_failure':
+ this.authenticationFailureContainer.classList.remove('hidden');
+ break;
case 'scheduled':
case 'creating':
this.creatingContainer.classList.remove('hidden');
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index a0ca44caa07..9216d4ab372 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -6,6 +6,7 @@ import 'core-js/fn/array/from';
import 'core-js/fn/array/includes';
import 'core-js/fn/object/assign';
import 'core-js/fn/object/values';
+import 'core-js/fn/object/entries';
import 'core-js/fn/promise';
import 'core-js/fn/promise/finally';
import 'core-js/fn/string/code-point-at';
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index 02aa507ba03..8f5cece0788 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -118,7 +118,7 @@ export default class CreateMergeRequestDropdown {
this.branchCreated = true;
window.location.href = data.url;
})
- .catch(() => Flash('Failed to create a branch for this issue. Please try again.'));
+ .catch(() => Flash(__('Failed to create a branch for this issue. Please try again.')));
}
createMergeRequest() {
@@ -130,7 +130,7 @@ export default class CreateMergeRequestDropdown {
this.mergeRequestCreated = true;
window.location.href = data.url;
})
- .catch(() => Flash('Failed to create Merge Request. Please try again.'));
+ .catch(() => Flash(__('Failed to create Merge Request. Please try again.')));
}
disable() {
@@ -227,7 +227,7 @@ export default class CreateMergeRequestDropdown {
.catch(() => {
this.unavailable();
this.disable();
- new Flash('Failed to get ref.');
+ new Flash(__('Failed to get ref.'));
this.isGettingRef = false;
diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_form.js b/app/assets/javascripts/dirty_submit/dirty_submit_form.js
index 765969daa32..0fcaec9531c 100644
--- a/app/assets/javascripts/dirty_submit/dirty_submit_form.js
+++ b/app/assets/javascripts/dirty_submit/dirty_submit_form.js
@@ -21,10 +21,15 @@ class DirtySubmitForm {
}
registerListeners() {
- const throttledUpdateDirtyInput = _.throttle(
- event => this.updateDirtyInput(event),
- DirtySubmitForm.THROTTLE_DURATION,
+ const getThrottledHandlerForInput = _.memoize(() =>
+ _.throttle(event => this.updateDirtyInput(event), DirtySubmitForm.THROTTLE_DURATION),
);
+
+ const throttledUpdateDirtyInput = event => {
+ const throttledHandler = getThrottledHandlerForInput(event.target.name);
+ throttledHandler(event);
+ };
+
this.form.addEventListener('input', throttledUpdateDirtyInput);
this.form.addEventListener('change', throttledUpdateDirtyInput);
$(this.form).on('change.select2', throttledUpdateDirtyInput);
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 9987fbcb6a7..0ff26445a6a 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -4,6 +4,7 @@ import _ from 'underscore';
import './behaviors/preview_markdown';
import csrf from './lib/utils/csrf';
import axios from './lib/utils/axios_utils';
+import { n__, __ } from '~/locale';
Dropzone.autoDiscover = false;
@@ -90,7 +91,7 @@ export default function dropzoneInput(form) {
if (!processingFileCount) $attachButton.removeClass('hide');
addFileToForm(response.link.url);
},
- error: (file, errorMessage = 'Attaching the file failed.', xhr) => {
+ error: (file, errorMessage = __('Attaching the file failed.'), xhr) => {
// If 'error' event is fired by dropzone, the second parameter is error message.
// If the 'errorMessage' parameter is empty, the default error message is set.
// If the 'error' event is fired by backend (xhr) error response, the third parameter is
@@ -273,19 +274,11 @@ export default function dropzoneInput(form) {
};
updateAttachingMessage = (files, messageContainer) => {
- let attachingMessage;
const filesCount = files.filter(file => file.status === 'uploading' || file.status === 'queued')
.length;
+ const attachingMessage = n__('Attaching a file', 'Attaching %d files', filesCount);
- // Dinamycally change uploading files text depending on files number in
- // dropzone files queue.
- if (filesCount > 1) {
- attachingMessage = `Attaching ${filesCount} files -`;
- } else {
- attachingMessage = 'Attaching a file -';
- }
-
- messageContainer.text(attachingMessage);
+ messageContainer.text(`${attachingMessage} -`);
};
form.find('.markdown-selector').click(function onMarkdownClick(e) {
diff --git a/app/assets/javascripts/error_tracking_settings/index.js b/app/assets/javascripts/error_tracking_settings/index.js
index ce315963723..e39452353f5 100644
--- a/app/assets/javascripts/error_tracking_settings/index.js
+++ b/app/assets/javascripts/error_tracking_settings/index.js
@@ -1,8 +1,10 @@
import Vue from 'vue';
import ErrorTrackingSettings from './components/app.vue';
import createStore from './store';
+import initSettingsPanels from '~/settings_panels';
export default () => {
+ initSettingsPanels();
const formContainerEl = document.querySelector('.js-error-tracking-form');
const {
dataset: { apiHost, enabled, project, token, listProjectsEndpoint, operationsSettingsEndpoint },
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index f1e7be6bde1..a65c0012b4d 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -18,6 +18,7 @@ export default class DropdownUser extends DropdownAjaxFilter {
group_id: this.getGroupId(),
project_id: this.getProjectId(),
current_user: true,
+ ...this.projectOrGroupId(),
},
onLoadingFinished: () => {
this.hideCurrentUser();
@@ -36,4 +37,17 @@ export default class DropdownUser extends DropdownAjaxFilter {
getProjectId() {
return this.input.getAttribute('data-project-id');
}
+
+ projectOrGroupId() {
+ const projectId = this.getProjectId();
+ const groupId = this.getGroupId();
+ if (groupId) {
+ return {
+ group_id: groupId,
+ };
+ }
+ return {
+ project_id: projectId,
+ };
+ }
}
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 1c6b18c0e03..a143d79097b 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -335,6 +335,10 @@ GitLabDropdown = (function() {
_this.fullData = data;
_this.parseData(_this.fullData);
_this.focusTextInput();
+
+ // Update dropdown position since remote data may have changed dropdown size
+ _this.dropdown.find('.dropdown-menu-toggle').dropdown('update');
+
if (
_this.options.filterable &&
_this.filter &&
@@ -702,6 +706,10 @@ GitLabDropdown = (function() {
}
html = document.createElement('li');
+ if (rowHidden) {
+ html.style.display = 'none';
+ }
+
if (data === 'divider' || data === 'separator') {
html.className = data;
return html;
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 9894ebb0624..e41b1530226 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,6 +1,7 @@
<script>
import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex';
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import FindFile from '~/vue_shared/components/file_finder/index.vue';
import NewModal from './new_dropdown/modal.vue';
@@ -22,6 +23,8 @@ export default {
FindFile,
ErrorMessage,
CommitEditorHeader,
+ GlButton,
+ GlLoadingIcon,
},
props: {
rightPaneComponent: {
@@ -47,13 +50,15 @@ export default {
'someUncommittedChanges',
'isCommitModeActive',
'allBlobs',
+ 'emptyRepo',
+ 'currentTree',
]),
},
mounted() {
window.onbeforeunload = e => this.onBeforeUnload(e);
},
methods: {
- ...mapActions(['toggleFileFinder']),
+ ...mapActions(['toggleFileFinder', 'openNewEntryModal']),
onBeforeUnload(e = {}) {
const returnValue = __('Are you sure you want to lose unsaved changes?');
@@ -98,17 +103,40 @@ export default {
<repo-editor :file="activeFile" class="multi-file-edit-pane-content" />
</template>
<template v-else>
- <div v-once class="ide-empty-state">
+ <div class="ide-empty-state">
<div class="row js-empty-state">
<div class="col-12">
<div class="svg-content svg-250"><img :src="emptyStateSvgPath" /></div>
</div>
<div class="col-12">
<div class="text-content text-center">
- <h4>Welcome to the GitLab IDE</h4>
- <p>
- Select a file from the left sidebar to begin editing. Afterwards, you'll be able
- to commit your changes.
+ <h4>
+ {{ __('Make and review changes in the browser with the Web IDE') }}
+ </h4>
+ <template v-if="emptyRepo">
+ <p>
+ {{
+ __(
+ "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes.",
+ )
+ }}
+ </p>
+ <gl-button
+ variant="success"
+ :title="__('New file')"
+ :aria-label="__('New file')"
+ @click="openNewEntryModal({ type: 'blob' })"
+ >
+ {{ __('New file') }}
+ </gl-button>
+ </template>
+ <gl-loading-icon v-else-if="!currentTree || currentTree.loading" size="md" />
+ <p v-else>
+ {{
+ __(
+ "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes.",
+ )
+ }}
</p>
</div>
</div>
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index 81374f26645..95782b2c88a 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -54,14 +54,17 @@ export default {
<slot name="header"></slot>
</header>
<div class="ide-tree-body h-100">
- <file-row
- v-for="file in currentTree.tree"
- :key="file.key"
- :file="file"
- :level="0"
- :extra-component="$options.FileRowExtra"
- @toggleTreeOpen="toggleTreeOpen"
- />
+ <template v-if="currentTree.tree.length">
+ <file-row
+ v-for="file in currentTree.tree"
+ :key="file.key"
+ :file="file"
+ :level="0"
+ :extra-component="$options.FileRowExtra"
+ @toggleTreeOpen="toggleTreeOpen"
+ />
+ </template>
+ <div v-else class="file-row">{{ __('No files') }}</div>
</div>
</template>
</div>
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index fd678e6e10c..dc8ca732879 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -1,12 +1,15 @@
import $ from 'jquery';
import Vue from 'vue';
+import { __, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
+import _ from 'underscore';
import * as types from './mutation_types';
import { decorateFiles } from '../lib/files';
import { stageKeys } from '../constants';
+import service from '../services';
-export const redirectToUrl = (_, url) => visitUrl(url);
+export const redirectToUrl = (self, url) => visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
@@ -239,6 +242,53 @@ export const renameEntry = (
}
};
+export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) =>
+ new Promise((resolve, reject) => {
+ const currentProject = state.projects[projectId];
+ if (!currentProject || !currentProject.branches[branchId] || force) {
+ service
+ .getBranchData(projectId, branchId)
+ .then(({ data }) => {
+ const { id } = data.commit;
+ commit(types.SET_BRANCH, {
+ projectPath: projectId,
+ branchName: branchId,
+ branch: data,
+ });
+ commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
+ resolve(data);
+ })
+ .catch(e => {
+ if (e.response.status === 404) {
+ reject(e);
+ } else {
+ flash(
+ __('Error loading branch data. Please try again.'),
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+
+ reject(
+ new Error(
+ sprintf(
+ __('Branch not loaded - %{branchId}'),
+ {
+ branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`,
+ },
+ false,
+ ),
+ ),
+ );
+ }
+ });
+ } else {
+ resolve(currentProject.branches[branchId]);
+ }
+ });
+
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 4b10d148ebf..dd8f17e4f3a 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -35,48 +35,6 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force
}
});
-export const getBranchData = (
- { commit, dispatch, state },
- { projectId, branchId, force = false } = {},
-) =>
- new Promise((resolve, reject) => {
- if (
- typeof state.projects[`${projectId}`] === 'undefined' ||
- !state.projects[`${projectId}`].branches[branchId] ||
- force
- ) {
- service
- .getBranchData(`${projectId}`, branchId)
- .then(({ data }) => {
- const { id } = data.commit;
- commit(types.SET_BRANCH, {
- projectPath: `${projectId}`,
- branchName: branchId,
- branch: data,
- });
- commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
- resolve(data);
- })
- .catch(e => {
- if (e.response.status === 404) {
- dispatch('showBranchNotFoundError', branchId);
- } else {
- flash(
- __('Error loading branch data. Please try again.'),
- 'alert',
- document,
- null,
- false,
- true,
- );
- }
- reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
- });
- } else {
- resolve(state.projects[`${projectId}`].branches[branchId]);
- }
- });
-
export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) =>
service
.getBranchData(projectId, branchId)
@@ -125,40 +83,66 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => {
});
};
-export const openBranch = ({ dispatch, state }, { projectId, branchId, basePath }) => {
- dispatch('setCurrentBranchId', branchId);
-
- dispatch('getBranchData', {
- projectId,
- branchId,
+export const showEmptyState = ({ commit, state }, { projectId, branchId }) => {
+ const treePath = `${projectId}/${branchId}`;
+ commit(types.CREATE_TREE, { treePath });
+ commit(types.TOGGLE_LOADING, {
+ entry: state.trees[treePath],
+ forceValue: false,
});
+};
- return dispatch('getFiles', {
+export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, basePath }) => {
+ dispatch('setCurrentBranchId', branchId);
+
+ if (getters.emptyRepo) {
+ return dispatch('showEmptyState', { projectId, branchId });
+ }
+ return dispatch('getBranchData', {
projectId,
branchId,
})
.then(() => {
- if (basePath) {
- const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
- const treeEntryKey = Object.keys(state.entries).find(
- key => key === path && !state.entries[key].pending,
- );
- const treeEntry = state.entries[treeEntryKey];
-
- if (treeEntry) {
- dispatch('handleTreeEntryAction', treeEntry);
- } else {
- dispatch('createTempEntry', {
- name: path,
- type: 'blob',
- });
- }
- }
- })
- .then(() => {
dispatch('getMergeRequestsForBranch', {
projectId,
branchId,
});
+ dispatch('getFiles', {
+ projectId,
+ branchId,
+ })
+ .then(() => {
+ if (basePath) {
+ const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
+ const treeEntryKey = Object.keys(state.entries).find(
+ key => key === path && !state.entries[key].pending,
+ );
+ const treeEntry = state.entries[treeEntryKey];
+
+ if (treeEntry) {
+ dispatch('handleTreeEntryAction', treeEntry);
+ } else {
+ dispatch('createTempEntry', {
+ name: path,
+ type: 'blob',
+ });
+ }
+ }
+ })
+ .catch(
+ () =>
+ new Error(
+ sprintf(
+ __('An error occurred whilst getting files for - %{branchId}'),
+ {
+ branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`,
+ },
+ false,
+ ),
+ ),
+ );
+ })
+ .catch(() => {
+ dispatch('showBranchNotFoundError', branchId);
});
};
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 3d83e4a9ba5..75511574d3e 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -74,17 +74,13 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } =
resolve();
})
.catch(e => {
- if (e.response.status === 404) {
- dispatch('showBranchNotFoundError', branchId);
- } else {
- dispatch('setErrorMessage', {
- text: __('An error occurred whilst loading all the files.'),
- action: payload =>
- dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)),
- actionText: __('Please try again'),
- actionPayload: { projectId, branchId },
- });
- }
+ dispatch('setErrorMessage', {
+ text: __('An error occurred whilst loading all the files.'),
+ action: payload =>
+ dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)),
+ actionText: __('Please try again'),
+ actionPayload: { projectId, branchId },
+ });
reject(e);
});
} else {
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 490658a4543..7a88ac5b116 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -36,6 +36,9 @@ export const currentMergeRequest = state => {
export const currentProject = state => state.projects[state.currentProjectId];
+export const emptyRepo = state =>
+ state.projects[state.currentProjectId] && state.projects[state.currentProjectId].empty_repo;
+
export const currentTree = state =>
state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index c2760eb1554..f81bdb8a30e 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -135,6 +135,17 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
return null;
}
+ if (!data.parent_ids.length) {
+ commit(
+ rootTypes.TOGGLE_EMPTY_STATE,
+ {
+ projectPath: rootState.currentProjectId,
+ value: false,
+ },
+ { root: true },
+ );
+ }
+
dispatch('setLastCommitMessage', data);
dispatch('updateCommitMessage', '');
return dispatch('updateFilesAfterCommit', {
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index a5f8098dc17..86ab76136df 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -12,6 +12,7 @@ export const SET_LINKS = 'SET_LINKS';
export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
+export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
// Merge Request Mutation Types
export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST';
diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js
index e09f88878f4..6afd8de2aa4 100644
--- a/app/assets/javascripts/ide/stores/mutations/branch.js
+++ b/app/assets/javascripts/ide/stores/mutations/branch.js
@@ -19,6 +19,12 @@ export default {
});
},
[types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) {
+ if (!state.projects[projectId].branches[branchId]) {
+ Object.assign(state.projects[projectId].branches, {
+ [branchId]: {},
+ });
+ }
+
Object.assign(state.projects[projectId].branches[branchId], {
workingReference: reference,
});
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
index 284b39a2c72..9230f3839c1 100644
--- a/app/assets/javascripts/ide/stores/mutations/project.js
+++ b/app/assets/javascripts/ide/stores/mutations/project.js
@@ -21,4 +21,9 @@ export default {
}),
});
},
+ [types.TOGGLE_EMPTY_STATE](state, { projectPath, value }) {
+ Object.assign(state.projects[projectPath], {
+ empty_repo: value,
+ });
+ },
};
diff --git a/app/assets/javascripts/import_projects/store/getters.js b/app/assets/javascripts/import_projects/store/getters.js
index f03474a8404..727b80765bd 100644
--- a/app/assets/javascripts/import_projects/store/getters.js
+++ b/app/assets/javascripts/import_projects/store/getters.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export const namespaceSelectOptions = state => {
const serializedNamespaces = state.namespaces.map(({ fullPath }) => ({
id: fullPath,
@@ -5,9 +7,9 @@ export const namespaceSelectOptions = state => {
}));
return [
- { text: 'Groups', children: serializedNamespaces },
+ { text: __('Groups'), children: serializedNamespaces },
{
- text: 'Users',
+ text: __('Users'),
children: [{ id: state.defaultTargetNamespace, text: state.defaultTargetNamespace }],
},
];
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index 7594edfac27..79fb67d38cd 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -86,6 +86,7 @@ export default {
'isScrollTopDisabled',
'isScrolledToBottomBeforeReceivingTrace',
'hasError',
+ 'selectedStage',
]),
...mapGetters([
'headerTime',
@@ -121,7 +122,13 @@ export default {
// fetch the stages for the dropdown on the sidebar
job(newVal, oldVal) {
if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
- this.fetchStages();
+ const stages = this.job.pipeline.details.stages || [];
+
+ const defaultStage = stages.find(stage => stage && stage.name === this.selectedStage);
+
+ if (defaultStage) {
+ this.fetchJobsForStage(defaultStage);
+ }
}
if (newVal.archived) {
@@ -160,7 +167,7 @@ export default {
'setJobEndpoint',
'setTraceOptions',
'fetchJob',
- 'fetchStages',
+ 'fetchJobsForStage',
'hideSidebar',
'showSidebar',
'toggleSidebar',
@@ -269,7 +276,6 @@ export default {
:class="{ 'sticky-top border-bottom-0': hasTrace }"
>
<icon name="lock" class="align-text-bottom" />
-
{{ __('This job is archived. Only the complete pipeline can be retried.') }}
</div>
<!-- job log -->
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
index 1691ac62100..24276c06486 100644
--- a/app/assets/javascripts/jobs/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -34,7 +34,7 @@ export default {
},
},
computed: {
- ...mapState(['job', 'stages', 'jobs', 'selectedStage', 'isLoadingStages']),
+ ...mapState(['job', 'stages', 'jobs', 'selectedStage']),
coverage() {
return `${this.job.coverage}%`;
},
@@ -208,7 +208,6 @@ export default {
/>
<stages-dropdown
- v-if="!isLoadingStages"
:stages="stages"
:pipeline="job.pipeline"
:selected-stage="selectedStage"
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js
index 8045f6dc3ff..12d67a43599 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/jobs/store/actions.js
@@ -179,37 +179,13 @@ export const receiveTraceError = ({ commit }) => {
};
/**
- * Stages dropdown on sidebar
- */
-export const requestStages = ({ commit }) => commit(types.REQUEST_STAGES);
-export const fetchStages = ({ state, dispatch }) => {
- dispatch('requestStages');
-
- axios
- .get(`${state.job.pipeline.path}.json`)
- .then(({ data }) => {
- // Set selected stage
- dispatch('receiveStagesSuccess', data.details.stages);
- const selectedStage = data.details.stages.find(stage => stage.name === state.selectedStage);
- dispatch('fetchJobsForStage', selectedStage);
- })
- .catch(() => dispatch('receiveStagesError'));
-};
-export const receiveStagesSuccess = ({ commit }, data) =>
- commit(types.RECEIVE_STAGES_SUCCESS, data);
-export const receiveStagesError = ({ commit }) => {
- commit(types.RECEIVE_STAGES_ERROR);
- flash(__('An error occurred while fetching stages.'));
-};
-
-/**
* Jobs list on sidebar - depend on stages dropdown
*/
export const requestJobsForStage = ({ commit }, stage) =>
commit(types.REQUEST_JOBS_FOR_STAGE, stage);
// On stage click, set selected stage + fetch job
-export const fetchJobsForStage = ({ dispatch }, stage) => {
+export const fetchJobsForStage = ({ dispatch }, stage = {}) => {
dispatch('requestJobsForStage', stage);
axios
diff --git a/app/assets/javascripts/jobs/store/mutation_types.js b/app/assets/javascripts/jobs/store/mutation_types.js
index fd098f13e90..39146b2eefd 100644
--- a/app/assets/javascripts/jobs/store/mutation_types.js
+++ b/app/assets/javascripts/jobs/store/mutation_types.js
@@ -24,10 +24,6 @@ export const STOP_POLLING_TRACE = 'STOP_POLLING_TRACE';
export const RECEIVE_TRACE_SUCCESS = 'RECEIVE_TRACE_SUCCESS';
export const RECEIVE_TRACE_ERROR = 'RECEIVE_TRACE_ERROR';
-export const REQUEST_STAGES = 'REQUEST_STAGES';
-export const RECEIVE_STAGES_SUCCESS = 'RECEIVE_STAGES_SUCCESS';
-export const RECEIVE_STAGES_ERROR = 'RECEIVE_STAGES_ERROR';
-
export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE';
export const REQUEST_JOBS_FOR_STAGE = 'REQUEST_JOBS_FOR_STAGE';
export const RECEIVE_JOBS_FOR_STAGE_SUCCESS = 'RECEIVE_JOBS_FOR_STAGE_SUCCESS';
diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js
index cd440d21c1f..ad08f27b147 100644
--- a/app/assets/javascripts/jobs/store/mutations.js
+++ b/app/assets/javascripts/jobs/store/mutations.js
@@ -65,6 +65,11 @@ export default {
state.isLoading = false;
state.job = job;
+ state.stages =
+ job.pipeline && job.pipeline.details && job.pipeline.details.stages
+ ? job.pipeline.details.stages
+ : [];
+
/**
* We only update it on the first request
* The dropdown can be changed by the user
@@ -101,19 +106,7 @@ export default {
state.isScrolledToBottomBeforeReceivingTrace = toggle;
},
- [types.REQUEST_STAGES](state) {
- state.isLoadingStages = true;
- },
- [types.RECEIVE_STAGES_SUCCESS](state, stages) {
- state.isLoadingStages = false;
- state.stages = stages;
- },
- [types.RECEIVE_STAGES_ERROR](state) {
- state.isLoadingStages = false;
- state.stages = [];
- },
-
- [types.REQUEST_JOBS_FOR_STAGE](state, stage) {
+ [types.REQUEST_JOBS_FOR_STAGE](state, stage = {}) {
state.isLoadingJobs = true;
state.selectedStage = stage.name;
},
diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js
index 04825187c99..6019214e62c 100644
--- a/app/assets/javascripts/jobs/store/state.js
+++ b/app/assets/javascripts/jobs/store/state.js
@@ -25,7 +25,6 @@ export default () => ({
traceState: null,
// sidebar dropdown & list of jobs
- isLoadingStages: false,
isLoadingJobs: false,
selectedStage: '',
stages: [],
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index 4d304c5fe69..c6dd21cd2d4 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -5,13 +5,14 @@ import Sortable from 'sortablejs';
import flash from './flash';
import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
export default class LabelManager {
constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority');
this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels');
this.otherLabels = otherLabels || $('.js-other-labels');
- this.errorMessage = 'Unable to update label prioritization at this time';
+ this.errorMessage = __('Unable to update label prioritization at this time');
this.emptyState = document.querySelector('#js-priority-labels-empty-state');
this.$badgeItemTemplate = $('#js-badge-item-template');
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 2c30b4ea587..3f954b43ee3 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -4,7 +4,7 @@
import $ from 'jquery';
import _ from 'underscore';
-import { sprintf, __ } from './locale';
+import { sprintf, s__, __ } from './locale';
import axios from './lib/utils/axios_utils';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import CreateLabelDropdown from './create_label';
@@ -178,7 +178,7 @@ export default class LabelsSelect {
});
}
} else {
- template = '<span class="no-value">None</span>';
+ template = `<span class="no-value">${__('None')}</span>`;
}
$value.removeAttr('style').html(template);
$sidebarCollapsedValue.text(labelCount);
@@ -190,7 +190,9 @@ export default class LabelsSelect {
if (labelTitles.length > 5) {
labelTitles = labelTitles.slice(0, 5);
- labelTitles.push('and ' + (data.labels.length - 5) + ' more');
+ labelTitles.push(
+ sprintf(s__('Labels|and %{count} more'), { count: data.labels.length - 5 }),
+ );
}
labelTooltipTitle = labelTitles.join(', ');
@@ -219,13 +221,13 @@ export default class LabelsSelect {
if (showNo) {
extraData.unshift({
id: 0,
- title: 'No Label',
+ title: __('No Label'),
});
}
if (showAny) {
extraData.unshift({
isAny: true,
- title: 'Any Label',
+ title: __('Any Label'),
});
}
if (extraData.length) {
@@ -341,7 +343,7 @@ export default class LabelsSelect {
if (selected && selected.id === 0) {
this.selected = [];
- return 'No Label';
+ return __('No Label');
} else if (isSelected) {
this.selected.push(title);
} else if (!isSelected && title) {
@@ -579,7 +581,7 @@ export default class LabelsSelect {
if ($('.selected-issuable:checked').length) {
return;
}
- return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label');
+ return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text(__('Label'));
}
// eslint-disable-next-line class-methods-use-this
enableBulkLabelDropdown() {
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 498c2348ca2..47e91dedd5a 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -3,12 +3,12 @@ import { InMemoryCache } from 'apollo-cache-inmemory';
import { createUploadLink } from 'apollo-upload-client';
import csrf from '~/lib/utils/csrf';
-export default (resolvers = {}, baseUrl = '') => {
+export default (resolvers = {}, config = {}) => {
let uri = `${gon.relative_url_root}/api/graphql`;
- if (baseUrl) {
+ if (config.baseUrl) {
// Prepend baseUrl and ensure that `///` are replaced with `/`
- uri = `${baseUrl}${uri}`.replace(/\/{3,}/g, '/');
+ uri = `${config.baseUrl}${uri}`.replace(/\/{3,}/g, '/');
}
return new ApolloClient({
@@ -18,7 +18,7 @@ export default (resolvers = {}, baseUrl = '') => {
[csrf.headerKey]: csrf.token,
},
}),
- cache: new InMemoryCache(),
+ cache: new InMemoryCache(config.cacheConfig),
resolvers,
});
};
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 4d6327840db..32cafb74d91 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -3,7 +3,7 @@ import _ from 'underscore';
import timeago from 'timeago.js';
import dateFormat from 'dateformat';
import { pluralize } from './text_utility';
-import { languageCode, s__ } from '../../locale';
+import { languageCode, s__, __ } from '../../locale';
window.timeago = timeago;
@@ -63,7 +63,15 @@ export const pad = (val, len = 2) => `0${val}`.slice(-len);
* @returns {String}
*/
export const getDayName = date =>
- ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()];
+ [
+ __('Sunday'),
+ __('Monday'),
+ __('Tuesday'),
+ __('Wednesday'),
+ __('Thursday'),
+ __('Friday'),
+ __('Saturday'),
+ ][date.getDay()];
/**
* @example
@@ -71,7 +79,12 @@ export const getDayName = date =>
* @param {date} datetime
* @returns {String}
*/
-export const formatDate = datetime => dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
+export const formatDate = datetime => {
+ if (_.isString(datetime) && datetime.match(/\d+-\d+\d+ /)) {
+ throw new Error('Invalid date');
+ }
+ return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
+};
/**
* Timeago uses underscores instead of dashes to separate language from country code.
@@ -320,13 +333,13 @@ export const getSundays = date => {
}
const daysToSunday = [
- 'Saturday',
- 'Friday',
- 'Thursday',
- 'Wednesday',
- 'Tuesday',
- 'Monday',
- 'Sunday',
+ __('Saturday'),
+ __('Friday'),
+ __('Thursday'),
+ __('Wednesday'),
+ __('Tuesday'),
+ __('Monday'),
+ __('Sunday'),
];
const month = date.getMonth();
@@ -336,7 +349,7 @@ export const getSundays = date => {
while (dateOfMonth.getMonth() === month) {
const dayName = getDayName(dateOfMonth);
- if (dayName === 'Sunday') {
+ if (dayName === __('Sunday')) {
sundays.push(new Date(dateOfMonth.getTime()));
}
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index 19c4de6083d..9ddfb4bca11 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -1,4 +1,5 @@
import { BYTES_IN_KIB } from './constants';
+import { sprintf, __ } from '~/locale';
/**
* Function that allows a number with an X amount of decimals
@@ -72,13 +73,13 @@ export function bytesToGiB(number) {
*/
export function numberToHumanSize(size) {
if (size < BYTES_IN_KIB) {
- return `${size} bytes`;
+ return sprintf(__('%{size} bytes'), { size });
} else if (size < BYTES_IN_KIB * BYTES_IN_KIB) {
- return `${bytesToKiB(size).toFixed(2)} KiB`;
+ return sprintf(__('%{size} KiB'), { size: bytesToKiB(size).toFixed(2) });
} else if (size < BYTES_IN_KIB * BYTES_IN_KIB * BYTES_IN_KIB) {
- return `${bytesToMiB(size).toFixed(2)} MiB`;
+ return sprintf(__('%{size} MiB'), { size: bytesToMiB(size).toFixed(2) });
}
- return `${bytesToGiB(size).toFixed(2)} GiB`;
+ return sprintf(__('%{size} GiB'), { size: bytesToGiB(size).toFixed(2) });
}
/**
diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js
index 1ae3362c4bc..41aa0f4ddb9 100644
--- a/app/assets/javascripts/locale/index.js
+++ b/app/assets/javascripts/locale/index.js
@@ -11,7 +11,7 @@ delete window.translations;
@param text The text to be translated
@returns {String} The translated text
*/
-const gettext = text => locale.gettext.bind(locale)(ensureSingleLine(text));
+const gettext = text => locale.gettext(ensureSingleLine(text));
/**
Translate the text with a number
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue
index afe8d87a8d6..c43791f2426 100644
--- a/app/assets/javascripts/monitoring/components/charts/area.vue
+++ b/app/assets/javascripts/monitoring/components/charts/area.vue
@@ -125,17 +125,17 @@ export default {
},
earliestDatapoint() {
return this.chartData.reduce((acc, series) => {
- if (!series.data.length) {
+ const { data } = series;
+ const { length } = data;
+ if (!length) {
return acc;
}
- const [[timestamp]] = series.data.sort(([a], [b]) => {
- if (a < b) {
- return -1;
- }
- return a > b ? 1 : 0;
- });
- return timestamp < acc || acc === null ? timestamp : acc;
+ const [first] = data[0];
+ const [last] = data[length - 1];
+ const seriesEarliest = first < last ? first : last;
+
+ return seriesEarliest < acc || acc === null ? seriesEarliest : acc;
}, null);
},
isMultiSeries() {
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index ff1e1805948..a55a47c277d 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -8,16 +8,14 @@ import {
GlLink,
} from '@gitlab/ui';
import _ from 'underscore';
+import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import '~/vue_shared/mixins/is_ee';
import { getParameterValues } from '~/lib/utils/url_utility';
-import Flash from '../../flash';
-import MonitoringService from '../services/monitoring_service';
import MonitorAreaChart from './charts/area.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
-import MonitoringStore from '../stores/monitoring_store';
import { timeWindows, timeWindowsKeyNames } from '../constants';
import { getTimeDiff } from '../utils';
@@ -128,9 +126,7 @@ export default {
},
data() {
return {
- store: new MonitoringStore(),
state: 'gettingStarted',
- showEmptyState: true,
elWidth: 0,
selectedTimeWindow: '',
selectedTimeWindowKey: '',
@@ -141,13 +137,21 @@ export default {
canAddMetrics() {
return this.customMetricsAvailable && this.customMetricsPath.length;
},
+ ...mapState('monitoringDashboard', [
+ 'groups',
+ 'emptyState',
+ 'showEmptyState',
+ 'environments',
+ 'deploymentData',
+ ]),
},
created() {
- this.service = new MonitoringService({
+ this.setEndpoints({
metricsEndpoint: this.metricsEndpoint,
- deploymentEndpoint: this.deploymentEndpoint,
environmentsEndpoint: this.environmentsEndpoint,
+ deploymentsEndpoint: this.deploymentEndpoint,
});
+
this.timeWindows = timeWindows;
this.selectedTimeWindowKey =
_.escape(getParameterValues('time_window')[0]) || timeWindowsKeyNames.eightHours;
@@ -165,31 +169,11 @@ export default {
}
},
mounted() {
- const startEndWindow = getTimeDiff(this.timeWindows[this.selectedTimeWindowKey]);
- this.servicePromises = [
- this.service
- .getGraphsData(startEndWindow)
- .then(data => this.store.storeMetrics(data))
- .catch(() => Flash(s__('Metrics|There was an error while retrieving metrics'))),
- this.service
- .getDeploymentData()
- .then(data => this.store.storeDeploymentData(data))
- .catch(() => Flash(s__('Metrics|There was an error getting deployment information.'))),
- ];
if (!this.hasMetrics) {
- this.state = 'gettingStarted';
+ this.setGettingStartedEmptyState();
} else {
- if (this.environmentsEndpoint) {
- this.servicePromises.push(
- this.service
- .getEnvironmentsData()
- .then(data => this.store.storeEnvironmentsData(data))
- .catch(() =>
- Flash(s__('Metrics|There was an error getting environments information.')),
- ),
- );
- }
- this.getGraphsData();
+ this.fetchData(getTimeDiff(this.timeWindows.eightHours));
+
sidebarMutationObserver = new MutationObserver(this.onSidebarMutation);
sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
attributes: true,
@@ -199,6 +183,11 @@ export default {
}
},
methods: {
+ ...mapActions('monitoringDashboard', [
+ 'fetchData',
+ 'setGettingStartedEmptyState',
+ 'setEndpoints',
+ ]),
getGraphAlerts(queries) {
if (!this.allAlerts) return {};
const metricIdsForChart = queries.map(q => q.metricId);
@@ -207,21 +196,6 @@ export default {
getGraphAlertValues(queries) {
return Object.values(this.getGraphAlerts(queries));
},
- getGraphsData() {
- this.state = 'loading';
- Promise.all(this.servicePromises)
- .then(() => {
- if (this.store.groups.length < 1) {
- this.state = 'noData';
- return;
- }
-
- this.showEmptyState = false;
- })
- .catch(() => {
- this.state = 'unableToConnect';
- });
- },
hideAddMetricModal() {
this.$refs.addMetricModal.hide();
},
@@ -263,10 +237,10 @@ export default {
class="prepend-left-10 js-environments-dropdown"
toggle-class="dropdown-menu-toggle"
:text="currentEnvironmentName"
- :disabled="store.environmentsData.length === 0"
+ :disabled="environments.length === 0"
>
<gl-dropdown-item
- v-for="environment in store.environmentsData"
+ v-for="environment in environments"
:key="environment.id"
:active="environment.name === currentEnvironmentName"
active-class="is-active"
@@ -336,7 +310,7 @@ export default {
</div>
</div>
<graph-group
- v-for="(groupData, index) in store.groups"
+ v-for="(groupData, index) in groups"
:key="index"
:name="groupData.group"
:show-panels="showPanels"
@@ -345,7 +319,7 @@ export default {
v-for="(graphData, graphIndex) in groupData.metrics"
:key="graphIndex"
:graph-data="graphData"
- :deployment-data="store.deploymentData"
+ :deployment-data="deploymentData"
:thresholds="getGraphAlertValues(graphData.queries)"
:container-width="elWidth"
group-id="monitor-area-chart"
@@ -362,7 +336,7 @@ export default {
</div>
<empty-state
v-else
- :selected-state="state"
+ :selected-state="emptyState"
:documentation-path="documentationPath"
:settings-path="settingsPath"
:clusters-path="clustersPath"
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index 08dc57d545c..57771ccf4d9 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import Dashboard from 'ee_else_ce/monitoring/components/dashboard.vue';
+import store from './stores';
export default (props = {}) => {
const el = document.getElementById('prometheus-graphs');
@@ -9,6 +10,7 @@ export default (props = {}) => {
// eslint-disable-next-line no-new
new Vue({
el,
+ store,
render(createElement) {
return createElement(Dashboard, {
props: {
diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js
deleted file mode 100644
index 1efa5189996..00000000000
--- a/app/assets/javascripts/monitoring/services/monitoring_service.js
+++ /dev/null
@@ -1,75 +0,0 @@
-import axios from '../../lib/utils/axios_utils';
-import statusCodes from '../../lib/utils/http_status';
-import { backOff } from '../../lib/utils/common_utils';
-import { s__, __ } from '../../locale';
-
-const MAX_REQUESTS = 3;
-
-function backOffRequest(makeRequestCallback) {
- let requestCounter = 0;
- return backOff((next, stop) => {
- makeRequestCallback()
- .then(resp => {
- if (resp.status === statusCodes.NO_CONTENT) {
- requestCounter += 1;
- if (requestCounter < MAX_REQUESTS) {
- next();
- } else {
- stop(new Error(__('Failed to connect to the prometheus server')));
- }
- } else {
- stop(resp);
- }
- })
- .catch(stop);
- });
-}
-
-export default class MonitoringService {
- constructor({ metricsEndpoint, deploymentEndpoint, environmentsEndpoint }) {
- this.metricsEndpoint = metricsEndpoint;
- this.deploymentEndpoint = deploymentEndpoint;
- this.environmentsEndpoint = environmentsEndpoint;
- }
-
- getGraphsData(params = {}) {
- return backOffRequest(() => axios.get(this.metricsEndpoint, { params }))
- .then(resp => resp.data)
- .then(response => {
- if (!response || !response.data || !response.success) {
- throw new Error(s__('Metrics|Unexpected metrics data response from prometheus endpoint'));
- }
- return response.data;
- });
- }
-
- getDeploymentData() {
- if (!this.deploymentEndpoint) {
- return Promise.resolve([]);
- }
- return backOffRequest(() => axios.get(this.deploymentEndpoint))
- .then(resp => resp.data)
- .then(response => {
- if (!response || !response.deployments) {
- throw new Error(
- s__('Metrics|Unexpected deployment data response from prometheus endpoint'),
- );
- }
- return response.deployments;
- });
- }
-
- getEnvironmentsData() {
- return axios
- .get(this.environmentsEndpoint)
- .then(resp => resp.data)
- .then(response => {
- if (!response || !response.environments) {
- throw new Error(
- s__('Metrics|There was an error fetching the environments data, please try again'),
- );
- }
- return response.environments;
- });
- }
-}
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
new file mode 100644
index 00000000000..63c23e8449d
--- /dev/null
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -0,0 +1,117 @@
+import * as types from './mutation_types';
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
+import statusCodes from '../../lib/utils/http_status';
+import { backOff } from '../../lib/utils/common_utils';
+import { s__, __ } from '../../locale';
+
+const MAX_REQUESTS = 3;
+
+function backOffRequest(makeRequestCallback) {
+ let requestCounter = 0;
+ return backOff((next, stop) => {
+ makeRequestCallback()
+ .then(resp => {
+ if (resp.status === statusCodes.NO_CONTENT) {
+ requestCounter += 1;
+ if (requestCounter < MAX_REQUESTS) {
+ next();
+ } else {
+ stop(new Error(__('Failed to connect to the prometheus server')));
+ }
+ } else {
+ stop(resp);
+ }
+ })
+ .catch(stop);
+ });
+}
+
+export const setGettingStartedEmptyState = ({ commit }) => {
+ commit(types.SET_GETTING_STARTED_EMPTY_STATE);
+};
+
+export const setEndpoints = ({ commit }, endpoints) => {
+ commit(types.SET_ENDPOINTS, endpoints);
+};
+
+export const requestMetricsData = ({ commit }) => commit(types.REQUEST_METRICS_DATA);
+export const receiveMetricsDataSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_METRICS_DATA_SUCCESS, data);
+export const receiveMetricsDataFailure = ({ commit }, error) =>
+ commit(types.RECEIVE_METRICS_DATA_FAILURE, error);
+export const receiveDeploymentsDataSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS, data);
+export const receiveDeploymentsDataFailure = ({ commit }) =>
+ commit(types.RECEIVE_DEPLOYMENTS_DATA_FAILURE);
+export const receiveEnvironmentsDataSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS, data);
+export const receiveEnvironmentsDataFailure = ({ commit }) =>
+ commit(types.RECEIVE_ENVIRONMENTS_DATA_FAILURE);
+
+export const fetchData = ({ dispatch }, params) => {
+ dispatch('fetchMetricsData', params);
+ dispatch('fetchDeploymentsData');
+ dispatch('fetchEnvironmentsData');
+};
+
+export const fetchMetricsData = ({ state, dispatch }, params) => {
+ dispatch('requestMetricsData');
+
+ return backOffRequest(() => axios.get(state.metricsEndpoint, { params }))
+ .then(resp => resp.data)
+ .then(response => {
+ if (!response || !response.data || !response.success) {
+ dispatch('receiveMetricsDataFailure', null);
+ createFlash(s__('Metrics|Unexpected metrics data response from prometheus endpoint'));
+ }
+ dispatch('receiveMetricsDataSuccess', response.data);
+ })
+ .catch(error => {
+ dispatch('receiveMetricsDataFailure', error);
+ createFlash(s__('Metrics|There was an error while retrieving metrics'));
+ });
+};
+
+export const fetchDeploymentsData = ({ state, dispatch }) => {
+ if (!state.deploymentEndpoint) {
+ return Promise.resolve([]);
+ }
+ return backOffRequest(() => axios.get(state.deploymentEndpoint))
+ .then(resp => resp.data)
+ .then(response => {
+ if (!response || !response.deployments) {
+ createFlash(s__('Metrics|Unexpected deployment data response from prometheus endpoint'));
+ }
+
+ dispatch('receiveDeploymentsDataSuccess', response.deployments);
+ })
+ .catch(() => {
+ dispatch('receiveDeploymentsDataFailure');
+ createFlash(s__('Metrics|There was an error getting deployment information.'));
+ });
+};
+
+export const fetchEnvironmentsData = ({ state, dispatch }) => {
+ if (!state.environmentsEndpoint) {
+ return Promise.resolve([]);
+ }
+ return axios
+ .get(state.environmentsEndpoint)
+ .then(resp => resp.data)
+ .then(response => {
+ if (!response || !response.environments) {
+ createFlash(
+ s__('Metrics|There was an error fetching the environments data, please try again'),
+ );
+ }
+ dispatch('receiveEnvironmentsDataSuccess', response.environments);
+ })
+ .catch(() => {
+ dispatch('receiveEnvironmentsDataFailure');
+ createFlash(s__('Metrics|There was an error getting environments information.'));
+ });
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/index.js b/app/assets/javascripts/monitoring/stores/index.js
new file mode 100644
index 00000000000..d58398c54ae
--- /dev/null
+++ b/app/assets/javascripts/monitoring/stores/index.js
@@ -0,0 +1,21 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export const createStore = () =>
+ new Vuex.Store({
+ modules: {
+ monitoringDashboard: {
+ namespaced: true,
+ actions,
+ mutations,
+ state,
+ },
+ },
+ });
+
+export default createStore();
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
new file mode 100644
index 00000000000..74c4ae64712
--- /dev/null
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -0,0 +1,15 @@
+export const REQUEST_METRICS_DATA = 'REQUEST_METRICS_DATA';
+export const RECEIVE_METRICS_DATA_SUCCESS = 'RECEIVE_METRICS_DATA_SUCCESS';
+export const RECEIVE_METRICS_DATA_FAILURE = 'RECEIVE_METRICS_DATA_FAILURE';
+export const REQUEST_DEPLOYMENTS_DATA = 'REQUEST_DEPLOYMENTS_DATA';
+export const RECEIVE_DEPLOYMENTS_DATA_SUCCESS = 'RECEIVE_DEPLOYMENTS_DATA_SUCCESS';
+export const RECEIVE_DEPLOYMENTS_DATA_FAILURE = 'RECEIVE_DEPLOYMENTS_DATA_FAILURE';
+export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA';
+export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS';
+export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAILURE';
+export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
+export const SET_METRICS_ENDPOINT = 'SET_METRICS_ENDPOINT';
+export const SET_ENVIRONMENTS_ENDPOINT = 'SET_ENVIRONMENTS_ENDPOINT';
+export const SET_DEPLOYMENTS_ENDPOINT = 'SET_DEPLOYMENTS_ENDPOINT';
+export const SET_ENDPOINTS = 'SET_ENDPOINTS';
+export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
new file mode 100644
index 00000000000..c1779333d75
--- /dev/null
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -0,0 +1,45 @@
+import * as types from './mutation_types';
+import { normalizeMetrics, sortMetrics } from './utils';
+
+export default {
+ [types.REQUEST_METRICS_DATA](state) {
+ state.emptyState = 'loading';
+ state.showEmptyState = true;
+ },
+ [types.RECEIVE_METRICS_DATA_SUCCESS](state, groupData) {
+ state.groups = groupData.map(group => ({
+ ...group,
+ metrics: normalizeMetrics(sortMetrics(group.metrics)),
+ }));
+
+ if (!state.groups.length) {
+ state.emptyState = 'noData';
+ } else {
+ state.showEmptyState = false;
+ }
+ },
+ [types.RECEIVE_METRICS_DATA_FAILURE](state, error) {
+ state.emptyState = error ? 'unableToConnect' : 'noData';
+ state.showEmptyState = true;
+ },
+ [types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS](state, deployments) {
+ state.deploymentData = deployments;
+ },
+ [types.RECEIVE_DEPLOYMENTS_DATA_FAILURE](state) {
+ state.deploymentData = [];
+ },
+ [types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS](state, environments) {
+ state.environments = environments;
+ },
+ [types.RECEIVE_ENVIRONMENTS_DATA_FAILURE](state) {
+ state.environments = [];
+ },
+ [types.SET_ENDPOINTS](state, endpoints) {
+ state.metricsEndpoint = endpoints.metricsEndpoint;
+ state.environmentsEndpoint = endpoints.environmentsEndpoint;
+ state.deploymentsEndpoint = endpoints.deploymentsEndpoint;
+ },
+ [types.SET_GETTING_STARTED_EMPTY_STATE](state) {
+ state.emptyState = 'gettingStarted';
+ },
+};
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
new file mode 100644
index 00000000000..5103122612a
--- /dev/null
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -0,0 +1,12 @@
+export default () => ({
+ hasMetrics: false,
+ showPanels: true,
+ metricsEndpoint: null,
+ environmentsEndpoint: null,
+ deploymentsEndpoint: null,
+ emptyState: 'gettingStarted',
+ showEmptyState: true,
+ groups: [],
+ deploymentData: [],
+ environments: [],
+});
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/utils.js
index 013fb0d4540..9216554ecbf 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/utils.js
@@ -1,12 +1,5 @@
import _ from 'underscore';
-function sortMetrics(metrics) {
- return _.chain(metrics)
- .sortBy('title')
- .sortBy('weight')
- .value();
-}
-
function checkQueryEmptyData(query) {
return {
...query,
@@ -59,7 +52,13 @@ function groupQueriesByChartInfo(metrics) {
return Object.values(metricsByChart);
}
-function normalizeMetrics(metrics) {
+export const sortMetrics = metrics =>
+ _.chain(metrics)
+ .sortBy('title')
+ .sortBy('weight')
+ .value();
+
+export const normalizeMetrics = metrics => {
const groupedMetrics = groupQueriesByChartInfo(metrics);
return groupedMetrics.map(metric => {
@@ -81,31 +80,4 @@ function normalizeMetrics(metrics) {
queries: removeTimeSeriesNoData(queries),
};
});
-}
-
-export default class MonitoringStore {
- constructor() {
- this.groups = [];
- this.deploymentData = [];
- this.environmentsData = [];
- }
-
- storeMetrics(groups = []) {
- this.groups = groups.map(group => ({
- ...group,
- metrics: normalizeMetrics(sortMetrics(group.metrics)),
- }));
- }
-
- storeDeploymentData(deploymentData = []) {
- this.deploymentData = deploymentData;
- }
-
- storeEnvironmentsData(environmentsData = []) {
- this.environmentsData = environmentsData.filter(environment => !!environment.last_deployment);
- }
-
- getMetricsCount() {
- return this.groups.reduce((count, group) => count + group.metrics.length, 0);
- }
-}
+};
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 7e8f23d6a96..5a4ff15d198 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -148,12 +148,9 @@ export default {
href="#"
title="Add reaction"
>
- <icon
- css-classes="link-highlight award-control-icon-neutral"
- name="emoji_slightly_smiling_face"
- />
- <icon css-classes="link-highlight award-control-icon-positive" name="emoji_smiley" />
- <icon css-classes="link-highlight award-control-icon-super-positive" name="emoji_smiley" />
+ <icon css-classes="link-highlight award-control-icon-neutral" name="slight-smile" />
+ <icon css-classes="link-highlight award-control-icon-positive" name="smiley" />
+ <icon css-classes="link-highlight award-control-icon-super-positive" name="smiley" />
</a>
</div>
<reply-button
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index 17e5fcab5b7..941b6d5cab3 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -189,13 +189,13 @@ export default {
type="button"
>
<span class="award-control-icon award-control-icon-neutral">
- <icon name="emoji_slightly_smiling_face" />
+ <icon name="slight-smile" />
</span>
<span class="award-control-icon award-control-icon-positive">
- <icon name="emoji_smiley" />
+ <icon name="smiley" />
</span>
<span class="award-control-icon award-control-icon-super-positive">
- <icon name="emoji_smiley" />
+ <icon name="smiley" />
</span>
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
index a79ef07f1c5..c563514d36b 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
@@ -33,8 +33,7 @@ export default {
text() {
return sprintf(
s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}.
- Existing project milestones with the same title will be merged.
- This action cannot be reversed.`),
+ Existing project milestones with the same title will be merged.`),
{ milestoneTitle: this.milestoneTitle, groupName: this.groupName },
);
},
@@ -72,6 +71,9 @@ export default {
<template slot="title">
{{ title }}
</template>
- {{ text }}
+ <div>
+ <p>{{ text }}</p>
+ <p>{{ s__('Milestones|This action cannot be reversed.') }}</p>
+ </div>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue
index dc5f9ba9607..6d39abd4a1f 100644
--- a/app/assets/javascripts/pdf/index.vue
+++ b/app/assets/javascripts/pdf/index.vue
@@ -1,6 +1,6 @@
<script>
-import pdfjsLib from 'vendor/pdf';
-import workerSrc from 'vendor/pdf.worker.min';
+import pdfjsLib from 'pdfjs-dist/build/pdf';
+import workerSrc from 'pdfjs-dist/build/pdf.worker.min';
import page from './page/index.vue';
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
index 482898b80c4..ebd7a17040a 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
@@ -69,7 +69,9 @@ export default {
>
<ci-icon :status="group.status" />
- <span class="ci-status-text"> {{ group.name }} </span>
+ <span class="ci-status-text text-truncate mw-70p gl-pl-1 d-inline-block align-bottom">
+ {{ group.name }}
+ </span>
<span class="dropdown-counter-badge"> {{ group.size }} </span>
</button>
diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js
index d3c604dcee1..5395e14cc79 100644
--- a/app/assets/javascripts/project_label_subscription.js
+++ b/app/assets/javascripts/project_label_subscription.js
@@ -38,9 +38,9 @@ export default class ProjectLabelSubscription {
let newAction;
if (oldStatus === 'unsubscribed') {
- [newStatus, newAction] = ['subscribed', 'Unsubscribe'];
+ [newStatus, newAction] = ['subscribed', __('Unsubscribe')];
} else {
- [newStatus, newAction] = ['unsubscribed', 'Subscribe'];
+ [newStatus, newAction] = ['unsubscribed', __('Subscribe')];
}
$btn.removeClass('disabled');
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 5ee510eb11d..dbe354a547b 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -3,6 +3,7 @@
import $ from 'jquery';
import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button';
+import { s__ } from './locale';
export default function projectSelect() {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
@@ -21,9 +22,9 @@ export default function projectSelect() {
this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
this.allowClear = $(select).data('allowClear') || false;
- placeholder = 'Search for project';
+ placeholder = s__('ProjectSelect|Search for project');
if (this.includeGroups) {
- placeholder += ' or group';
+ placeholder += s__('ProjectSelect| or group');
}
$(select).select2({
diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue
index 420e71f5e86..241185e3126 100644
--- a/app/assets/javascripts/reports/components/report_section.vue
+++ b/app/assets/javascripts/reports/components/report_section.vue
@@ -156,7 +156,7 @@ export default {
<button
v-if="isCollapsible"
type="button"
- class="js-collapse-btn btn float-right btn-sm"
+ class="js-collapse-btn btn float-right btn-sm qa-expand-report-button"
@click="toggleCollapsed"
>
{{ collapseText }}
diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue
new file mode 100644
index 00000000000..6eca015036f
--- /dev/null
+++ b/app/assets/javascripts/repository/components/breadcrumbs.vue
@@ -0,0 +1,61 @@
+<script>
+import getRefMixin from '../mixins/get_ref';
+import getProjectShortPath from '../queries/getProjectShortPath.graphql';
+
+export default {
+ apollo: {
+ projectShortPath: {
+ query: getProjectShortPath,
+ },
+ },
+ mixins: [getRefMixin],
+ props: {
+ currentPath: {
+ type: String,
+ required: false,
+ default: '/',
+ },
+ },
+ data() {
+ return {
+ projectShortPath: '',
+ };
+ },
+ computed: {
+ pathLinks() {
+ return this.currentPath
+ .split('/')
+ .filter(p => p !== '')
+ .reduce(
+ (acc, name, i) => {
+ const path = `${i > 0 ? acc[i].path : ''}/${name}`;
+
+ return acc.concat({
+ name,
+ path,
+ to: `/tree/${this.ref}${path}`,
+ });
+ },
+ [{ name: this.projectShortPath, path: '/', to: `/tree/${this.ref}` }],
+ );
+ },
+ },
+ methods: {
+ isLast(i) {
+ return i === this.pathLinks.length - 1;
+ },
+ },
+};
+</script>
+
+<template>
+ <nav :aria-label="__('Files breadcrumb')">
+ <ol class="breadcrumb repo-breadcrumb">
+ <li v-for="(link, i) in pathLinks" :key="i" class="breadcrumb-item">
+ <router-link :to="link.to" :aria-current="isLast(i) ? 'page' : null">
+ {{ link.name }}
+ </router-link>
+ </li>
+ </ol>
+ </nav>
+</template>
diff --git a/app/assets/javascripts/repository/components/table/header.vue b/app/assets/javascripts/repository/components/table/header.vue
new file mode 100644
index 00000000000..9d30aa88155
--- /dev/null
+++ b/app/assets/javascripts/repository/components/table/header.vue
@@ -0,0 +1,9 @@
+<template>
+ <thead>
+ <tr>
+ <th id="name" scope="col">{{ s__('ProjectFileTree|Name') }}</th>
+ <th id="last-commit" scope="col" class="d-none d-sm-table-cell">{{ __('Last commit') }}</th>
+ <th id="last-update" scope="col" class="text-right">{{ __('Last update') }}</th>
+ </tr>
+ </thead>
+</template>
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
new file mode 100644
index 00000000000..f4df98ac2ff
--- /dev/null
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -0,0 +1,144 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { sprintf, __ } from '../../../locale';
+import getRefMixin from '../../mixins/get_ref';
+import getFiles from '../../queries/getFiles.graphql';
+import getProjectPath from '../../queries/getProjectPath.graphql';
+import TableHeader from './header.vue';
+import TableRow from './row.vue';
+import ParentRow from './parent_row.vue';
+
+const PAGE_SIZE = 100;
+
+export default {
+ components: {
+ GlLoadingIcon,
+ TableHeader,
+ TableRow,
+ ParentRow,
+ },
+ mixins: [getRefMixin],
+ apollo: {
+ projectPath: {
+ query: getProjectPath,
+ },
+ },
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ projectPath: '',
+ nextPageCursor: '',
+ entries: {
+ trees: [],
+ submodules: [],
+ blobs: [],
+ },
+ isLoadingFiles: false,
+ };
+ },
+ computed: {
+ tableCaption() {
+ return sprintf(
+ __('Files, directories, and submodules in the path %{path} for commit reference %{ref}'),
+ { path: this.path, ref: this.ref },
+ );
+ },
+ showParentRow() {
+ return !this.isLoadingFiles && ['', '/'].indexOf(this.path) === -1;
+ },
+ },
+ watch: {
+ $route: function routeChange() {
+ this.entries.trees = [];
+ this.entries.submodules = [];
+ this.entries.blobs = [];
+ this.nextPageCursor = '';
+ this.fetchFiles();
+ },
+ },
+ mounted() {
+ // We need to wait for `ref` and `projectPath` to be set
+ this.$nextTick(() => this.fetchFiles());
+ },
+ methods: {
+ fetchFiles() {
+ this.isLoadingFiles = true;
+
+ return this.$apollo
+ .query({
+ query: getFiles,
+ variables: {
+ projectPath: this.projectPath,
+ ref: this.ref,
+ path: this.path,
+ nextPageCursor: this.nextPageCursor,
+ pageSize: PAGE_SIZE,
+ },
+ })
+ .then(({ data }) => {
+ if (!data) return;
+
+ const pageInfo = this.hasNextPage(data.project.repository.tree);
+
+ this.isLoadingFiles = false;
+ this.entries = Object.keys(this.entries).reduce(
+ (acc, key) => ({
+ ...acc,
+ [key]: this.normalizeData(key, data.project.repository.tree[key].edges),
+ }),
+ {},
+ );
+
+ if (pageInfo && pageInfo.hasNextPage) {
+ this.nextPageCursor = pageInfo.endCursor;
+ this.fetchFiles();
+ }
+ })
+ .catch(() => createFlash(__('An error occurding while fetching folder content.')));
+ },
+ normalizeData(key, data) {
+ return this.entries[key].concat(data.map(({ node }) => node));
+ },
+ hasNextPage(data) {
+ return []
+ .concat(data.trees.pageInfo, data.submodules.pageInfo, data.blobs.pageInfo)
+ .find(({ hasNextPage }) => hasNextPage);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="tree-content-holder">
+ <div class="table-holder bordered-box">
+ <table class="table tree-table qa-file-tree" aria-live="polite">
+ <caption class="sr-only">
+ {{
+ tableCaption
+ }}
+ </caption>
+ <table-header v-once />
+ <tbody>
+ <parent-row v-show="showParentRow" :commit-ref="ref" :path="path" />
+ <template v-for="val in entries">
+ <table-row
+ v-for="entry in val"
+ :id="entry.id"
+ :key="`${entry.flatPath}-${entry.id}`"
+ :current-path="path"
+ :path="entry.flatPath"
+ :type="entry.type"
+ />
+ </template>
+ </tbody>
+ </table>
+ <gl-loading-icon v-show="isLoadingFiles" class="my-3" size="md" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repository/components/table/parent_row.vue b/app/assets/javascripts/repository/components/table/parent_row.vue
new file mode 100644
index 00000000000..b4433f00d8a
--- /dev/null
+++ b/app/assets/javascripts/repository/components/table/parent_row.vue
@@ -0,0 +1,37 @@
+<script>
+export default {
+ props: {
+ commitRef: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ parentRoute() {
+ const splitArray = this.path.split('/');
+ splitArray.pop();
+
+ return { path: `/tree/${this.commitRef}/${splitArray.join('/')}` };
+ },
+ },
+ methods: {
+ clickRow() {
+ this.$router.push(this.parentRoute);
+ },
+ },
+};
+</script>
+
+<template>
+ <tr v-once @click="clickRow">
+ <td colspan="3" class="tree-item-file-name">
+ <router-link :to="parentRoute" :aria-label="__('Go to parent')">
+ ..
+ </router-link>
+ </td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
new file mode 100644
index 00000000000..9a264bef87e
--- /dev/null
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -0,0 +1,72 @@
+<script>
+import { getIconName } from '../../utils/icon';
+import getRefMixin from '../../mixins/get_ref';
+
+export default {
+ mixins: [getRefMixin],
+ props: {
+ id: {
+ type: String,
+ required: true,
+ },
+ currentPath: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ type: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ routerLinkTo() {
+ return this.isFolder ? { path: `/tree/${this.ref}/${this.path}` } : null;
+ },
+ iconName() {
+ return `fa-${getIconName(this.type, this.path)}`;
+ },
+ isFolder() {
+ return this.type === 'tree';
+ },
+ isSubmodule() {
+ return this.type === 'commit';
+ },
+ linkComponent() {
+ return this.isFolder ? 'router-link' : 'a';
+ },
+ fullPath() {
+ return this.path.replace(new RegExp(`^${this.currentPath}/`), '');
+ },
+ shortSha() {
+ return this.id.slice(0, 8);
+ },
+ },
+ methods: {
+ openRow() {
+ if (this.isFolder) {
+ this.$router.push(this.routerLinkTo);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <tr v-once :class="`file_${id}`" class="tree-item" @click="openRow">
+ <td class="tree-item-file-name">
+ <i :aria-label="type" role="img" :class="iconName" class="fa fa-fw"></i>
+ <component :is="linkComponent" :to="routerLinkTo" class="str-truncated">
+ {{ fullPath }}
+ </component>
+ <template v-if="isSubmodule">
+ @ <a href="#" class="commit-sha">{{ shortSha }}</a>
+ </template>
+ </td>
+ <td class="d-none d-sm-table-cell tree-commit"></td>
+ <td class="tree-time-ago text-right"></td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/repository/fragmentTypes.json b/app/assets/javascripts/repository/fragmentTypes.json
new file mode 100644
index 00000000000..949ebca432b
--- /dev/null
+++ b/app/assets/javascripts/repository/fragmentTypes.json
@@ -0,0 +1 @@
+{"__schema":{"types":[{"kind":"INTERFACE","name":"Entry","possibleTypes":[{"name":"Blob"},{"name":"Submodule"},{"name":"TreeEntry"}]}]}}
diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js
index febfcce780c..c64d16ef02a 100644
--- a/app/assets/javascripts/repository/graphql.js
+++ b/app/assets/javascripts/repository/graphql.js
@@ -1,10 +1,42 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql';
+import introspectionQueryResultData from './fragmentTypes.json';
Vue.use(VueApollo);
-const defaultClient = createDefaultClient({});
+// We create a fragment matcher so that we can create a fragment from an interface
+// Without this, Apollo throws a heuristic fragment matcher warning
+const fragmentMatcher = new IntrospectionFragmentMatcher({
+ introspectionQueryResultData,
+});
+
+const defaultClient = createDefaultClient(
+ {},
+ {
+ cacheConfig: {
+ fragmentMatcher,
+ dataIdFromObject: obj => {
+ // eslint-disable-next-line no-underscore-dangle
+ switch (obj.__typename) {
+ // We need to create a dynamic ID for each entry
+ // Each entry can have the same ID as the ID is a commit ID
+ // So we create a unique cache ID with the path and the ID
+ case 'TreeEntry':
+ case 'Submodule':
+ case 'Blob':
+ return `${obj.flatPath}-${obj.id}`;
+ default:
+ // If the type doesn't match any of the above we fallback
+ // to using the default Apollo ID
+ // eslint-disable-next-line no-underscore-dangle
+ return obj.id || obj._id;
+ }
+ },
+ },
+ },
+);
export default new VueApollo({
defaultClient,
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 00b69362312..52f53be045b 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -1,22 +1,56 @@
import Vue from 'vue';
import createRouter from './router';
import App from './components/app.vue';
+import Breadcrumbs from './components/breadcrumbs.vue';
import apolloProvider from './graphql';
+import { setTitle } from './utils/title';
export default function setupVueRepositoryList() {
const el = document.getElementById('js-tree-list');
- const { projectPath, ref } = el.dataset;
+ const { projectPath, projectShortPath, ref, fullName } = el.dataset;
+ const router = createRouter(projectPath, ref);
apolloProvider.clients.defaultClient.cache.writeData({
data: {
projectPath,
+ projectShortPath,
ref,
},
});
+ router.afterEach(({ params: { pathMatch } }) => {
+ const isRoot = pathMatch === undefined || pathMatch === '/';
+
+ setTitle(pathMatch, ref, fullName);
+
+ if (!isRoot) {
+ document
+ .querySelectorAll('.js-keep-hidden-on-navigation')
+ .forEach(elem => elem.classList.add('hidden'));
+ }
+
+ document
+ .querySelectorAll('.js-hide-on-navigation')
+ .forEach(elem => elem.classList.toggle('hidden', !isRoot));
+ });
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: document.getElementById('js-repo-breadcrumb'),
+ router,
+ apolloProvider,
+ render(h) {
+ return h(Breadcrumbs, {
+ props: {
+ currentPath: this.$route.params.pathMatch,
+ },
+ });
+ },
+ });
+
return new Vue({
el,
- router: createRouter(projectPath, ref),
+ router,
apolloProvider,
render(h) {
return h(App);
diff --git a/app/assets/javascripts/repository/mixins/get_ref.js b/app/assets/javascripts/repository/mixins/get_ref.js
new file mode 100644
index 00000000000..b06087d6f42
--- /dev/null
+++ b/app/assets/javascripts/repository/mixins/get_ref.js
@@ -0,0 +1,14 @@
+import getRef from '../queries/getRef.graphql';
+
+export default {
+ apollo: {
+ ref: {
+ query: getRef,
+ },
+ },
+ data() {
+ return {
+ ref: '',
+ };
+ },
+};
diff --git a/app/assets/javascripts/repository/pages/index.vue b/app/assets/javascripts/repository/pages/index.vue
index fdbf195f0f6..2d92e9174ca 100644
--- a/app/assets/javascripts/repository/pages/index.vue
+++ b/app/assets/javascripts/repository/pages/index.vue
@@ -1,11 +1,9 @@
<script>
-import getRef from '../queries/getRef.graphql';
+import FileTable from '../components/table/index.vue';
export default {
- apollo: {
- ref: {
- query: getRef,
- },
+ components: {
+ FileTable,
},
data() {
return {
@@ -16,9 +14,5 @@ export default {
</script>
<template>
- <div>
- <router-link :to="{ path: `/tree/${ref}/app` }">
- Go to tree
- </router-link>
- </div>
+ <file-table path="/" />
</template>
diff --git a/app/assets/javascripts/repository/pages/tree.vue b/app/assets/javascripts/repository/pages/tree.vue
index f51aafee775..3b898d1aa91 100644
--- a/app/assets/javascripts/repository/pages/tree.vue
+++ b/app/assets/javascripts/repository/pages/tree.vue
@@ -1,5 +1,10 @@
<script>
+import FileTable from '../components/table/index.vue';
+
export default {
+ components: {
+ FileTable,
+ },
props: {
path: {
type: String,
@@ -11,5 +16,5 @@ export default {
</script>
<template>
- <div>{{ path }}</div>
+ <file-table :path="path" />
</template>
diff --git a/app/assets/javascripts/repository/queries/getFiles.graphql b/app/assets/javascripts/repository/queries/getFiles.graphql
new file mode 100644
index 00000000000..a9b61d28560
--- /dev/null
+++ b/app/assets/javascripts/repository/queries/getFiles.graphql
@@ -0,0 +1,55 @@
+fragment TreeEntry on Entry {
+ id
+ flatPath
+ type
+}
+
+fragment PageInfo on PageInfo {
+ hasNextPage
+ endCursor
+}
+
+query getFiles(
+ $projectPath: ID!
+ $path: String
+ $ref: String!
+ $pageSize: Int!
+ $nextPageCursor: String
+) {
+ project(fullPath: $projectPath) {
+ repository {
+ tree(path: $path, ref: $ref) {
+ trees(first: $pageSize, after: $nextPageCursor) {
+ edges {
+ node {
+ ...TreeEntry
+ }
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ submodules(first: $pageSize, after: $nextPageCursor) {
+ edges {
+ node {
+ ...TreeEntry
+ }
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ blobs(first: $pageSize, after: $nextPageCursor) {
+ edges {
+ node {
+ ...TreeEntry
+ }
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/repository/queries/getProjectPath.graphql b/app/assets/javascripts/repository/queries/getProjectPath.graphql
new file mode 100644
index 00000000000..74e73e07577
--- /dev/null
+++ b/app/assets/javascripts/repository/queries/getProjectPath.graphql
@@ -0,0 +1,3 @@
+query getProjectPath {
+ projectPath
+}
diff --git a/app/assets/javascripts/repository/queries/getProjectShortPath.graphql b/app/assets/javascripts/repository/queries/getProjectShortPath.graphql
new file mode 100644
index 00000000000..34eb26598c2
--- /dev/null
+++ b/app/assets/javascripts/repository/queries/getProjectShortPath.graphql
@@ -0,0 +1,3 @@
+query getProjectShortPath {
+ projectShortPath @client
+}
diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js
index b42a96a4ee2..9322c81ab97 100644
--- a/app/assets/javascripts/repository/router.js
+++ b/app/assets/javascripts/repository/router.js
@@ -12,24 +12,17 @@ export default function createRouter(base, baseRef) {
base: joinPaths(gon.relative_url_root || '', base),
routes: [
{
- path: '/',
- name: 'projectRoot',
- component: IndexPage,
- },
- {
path: `/tree/${baseRef}(/.*)?`,
name: 'treePath',
component: TreePage,
props: route => ({
- path: route.params.pathMatch,
+ path: route.params.pathMatch && route.params.pathMatch.replace(/^\//, ''),
}),
- beforeEnter(to, from, next) {
- document
- .querySelectorAll('.js-hide-on-navigation')
- .forEach(el => el.classList.add('hidden'));
-
- next();
- },
+ },
+ {
+ path: '/',
+ name: 'projectRoot',
+ component: IndexPage,
},
],
});
diff --git a/app/assets/javascripts/repository/utils/icon.js b/app/assets/javascripts/repository/utils/icon.js
new file mode 100644
index 00000000000..661ebb6edfc
--- /dev/null
+++ b/app/assets/javascripts/repository/utils/icon.js
@@ -0,0 +1,99 @@
+const entryTypeIcons = {
+ tree: 'folder',
+ commit: 'archive',
+};
+
+const fileTypeIcons = [
+ { extensions: ['pdf'], name: 'file-pdf-o' },
+ {
+ extensions: [
+ 'jpg',
+ 'jpeg',
+ 'jif',
+ 'jfif',
+ 'jp2',
+ 'jpx',
+ 'j2k',
+ 'j2c',
+ 'png',
+ 'gif',
+ 'tif',
+ 'tiff',
+ 'svg',
+ 'ico',
+ 'bmp',
+ ],
+ name: 'file-image-o',
+ },
+ {
+ extensions: ['zip', 'zipx', 'tar', 'gz', 'bz', 'bzip', 'xz', 'rar', '7z'],
+ name: 'file-archive-o',
+ },
+ { extensions: ['mp3', 'wma', 'ogg', 'oga', 'wav', 'flac', 'aac'], name: 'file-audio-o' },
+ {
+ extensions: [
+ 'mp4',
+ 'm4p',
+ 'm4v',
+ 'mpg',
+ 'mp2',
+ 'mpeg',
+ 'mpe',
+ 'mpv',
+ 'm2v',
+ 'avi',
+ 'mkv',
+ 'flv',
+ 'ogv',
+ 'mov',
+ '3gp',
+ '3g2',
+ ],
+ name: 'file-video-o',
+ },
+ { extensions: ['doc', 'dot', 'docx', 'docm', 'dotx', 'dotm', 'docb'], name: 'file-word-o' },
+ {
+ extensions: [
+ 'xls',
+ 'xlt',
+ 'xlm',
+ 'xlsx',
+ 'xlsm',
+ 'xltx',
+ 'xltm',
+ 'xlsb',
+ 'xla',
+ 'xlam',
+ 'xll',
+ 'xlw',
+ ],
+ name: 'file-excel-o',
+ },
+ {
+ extensions: [
+ 'ppt',
+ 'pot',
+ 'pps',
+ 'pptx',
+ 'pptm',
+ 'potx',
+ 'potm',
+ 'ppam',
+ 'ppsx',
+ 'ppsm',
+ 'sldx',
+ 'sldm',
+ ],
+ name: 'file-powerpoint-o',
+ },
+];
+
+// eslint-disable-next-line import/prefer-default-export
+export const getIconName = (type, path) => {
+ if (entryTypeIcons[type]) return entryTypeIcons[type];
+
+ const extension = path.split('.').pop();
+ const file = fileTypeIcons.find(t => t.extensions.some(ext => ext === extension));
+
+ return file ? file.name : 'file-text-o';
+};
diff --git a/app/assets/javascripts/repository/utils/title.js b/app/assets/javascripts/repository/utils/title.js
new file mode 100644
index 00000000000..4e194640e92
--- /dev/null
+++ b/app/assets/javascripts/repository/utils/title.js
@@ -0,0 +1,9 @@
+// eslint-disable-next-line import/prefer-default-export
+export const setTitle = (pathMatch, ref, project) => {
+ if (!pathMatch) return;
+
+ const path = pathMatch.replace(/^\//, '');
+ const isEmpty = path === '';
+
+ document.title = `${isEmpty ? 'Files' : path} · ${ref} · ${project}`;
+};
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 9a0cdc02952..72e061df573 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -5,7 +5,7 @@ import _ from 'underscore';
import Cookies from 'js-cookie';
import flash from './flash';
import axios from './lib/utils/axios_utils';
-import { __ } from './locale';
+import { sprintf, s__, __ } from './locale';
function Sidebar(currentUser) {
this.toggleTodo = this.toggleTodo.bind(this);
@@ -101,7 +101,10 @@ Sidebar.prototype.toggleTodo = function(e) {
this.todoUpdateDone(data);
})
.catch(() =>
- flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`),
+ flash(sprintf(__('There was an error %{message} todo.')), {
+ message:
+ ajaxType === 'post' ? s__('RightSidebar|adding a') : s__('RightSidebar|deleting the'),
+ }),
);
};
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 0a4583b5861..ab43c2139bf 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -2,7 +2,7 @@
import $ from 'jquery';
import { escape, throttle } from 'underscore';
-import { s__, sprintf } from '~/locale';
+import { s__, __, sprintf } from '~/locale';
import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
import axios from './lib/utils/axios_utils';
import DropdownUtils from './filtered_search/dropdown_utils';
@@ -407,6 +407,7 @@ export class SearchAutocomplete {
if (this.searchInput.val() === '') {
return this.restoreOriginalState();
}
+ this.dropdownMenu.removeClass('show');
}
restoreOriginalState() {
@@ -439,7 +440,7 @@ export class SearchAutocomplete {
restoreMenu() {
var html;
- html = '<ul><li class="dropdown-menu-empty-item"><a>Loading...</a></li></ul>';
+ html = `<ul><li class="dropdown-menu-empty-item"><a>${__('Loading...')}</a></li></ul>`;
return this.dropdownContent.html(html);
}
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index 10e2c8453e2..35eba266625 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -194,9 +194,9 @@ export default {
v-show="noEmoji"
class="js-no-emoji-placeholder no-emoji-placeholder position-relative"
>
- <icon name="emoji_slightly_smiling_face" css-classes="award-control-icon-neutral" />
- <icon name="emoji_smiley" css-classes="award-control-icon-positive" />
- <icon name="emoji_smile" css-classes="award-control-icon-super-positive" />
+ <icon name="slight-smile" css-classes="award-control-icon-neutral" />
+ <icon name="smiley" css-classes="award-control-icon-positive" />
+ <icon name="smile" css-classes="award-control-icon-super-positive" />
</span>
</button>
</span>
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index 7404dfbf22a..70f89152f70 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -31,7 +31,7 @@ export default class Star {
$this.prepend(spriteIcon('star', iconClasses));
}
})
- .catch(() => Flash('Star toggle failed. Try again later.'));
+ .catch(() => Flash(__('Star toggle failed. Try again later.')));
});
}
}
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js
index ebe1c6dd02d..7206bbd7109 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/subscription_select.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { __ } from './locale';
export default function subscriptionSelect() {
$('.js-subscription-event').each((i, element) => {
@@ -8,7 +9,7 @@ export default function subscriptionSelect() {
selectable: true,
fieldName,
toggleLabel(selected, el, instance) {
- let label = 'Subscription';
+ let label = __('Subscription');
const $item = instance.dropdown.find('.is-active');
if ($item.length) {
label = $item.text();
diff --git a/app/assets/javascripts/usage_ping_consent.js b/app/assets/javascripts/usage_ping_consent.js
index d3d745a3c11..1e7a5fb19c2 100644
--- a/app/assets/javascripts/usage_ping_consent.js
+++ b/app/assets/javascripts/usage_ping_consent.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import Flash, { hideFlash } from './flash';
import { parseBoolean } from './lib/utils/common_utils';
+import { __ } from './locale';
export default () => {
$('body').on('click', '.js-usage-consent-action', e => {
@@ -25,7 +26,7 @@ export default () => {
})
.catch(() => {
hideConsentMessage();
- Flash('Something went wrong. Try again later.');
+ Flash(__('Something went wrong. Try again later.'));
});
});
};
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 8c71615dff2..7e6f02b10af 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -5,7 +5,7 @@
import $ from 'jquery';
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
-import { __ } from './locale';
+import { s__, __, sprintf } from './locale';
import ModalStore from './boards/stores/modal_store';
// TODO: remove eventHub hack after code splitting refactor
@@ -157,14 +157,20 @@ function UsersSelect(currentUser, els, options = {}) {
.get(0);
if (selectedUsers.length === 0) {
- return 'Unassigned';
+ return s__('UsersSelect|Unassigned');
} else if (selectedUsers.length === 1) {
return firstUser.name;
} else if (isSelected) {
const otherSelected = selectedUsers.filter(s => s !== selectedUser.id);
- return `${selectedUser.name} + ${otherSelected.length} more`;
+ return sprintf(s__('UsersSelect|%{name} + %{length} more'), {
+ name: selectedUser.name,
+ length: otherSelected.length,
+ });
} else {
- return `${firstUser.name} + ${selectedUsers.length - 1} more`;
+ return sprintf(s__('UsersSelect|%{name} + %{length} more'), {
+ name: firstUser.name,
+ length: selectedUsers.length - 1,
+ });
}
};
@@ -218,11 +224,11 @@ function UsersSelect(currentUser, els, options = {}) {
tooltipTitle = _.escape(user.name);
} else {
user = {
- name: 'Unassigned',
+ name: s__('UsersSelect|Unassigned'),
username: '',
avatar: '',
};
- tooltipTitle = __('Assignee');
+ tooltipTitle = s__('UsersSelect|Assignee');
}
$value.html(assigneeTemplate(user));
$collapsedSidebar.attr('title', tooltipTitle).tooltip('_fixTitle');
@@ -233,7 +239,11 @@ function UsersSelect(currentUser, els, options = {}) {
'<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>',
);
assigneeTemplate = _.template(
- '<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>',
+ `<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself">
+ ${sprintf(s__('UsersSelect|No assignee - %{openingTag} assign yourself %{closingTag}'), {
+ openingTag: '<a href="#" class="js-assign-yourself">',
+ closingTag: '</a>',
+ })}</span> <% } %>`,
);
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
@@ -302,7 +312,7 @@ function UsersSelect(currentUser, els, options = {}) {
showDivider += 1;
users.unshift({
beforeDivider: true,
- name: 'Unassigned',
+ name: s__('UsersSelect|Unassigned'),
id: 0,
});
}
@@ -310,7 +320,7 @@ function UsersSelect(currentUser, els, options = {}) {
showDivider += 1;
name = showAnyUser;
if (name === true) {
- name = 'Any User';
+ name = s__('UsersSelect|Any User');
}
anyUser = {
beforeDivider: true,
@@ -596,7 +606,7 @@ function UsersSelect(currentUser, els, options = {}) {
showEmailUser = $(select).data('emailUser');
firstUser = $(select).data('firstUser');
return $(select).select2({
- placeholder: 'Search for a user',
+ placeholder: __('Search for a user'),
multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0,
query: function(query) {
@@ -621,7 +631,7 @@ function UsersSelect(currentUser, els, options = {}) {
}
if (showNullUser) {
nullUser = {
- name: 'Unassigned',
+ name: s__('UsersSelect|Unassigned'),
id: 0,
};
data.results.unshift(nullUser);
@@ -629,7 +639,7 @@ function UsersSelect(currentUser, els, options = {}) {
if (showAnyUser) {
name = showAnyUser;
if (name === true) {
- name = 'Any User';
+ name = s__('UsersSelect|Any User');
}
anyUser = {
name: name,
@@ -645,7 +655,7 @@ function UsersSelect(currentUser, els, options = {}) {
) {
var trimmed = query.term.trim();
emailUser = {
- name: 'Invite "' + trimmed + '" by email',
+ name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }),
username: trimmed,
id: trimmed,
invite: true,
@@ -688,7 +698,7 @@ UsersSelect.prototype.initSelection = function(element, callback) {
id = $(element).val();
if (id === '0') {
nullUser = {
- name: 'Unassigned',
+ name: s__('UsersSelect|Unassigned'),
};
return callback(nullUser);
} else if (id !== '') {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index 8f4cae8ae58..ad0464a3a98 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -49,7 +49,7 @@ export default {
required: false,
default: () => ({
sourceProjectId: '',
- issueId: '',
+ mergeRequestId: '',
appUrl: '',
}),
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index b9f5f602117..0686409a785 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -48,7 +48,7 @@ export default {
visualReviewAppMeta() {
return {
appUrl: this.mr.appUrl,
- issueId: this.mr.iid,
+ mergeRequestId: this.mr.iid,
sourceProjectId: this.mr.sourceProjectId,
};
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
index 780ecdcdac4..6aad2a26a53 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
@@ -14,7 +14,7 @@ export default {
</script>
<template>
- <p v-once class="mr-info-list mr-links source-branch-removal-status append-bottom-0">
+ <p v-once class="mr-info-list mr-links append-bottom-0">
<span class="status-text" v-html="removesBranchText"> </span>
<i v-tooltip :title="tooltipTitle" :aria-label="tooltipTitle" class="fa fa-question-circle">
</i>
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 705ee05e29f..bf175eb5f69 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -333,41 +333,45 @@ export default {
<div class="mr-widget-section">
<component :is="componentName" :mr="mr" :service="service" />
- <section v-if="shouldRenderCollaborationStatus" class="mr-info-list mr-links">
- {{ s__('mrWidget|Allows commits from members who can merge to the target branch') }}
- </section>
+ <div class="mr-widget-info">
+ <section v-if="shouldRenderCollaborationStatus" class="mr-info-list mr-links">
+ <p>
+ {{ s__('mrWidget|Allows commits from members who can merge to the target branch') }}
+ </p>
+ </section>
- <mr-widget-related-links
- v-if="shouldRenderRelatedLinks"
- :state="mr.state"
- :related-links="mr.relatedLinks"
- />
+ <mr-widget-related-links
+ v-if="shouldRenderRelatedLinks"
+ :state="mr.state"
+ :related-links="mr.relatedLinks"
+ />
- <mr-widget-alert-message
- v-if="showMergePipelineForkWarning"
- type="warning"
- :help-path="mr.mergeRequestPipelinesHelpPath"
- >
- {{
- s__(
- 'mrWidget|Fork merge requests do not create merge request pipelines which validate a post merge result',
- )
- }}
- </mr-widget-alert-message>
+ <mr-widget-alert-message
+ v-if="showMergePipelineForkWarning"
+ type="warning"
+ :help-path="mr.mergeRequestPipelinesHelpPath"
+ >
+ {{
+ s__(
+ 'mrWidget|Fork merge requests do not create merge request pipelines which validate a post merge result',
+ )
+ }}
+ </mr-widget-alert-message>
- <mr-widget-alert-message
- v-if="showTargetBranchAdvancedError"
- type="danger"
- :help-path="mr.mergeRequestPipelinesHelpPath"
- >
- {{
- s__(
- 'mrWidget|The target branch has advanced, which invalidates the merge request pipeline. Please update the source branch and retry merging',
- )
- }}
- </mr-widget-alert-message>
+ <mr-widget-alert-message
+ v-if="showTargetBranchAdvancedError"
+ type="danger"
+ :help-path="mr.mergeRequestPipelinesHelpPath"
+ >
+ {{
+ s__(
+ 'mrWidget|The target branch has advanced, which invalidates the merge request pipeline. Please update the source branch and retry merging',
+ )
+ }}
+ </mr-widget-alert-message>
- <source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" />
+ <source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" />
+ </div>
</div>
<div v-if="shouldRenderMergeHelp" class="mr-widget-footer"><mr-widget-merge-help /></div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index 4dbfd1ba6f4..a60d5eb491e 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -71,11 +71,15 @@ export default {
</div>
<div class="text-secondary">
<div v-if="user.bio" class="js-bio d-flex mb-1">
- <icon name="profile" css-classes="category-icon" />
+ <icon name="profile" css-classes="category-icon flex-shrink-0" />
<span class="ml-1">{{ user.bio }}</span>
</div>
<div v-if="user.organization" class="js-organization d-flex mb-1">
- <icon v-show="!jobInfoIsLoading" name="work" css-classes="category-icon" />
+ <icon
+ v-show="!jobInfoIsLoading"
+ name="work"
+ css-classes="category-icon flex-shrink-0"
+ />
<span class="ml-1">{{ user.organization }}</span>
</div>
<gl-skeleton-loading
@@ -88,7 +92,7 @@ export default {
<icon
v-show="!locationIsLoading && user.location"
name="location"
- css-classes="category-icon"
+ css-classes="category-icon flex-shrink-0"
/>
<span class="ml-1">{{ user.location }}</span>
<gl-skeleton-loading
diff --git a/app/assets/stylesheets/components/avatar.scss b/app/assets/stylesheets/components/avatar.scss
new file mode 100644
index 00000000000..25ee3ca944d
--- /dev/null
+++ b/app/assets/stylesheets/components/avatar.scss
@@ -0,0 +1,232 @@
+$avatar-sizes: (
+ 16: (
+ font-size: 10px,
+ line-height: 16px,
+ border-radius: $border-radius-small
+ ),
+ 18: (
+ border-radius: $border-radius-small
+ ),
+ 19: (
+ border-radius: $border-radius-small
+ ),
+ 20: (
+ border-radius: $border-radius-small
+ ),
+ 24: (
+ font-size: 12px,
+ line-height: 24px,
+ border-radius: $border-radius-default
+ ),
+ 26: (
+ font-size: 20px,
+ line-height: 1.33,
+ border-radius: $border-radius-default
+ ),
+ 32: (
+ font-size: 14px,
+ line-height: 32px,
+ border-radius: $border-radius-default
+ ),
+ 36: (
+ border-radius: $border-radius-default
+ ),
+ 40: (
+ font-size: 16px,
+ line-height: 38px,
+ border-radius: $border-radius-default
+ ),
+ 46: (
+ border-radius: $border-radius-default
+ ),
+ 48: (
+ font-size: 20px,
+ line-height: 48px,
+ border-radius: $border-radius-large
+ ),
+ 60: (
+ font-size: 32px,
+ line-height: 58px,
+ border-radius: $border-radius-large
+ ),
+ 64: (
+ font-size: 28px,
+ line-height: 64px,
+ border-radius: $border-radius-large
+ ),
+ 70: (
+ font-size: 34px,
+ line-height: 70px,
+ border-radius: $border-radius-large
+ ),
+ 90: (
+ font-size: 36px,
+ line-height: 88px,
+ border-radius: $border-radius-large
+ ),
+ 96: (
+ font-size: 48px,
+ line-height: 96px,
+ border-radius: $border-radius-large
+ ),
+ 100: (
+ font-size: 36px,
+ line-height: 98px,
+ border-radius: $border-radius-large
+ ),
+ 110: (
+ font-size: 40px,
+ line-height: 108px,
+ font-weight: $gl-font-weight-normal,
+ border-radius: $border-radius-large
+ ),
+ 140: (
+ font-size: 72px,
+ line-height: 138px,
+ border-radius: $border-radius-large
+ ),
+ 160: (
+ font-size: 96px,
+ line-height: 158px,
+ border-radius: $border-radius-large
+ )
+);
+
+$identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $identicon-blue, $identicon-teal,
+ $identicon-orange, $gray-darker;
+
+.avatar-circle {
+ float: left;
+ margin-right: 15px;
+ border-radius: $avatar-radius;
+ border: 1px solid $gray-normal;
+
+ @each $size, $size-config in $avatar-sizes {
+ &.s#{$size} {
+ @include avatar-size(#{$size}px, if($size < 36, 8px, 16px));
+ }
+ }
+}
+
+.avatar {
+ @extend .avatar-circle;
+ transition-property: none;
+
+ width: 40px;
+ height: 40px;
+ padding: 0;
+ background: $gray-lightest;
+ overflow: hidden;
+ border-color: rgba($black, $gl-avatar-border-opacity);
+
+ &.avatar-inline {
+ float: none;
+ display: inline-block;
+ margin-left: 2px;
+ flex-shrink: 0;
+
+ &.s16 {
+ margin-right: 4px;
+ }
+
+ &.s24 {
+ margin-right: 4px;
+ }
+ }
+
+ &.center {
+ font-size: 14px;
+ line-height: 1.8em;
+ text-align: center;
+ }
+
+ &.avatar-tile {
+ border-radius: 0;
+ border: 0;
+ }
+
+ &.avatar-placeholder {
+ border: 0;
+ }
+}
+
+.identicon {
+ text-align: center;
+ vertical-align: top;
+ color: $gray-800;
+ background-color: $gray-darker;
+
+ // Sizes
+ @each $size, $size-config in $avatar-sizes {
+ $keys: map-keys($size-config);
+
+ &.s#{$size} {
+ @each $key in $keys {
+ // We don't want `border-radius` to be included here.
+ @if ($key != 'border-radius') {
+ #{$key}: map-get($size-config, #{$key});
+ }
+ }
+ }
+ }
+
+ // Background colors
+ @for $i from 1 through length($identicon-backgrounds) {
+ &.bg#{$i} {
+ background-color: nth($identicon-backgrounds, $i);
+ }
+ }
+}
+
+.avatar-container {
+ @extend .avatar-circle;
+ overflow: hidden;
+ display: flex;
+
+ a {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ text-decoration: none;
+ }
+
+ .avatar {
+ border-radius: 0;
+ border: 0;
+ height: auto;
+ width: 100%;
+ margin: 0;
+ align-self: center;
+ }
+
+ &.s40 {
+ min-width: 40px;
+ min-height: 40px;
+ }
+
+ &.s64 {
+ min-width: 64px;
+ min-height: 64px;
+ }
+}
+
+.rect-avatar {
+ border-radius: $border-radius-small;
+
+ @each $size, $size-config in $avatar-sizes {
+ &.s#{$size} {
+ border-radius: map-get($size-config, 'border-radius');
+ }
+ }
+}
+
+.avatar-counter {
+ background-color: $gray-darkest;
+ color: $white-light;
+ border: 1px solid $gray-normal;
+ border-radius: 1em;
+ font-family: $regular-font;
+ font-size: 9px;
+ line-height: 16px;
+ text-align: center;
+}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index ab9047c54e4..9b0d19b0ef0 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -8,7 +8,6 @@
@import 'framework/animations';
@import 'framework/vue_transitions';
-@import 'framework/avatar';
@import 'framework/asciidoctor';
@import 'framework/banner';
@import 'framework/blocks';
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
deleted file mode 100644
index 37a729c7a63..00000000000
--- a/app/assets/stylesheets/framework/avatar.scss
+++ /dev/null
@@ -1,194 +0,0 @@
-@mixin avatar-size($size, $margin-right) {
- width: $size;
- height: $size;
- margin-right: $margin-right;
-}
-
-.avatar-circle {
- float: left;
- margin-right: 15px;
- border-radius: $avatar-radius;
- border: 1px solid $gray-normal;
- &.s16 { @include avatar-size(16px, 6px); }
- &.s18 { @include avatar-size(18px, 6px); }
- &.s19 { @include avatar-size(19px, 6px); }
- &.s20 { @include avatar-size(20px, 7px); }
- &.s24 { @include avatar-size(24px, 8px); }
- &.s26 { @include avatar-size(26px, 8px); }
- &.s32 { @include avatar-size(32px, 10px); }
- &.s36 { @include avatar-size(36px, 10px); }
- &.s40 { @include avatar-size(40px, 10px); }
- &.s46 { @include avatar-size(46px, 15px); }
- &.s48 { @include avatar-size(48px, 10px); }
- &.s60 { @include avatar-size(60px, 12px); }
- &.s64 { @include avatar-size(64px, 14px); }
- &.s70 { @include avatar-size(70px, 14px); }
- &.s90 { @include avatar-size(90px, 15px); }
- &.s100 { @include avatar-size(100px, 15px); }
- &.s110 { @include avatar-size(110px, 15px); }
- &.s140 { @include avatar-size(140px, 15px); }
- &.s160 { @include avatar-size(160px, 20px); }
-}
-
-.avatar {
- @extend .avatar-circle;
- transition-property: none;
-
- width: 40px;
- height: 40px;
- padding: 0;
- background: $gray-lightest;
- overflow: hidden;
-
- &.avatar-inline {
- float: none;
- display: inline-block;
- margin-left: 2px;
- flex-shrink: 0;
-
- &.s16 { margin-right: 4px; }
- &.s24 { margin-right: 4px; }
- }
-
- &.center {
- font-size: 14px;
- line-height: 1.8em;
- text-align: center;
- }
-
- &.avatar-tile {
- border-radius: 0;
- border: 0;
- }
-
- &.avatar-placeholder {
- border: 0;
- }
-
- &:not([href]):hover {
- border-color: darken($gray-normal, 10%);
- }
-}
-
-.identicon {
- text-align: center;
- vertical-align: top;
- color: $gl-gray-700;
- background-color: $gray-darker;
-
- // Sizes
- &.s16 { font-size: 12px;
- line-height: 1.33; }
-
- &.s24 { font-size: 13px;
- line-height: 1.8; }
-
- &.s26 { font-size: 20px;
- line-height: 1.33; }
-
- &.s32 { font-size: 20px;
- line-height: 30px; }
-
- &.s40 { font-size: 16px;
- line-height: 38px; }
-
- &.s48 { font-size: 20px;
- line-height: 46px; }
-
- &.s60 { font-size: 32px;
- line-height: 58px; }
-
- &.s64 { font-size: 32px;
- line-height: 64px; }
-
- &.s70 { font-size: 34px;
- line-height: 70px; }
-
- &.s90 { font-size: 36px;
- line-height: 88px; }
-
- &.s100 { font-size: 36px;
- line-height: 98px; }
-
- &.s110 { font-size: 40px;
- line-height: 108px;
- font-weight: $gl-font-weight-normal; }
-
- &.s140 { font-size: 72px;
- line-height: 138px; }
-
- &.s160 { font-size: 96px;
- line-height: 158px; }
-
- // Background colors
- &.bg1 { background-color: $identicon-red; }
- &.bg2 { background-color: $identicon-purple; }
- &.bg3 { background-color: $identicon-indigo; }
- &.bg4 { background-color: $identicon-blue; }
- &.bg5 { background-color: $identicon-teal; }
- &.bg6 { background-color: $identicon-orange; }
- &.bg7 { background-color: $gray-darker; }
-}
-
-.avatar-container {
- @extend .avatar-circle;
- overflow: hidden;
- display: flex;
-
- a {
- width: 100%;
- height: 100%;
- display: flex;
- text-decoration: none;
- }
-
- .avatar {
- border-radius: 0;
- border: 0;
- height: auto;
- width: 100%;
- margin: 0;
- align-self: center;
- }
-
- &.s40 { min-width: 40px;
- min-height: 40px; }
-
- &.s64 { min-width: 64px;
- min-height: 64px; }
-}
-
-.rect-avatar {
- border-radius: $border-radius-small;
- &.s16 { border-radius: $border-radius-small; }
- &.s18 { border-radius: $border-radius-small; }
- &.s19 { border-radius: $border-radius-small; }
- &.s20 { border-radius: $border-radius-small; }
- &.s24 { border-radius: $border-radius-default; }
- &.s26 { border-radius: $border-radius-default; }
- &.s32 { border-radius: $border-radius-default; }
- &.s36 { border-radius: $border-radius-default; }
- &.s40 { border-radius: $border-radius-default; }
- &.s46 { border-radius: $border-radius-default; }
- &.s48 { border-radius: $border-radius-large; }
- &.s60 { border-radius: $border-radius-large; }
- &.s64 { border-radius: $border-radius-large; }
- &.s70 { border-radius: $border-radius-large; }
- &.s90 { border-radius: $border-radius-large; }
- &.s96 { border-radius: $border-radius-large; }
- &.s100 { border-radius: $border-radius-large; }
- &.s110 { border-radius: $border-radius-large; }
- &.s140 { border-radius: $border-radius-large; }
- &.s160 { border-radius: $border-radius-large; }
-}
-
-.avatar-counter {
- background-color: $gray-darkest;
- color: $white-light;
- border: 1px solid $gray-normal;
- border-radius: 1em;
- font-family: $regular-font;
- font-size: 9px;
- line-height: 16px;
- text-align: center;
-}
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 648e1944388..a8df7e1bfad 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -151,8 +151,7 @@
outline: 0;
.award-control-icon svg {
- background: $award-emoji-positive-add-bg;
- fill: $award-emoji-positive-add-lines;
+ fill: $blue-500;
}
.award-control-icon-neutral {
@@ -236,7 +235,7 @@
}
path {
- fill: $border-gray-normal;
+ fill: $gray-700;
}
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index ab8f397f3a0..2c720703822 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -21,7 +21,7 @@
}
@mixin btn-default {
- border-radius: 3px;
+ border-radius: $border-radius-default;
font-size: $gl-font-size;
font-weight: $gl-font-weight-normal;
padding: $gl-vert-padding $gl-btn-padding;
@@ -37,7 +37,7 @@
@include btn-default;
}
-@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border, $active-background, $active-border) {
+@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border, $active-background, $active-border, $active-text) {
background-color: $background;
color: $text;
border-color: $border;
@@ -61,13 +61,22 @@
}
}
+ &:focus {
+ box-shadow: 0 0 4px 1px $blue-300;
+ }
+
&:active {
background-color: $active-background;
border-color: $active-border;
- color: $hover-text;
+ box-shadow: inset 0 2px 4px 0 rgba($black, 0.2);
+ color: $active-text;
> .icon {
- color: $hover-text;
+ color: $active-text;
+ }
+
+ &:focus {
+ box-shadow: inset 0 2px 4px 0 rgba($black, 0.2);
}
}
}
@@ -164,21 +173,21 @@
&.btn-inverted {
&.btn-success {
- @include btn-outline($white-light, $green-600, $green-500, $green-500, $white-light, $green-600, $green-600, $green-700);
+ @include btn-outline($white-light, $green-600, $green-500, $green-100, $green-700, $green-500, $green-200, $green-600, $green-800);
}
&.btn-remove,
&.btn-danger {
- @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700);
+ @include btn-outline($white-light, $red-500, $red-500, $red-100, $red-700, $red-500, $red-200, $red-600, $red-800);
}
&.btn-warning {
- @include btn-outline($white-light, $orange-500, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700);
+ @include btn-outline($white-light, $orange-500, $orange-500, $orange-100, $orange-700, $orange-500, $orange-200, $orange-600, $orange-800);
}
&.btn-primary,
&.btn-info {
- @include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700);
+ @include btn-outline($white-light, $blue-500, $blue-500, $blue-100, $blue-700, $blue-500, $blue-200, $blue-600, $blue-800);
}
}
@@ -193,11 +202,11 @@
&.btn-close,
&.btn-close-color {
- @include btn-outline($white-light, $orange-600, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700);
+ @include btn-outline($white-light, $orange-600, $orange-500, $orange-100, $orange-700, $orange-500, $orange-200, $orange-600, $orange-800);
}
&.btn-spam {
- @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700);
+ @include btn-outline($white-light, $red-500, $red-500, $red-100, $red-700, $red-500, $red-200, $red-600, $red-800);
}
&.btn-danger,
@@ -402,7 +411,7 @@
.btn-inverted {
&-secondary {
- @include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700);
+ @include btn-outline($white-light, $blue-500, $blue-500, $blue-100, $blue-700, $blue-500, $blue-200, $blue-600, $blue-800);
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 8fb4027bf97..cd951f67293 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -570,10 +570,10 @@
}
.dropdown-menu-close {
- right: 5px;
+ top: $gl-padding-4;
+ right: $gl-padding-8;
width: 20px;
height: 20px;
- top: -1px;
}
.dropdown-menu-close-icon {
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 53d3645cd63..17c117188b3 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -241,6 +241,7 @@
*/
&.code {
padding: 0;
+ border-radius: 0 0 $border-radius-default $border-radius-default;
}
.list-inline.previews {
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 5bcfd5d1322..26cbb7f5c13 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -218,7 +218,7 @@
min-width: 200px;
padding-right: 25px;
padding-left: 0;
- height: $input-height;
+ height: $input-height - 2;
line-height: inherit;
border-color: transparent;
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index d0f99c2df7e..4a9c73a1bc9 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -280,3 +280,7 @@ label {
max-width: $input-lg-width;
width: 100%;
}
+
+.input-group-text {
+ max-height: $input-height;
+}
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 946f575ac13..741f92110c3 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -8,7 +8,7 @@
pre {
padding: 10px 0;
border: 0;
- border-radius: 0;
+ border-radius: 0 0 $border-radius-default $border-radius-default;
font-family: $monospace-font;
font-size: $code-font-size;
line-height: 19px;
@@ -42,6 +42,7 @@
padding: 10px;
text-align: right;
float: left;
+ border-bottom-left-radius: $border-radius-default;
a {
font-family: $monospace-font;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 18eb10c1f23..18671f7c4d8 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -325,8 +325,8 @@
line-height: 1;
padding: 0;
min-width: 16px;
- color: $gray-darkest;
- fill: $gray-darkest;
+ color: $gray-600;
+ fill: $gray-600;
.fa {
position: relative;
@@ -376,3 +376,12 @@
}
}
}
+
+/*
+* Mixin that handles the size and right margin of avatars.
+*/
+@mixin avatar-size($size, $margin-right) {
+ width: $size;
+ height: $size;
+ margin-right: $margin-right;
+}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index e2bb1eb67c0..f75e5b55506 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -53,7 +53,8 @@
flex-direction: row;
.btn + .btn:not(.dropdown-toggle-split),
- .btn + .btn-group {
+ .btn + .btn-group,
+ .btn-group + .btn {
margin-left: $grid-size;
}
@@ -61,7 +62,8 @@
flex-direction: column;
.btn + .btn:not(.dropdown-toggle-split),
- .btn + .btn-group {
+ .btn + .btn-group,
+ .btn-group + .btn {
margin-left: 0;
margin-top: $grid-size;
}
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 3d5208c3db5..e8176e59c19 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -55,4 +55,5 @@
.discussion .timeline-entry {
margin: 0;
border-right: 0;
+ border-radius: $border-radius-default $border-radius-default 0 0;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 1cf122102cc..28768bdf88f 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -589,6 +589,7 @@ $issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-boards
*/
$avatar-radius: 50%;
$gl-avatar-size: 40px;
+$gl-avatar-border-opacity: 0.1;
/*
* Blame
diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss
index fb4d3f23cd9..ea96381a098 100644
--- a/app/assets/stylesheets/framework/variables_overrides.scss
+++ b/app/assets/stylesheets/framework/variables_overrides.scss
@@ -7,6 +7,7 @@ $secondary: $gray-light;
$input-disabled-bg: $gray-light;
$input-border-color: $gray-200;
$input-color: $gl-text-color;
+$input-font-size: $gl-font-size;
$font-family-sans-serif: $regular-font;
$font-family-monospace: $monospace-font;
$btn-line-height: 20px;
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 809ba6d4953..255383d89c8 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -69,6 +69,8 @@
align-self: flex-start;
font-weight: 500;
font-size: 20px;
+ color: $orange-900;
+ opacity: 1;
margin: $gl-padding-8 14px 0 0;
}
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index cb5f1a84005..c386493231c 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -21,7 +21,6 @@
.detail-page-header-body {
position: relative;
- line-height: 35px;
display: flex;
flex: 1 1;
min-width: 0;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index b2b3720fdde..b3a634e23a3 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -494,6 +494,12 @@ table.code {
}
}
+ .line_holder:last-of-type {
+ td:first-child {
+ border-bottom-left-radius: $border-radius-default;
+ }
+ }
+
&.left-side-selected {
td.line_content.parallel.right-side {
user-select: none;
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 618f23d81b1..e34628002ac 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -156,6 +156,10 @@
&:hover {
background: none;
}
+
+ a {
+ color: $blue-600;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 22a515cbdaa..297f642681b 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -21,13 +21,6 @@
color: $login-brand-holder-color;
}
- h1:first-child {
- font-weight: $gl-font-weight-normal;
- margin-bottom: 0.68em;
- margin-top: 0;
- font-size: 34px;
- }
-
h3 {
font-size: 22px;
}
@@ -49,8 +42,8 @@
.login-box,
.omniauth-container {
box-shadow: 0 0 0 1px $border-color;
- border-bottom-right-radius: $border-radius-small;
- border-bottom-left-radius: $border-radius-small;
+ border-bottom-right-radius: $border-radius;
+ border-bottom-left-radius: $border-radius;
padding: 15px;
.login-heading h3 {
@@ -95,7 +88,7 @@
}
.omniauth-container {
- border-radius: $border-radius-small;
+ border-radius: $border-radius;
font-size: 13px;
p {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 44b558dd5ff..ab5a9e170f0 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -87,6 +87,11 @@
padding: $gl-padding;
}
+.mr-widget-info {
+ padding-left: $gl-padding-50 - $gl-padding-32;
+ padding-right: $gl-padding;
+}
+
.mr-state-widget {
color: $gl-text-color;
@@ -560,6 +565,10 @@
.mr-links {
padding-left: $status-icon-size + $gl-btn-padding;
+
+ &:last-child {
+ padding-bottom: $gl-padding;
+ }
}
.mr-info-list {
@@ -1030,11 +1039,6 @@
background: $black-transparent;
}
-.source-branch-removal-status {
- padding-left: 50px;
- padding-bottom: $gl-padding;
-}
-
.mr-compare {
.diff-file .file-title-flex-parent {
top: $header-height + 51px;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index f2b67a693c3..50c87e55f56 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -107,6 +107,7 @@ $note-form-margin-left: 72px;
&.collapsed {
color: $gl-text-color-secondary;
+ border-radius: 0 0 $border-radius-default $border-radius-default;
svg {
float: left;
@@ -644,7 +645,7 @@ $note-form-margin-left: 72px;
display: inline-flex;
align-items: center;
margin-left: 10px;
- color: $gray-darkest;
+ color: $gray-600;
@include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
float: none;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 37071a57bb3..dbf600df9d6 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -261,3 +261,13 @@ input[type='checkbox']:hover {
color: $blue-600;
}
}
+
+// Disable webkit input icons, link to solution: https://stackoverflow.com/questions/9421551/how-do-i-remove-all-default-webkit-search-field-styling
+/* stylelint-disable property-no-vendor-prefix */
+input[type='search']::-webkit-search-decoration,
+input[type='search']::-webkit-search-cancel-button,
+input[type='search']::-webkit-search-results-button,
+input[type='search']::-webkit-search-results-decoration {
+ -webkit-appearance: none;
+}
+/* stylelint-enable */