diff options
Diffstat (limited to 'app/assets/javascripts')
29 files changed, 571 insertions, 186 deletions
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index a8dafd31f12..9c4cc2338c8 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -2,7 +2,7 @@ import Sortable from 'vendor/Sortable'; import Vue from 'vue'; import AccessorUtilities from '../../lib/utils/accessor'; -import boardList from './board_list'; +import boardList from './board_list.vue'; import boardBlankState from './board_blank_state'; import './board_delete'; diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.vue index 591f1dc8313..9a0442e2afe 100644 --- a/app/assets/javascripts/boards/components/board_list.js +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -1,3 +1,4 @@ +<script> import Sortable from 'vendor/Sortable'; import boardNewIssue from './board_new_issue'; import boardCard from './board_card.vue'; @@ -8,6 +9,11 @@ const Store = gl.issueBoards.BoardsStore; export default { name: 'BoardList', + components: { + boardCard, + boardNewIssue, + loadingIcon, + }, props: { disabled: { type: Boolean, @@ -42,46 +48,6 @@ export default { showIssueForm: false, }; }, - components: { - boardCard, - boardNewIssue, - loadingIcon, - }, - methods: { - listHeight() { - return this.$refs.list.getBoundingClientRect().height; - }, - scrollHeight() { - return this.$refs.list.scrollHeight; - }, - scrollTop() { - return this.$refs.list.scrollTop + this.listHeight(); - }, - scrollToTop() { - this.$refs.list.scrollTop = 0; - }, - loadNextPage() { - const getIssues = this.list.nextPage(); - const loadingDone = () => { - this.list.loadingMore = false; - }; - - if (getIssues) { - this.list.loadingMore = true; - getIssues - .then(loadingDone) - .catch(loadingDone); - } - }, - toggleForm() { - this.showIssueForm = !this.showIssueForm; - }, - onScroll() { - if (!this.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) { - this.loadNextPage(); - } - }, - }, watch: { filters: { handler() { @@ -157,51 +123,90 @@ export default { eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop); this.$refs.list.removeEventListener('scroll', this.onScroll); }, - template: ` - <div class="board-list-component"> - <div - class="board-list-loading text-center" - aria-label="Loading issues" - v-if="loading"> - <loading-icon /> - </div> - <board-new-issue - :list="list" - v-if="list.type !== 'closed' && showIssueForm"/> - <ul - class="board-list" - v-show="!loading" - ref="list" - :data-board="list.id" - :class="{ 'is-smaller': showIssueForm }"> - <board-card - v-for="(issue, index) in issues" - ref="issue" - :index="index" - :list="list" - :issue="issue" - :issue-link-base="issueLinkBase" - :root-path="rootPath" - :disabled="disabled" - :key="issue.id" /> - <li - class="board-list-count text-center" - v-if="showCount" - data-issue-id="-1"> + methods: { + listHeight() { + return this.$refs.list.getBoundingClientRect().height; + }, + scrollHeight() { + return this.$refs.list.scrollHeight; + }, + scrollTop() { + return this.$refs.list.scrollTop + this.listHeight(); + }, + scrollToTop() { + this.$refs.list.scrollTop = 0; + }, + loadNextPage() { + const getIssues = this.list.nextPage(); + const loadingDone = () => { + this.list.loadingMore = false; + }; - <loading-icon - v-show="list.loadingMore" - label="Loading more issues" - /> + if (getIssues) { + this.list.loadingMore = true; + getIssues + .then(loadingDone) + .catch(loadingDone); + } + }, + toggleForm() { + this.showIssueForm = !this.showIssueForm; + }, + onScroll() { + if (!this.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) { + this.loadNextPage(); + } + }, + }, +}; +</script> - <span v-if="list.issues.length === list.issuesSize"> - Showing all issues - </span> - <span v-else> - Showing {{ list.issues.length }} of {{ list.issuesSize }} issues - </span> - </li> - </ul> +<template> + <div class="board-list-component"> + <div + class="board-list-loading text-center" + aria-label="Loading issues" + v-if="loading"> + <loading-icon /> </div> - `, -}; + <board-new-issue + :list="list" + v-if="list.type !== 'closed' && showIssueForm"/> + <ul + class="board-list" + v-show="!loading" + ref="list" + :data-board="list.id" + :class="{ 'is-smaller': showIssueForm }"> + <board-card + v-for="(issue, index) in issues" + ref="issue" + :index="index" + :list="list" + :issue="issue" + :issue-link-base="issueLinkBase" + :root-path="rootPath" + :disabled="disabled" + :key="issue.id" /> + <li + class="board-list-count text-center" + v-if="showCount" + data-issue-id="-1"> + <loading-icon + v-show="list.loadingMore" + label="Loading more issues" + /> + <span + v-if="list.issues.length === list.issuesSize" + > + Showing all issues + </span> + <span + v-else + > + Showing {{ list.issues.length }} of {{ list.issuesSize }} issues + </span> + </li> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js new file mode 100644 index 00000000000..76f93e5c6bd --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js @@ -0,0 +1,116 @@ +import _ from 'underscore'; +import axios from '../lib/utils/axios_utils'; +import { s__ } from '../locale'; +import Flash from '../flash'; +import { convertPermissionToBoolean } from '../lib/utils/common_utils'; +import statusCodes from '../lib/utils/http_status'; +import VariableList from './ci_variable_list'; + +function generateErrorBoxContent(errors) { + const errorList = [].concat(errors).map(errorString => ` + <li> + ${_.escape(errorString)} + </li> + `); + + return ` + <p> + ${s__('CiVariable|Validation failed')} + </p> + <ul> + ${errorList.join('')} + </ul> + `; +} + +// Used for the variable list on CI/CD projects/groups settings page +export default class AjaxVariableList { + constructor({ + container, + saveButton, + errorBox, + formField = 'variables', + saveEndpoint, + }) { + this.container = container; + this.saveButton = saveButton; + this.errorBox = errorBox; + this.saveEndpoint = saveEndpoint; + + this.variableList = new VariableList({ + container: this.container, + formField, + }); + + this.bindEvents(); + this.variableList.init(); + } + + bindEvents() { + this.saveButton.addEventListener('click', this.onSaveClicked.bind(this)); + } + + onSaveClicked() { + const loadingIcon = this.saveButton.querySelector('.js-secret-variables-save-loading-icon'); + loadingIcon.classList.toggle('hide', false); + this.errorBox.classList.toggle('hide', true); + // We use this to prevent a user from changing a key before we have a chance + // to match it up in `updateRowsWithPersistedVariables` + this.variableList.toggleEnableRow(false); + + return axios.patch(this.saveEndpoint, { + variables_attributes: this.variableList.getAllData(), + }, { + // We want to be able to process the `res.data` from a 400 error response + // and print the validation messages such as duplicate variable keys + validateStatus: status => ( + status >= statusCodes.OK && + status < statusCodes.MULTIPLE_CHOICES + ) || + status === statusCodes.BAD_REQUEST, + }) + .then((res) => { + loadingIcon.classList.toggle('hide', true); + this.variableList.toggleEnableRow(true); + + if (res.status === statusCodes.OK && res.data) { + this.updateRowsWithPersistedVariables(res.data.variables); + } else if (res.status === statusCodes.BAD_REQUEST) { + // Validation failed + this.errorBox.innerHTML = generateErrorBoxContent(res.data); + this.errorBox.classList.toggle('hide', false); + } + }) + .catch(() => { + loadingIcon.classList.toggle('hide', true); + this.variableList.toggleEnableRow(true); + Flash(s__('CiVariable|Error occured while saving variables')); + }); + } + + updateRowsWithPersistedVariables(persistedVariables = []) { + const persistedVariableMap = [].concat(persistedVariables).reduce((variableMap, variable) => ({ + ...variableMap, + [variable.key]: variable, + }), {}); + + this.container.querySelectorAll('.js-row').forEach((row) => { + // If we submitted a row that was destroyed, remove it so we don't try + // to destroy it again which would cause a BE error + const destroyInput = row.querySelector('.js-ci-variable-input-destroy'); + if (convertPermissionToBoolean(destroyInput.value)) { + row.remove(); + // Update the ID input so any future edits and `_destroy` will apply on the BE + } else { + const key = row.querySelector('.js-ci-variable-input-key').value; + const persistedVariable = persistedVariableMap[key]; + + if (persistedVariable) { + // eslint-disable-next-line no-param-reassign + row.querySelector('.js-ci-variable-input-id').value = persistedVariable.id; + row.setAttribute('data-is-persisted', 'true'); + } + } + }); + } +} diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js index e46478ddb98..d91789c2192 100644 --- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js @@ -11,7 +11,7 @@ function createEnvironmentItem(value) { return { title: value === '*' ? ALL_ENVIRONMENTS_STRING : value, id: value, - text: value, + text: value === '*' ? s__('CiVariable|* (All environments)') : value, }; } @@ -41,11 +41,11 @@ export default class VariableList { selector: '.js-ci-variable-input-protected', default: 'true', }, - environment: { + environment_scope: { // We can't use a `.js-` class here because // gl_dropdown replaces the <input> and doesn't copy over the class // See https://gitlab.com/gitlab-org/gitlab-ce/issues/42458 - selector: `input[name="${this.formField}[variables_attributes][][environment]"]`, + selector: `input[name="${this.formField}[variables_attributes][][environment_scope]"]`, default: '*', }, _destroy: { @@ -104,12 +104,15 @@ export default class VariableList { setupToggleButtons($row[0]); + // Reset the resizable textarea + $row.find(this.inputMap.value.selector).css('height', ''); + const $environmentSelect = $row.find('.js-variable-environment-toggle'); if ($environmentSelect.length) { const createItemDropdown = new CreateItemDropdown({ $dropdown: $environmentSelect, defaultToggleLabel: ALL_ENVIRONMENTS_STRING, - fieldName: `${this.formField}[variables_attributes][][environment]`, + fieldName: `${this.formField}[variables_attributes][][environment_scope]`, getData: (term, callback) => callback(this.getEnvironmentValues()), createNewItemFromValue: createEnvironmentItem, onSelect: () => { @@ -117,7 +120,7 @@ export default class VariableList { // so they have the new value we just picked this.refreshDropdownData(); - $row.find(this.inputMap.environment.selector).trigger('trigger-change'); + $row.find(this.inputMap.environment_scope.selector).trigger('trigger-change'); }, }); @@ -143,7 +146,8 @@ export default class VariableList { $row.after($rowClone); } - removeRow($row) { + removeRow(row) { + const $row = $(row); const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted')); if (isPersisted) { @@ -155,6 +159,10 @@ export default class VariableList { } else { $row.remove(); } + + // Refresh the other dropdowns in the variable list + // so any value with the variable deleted is gone + this.refreshDropdownData(); } checkIfRowTouched($row) { @@ -165,6 +173,11 @@ export default class VariableList { }); } + toggleEnableRow(isEnabled = true) { + this.$container.find(this.inputMap.key.selector).attr('disabled', !isEnabled); + this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled); + } + getAllData() { // Ignore the last empty row because we don't want to try persist // a blank variable and run into validation problems. @@ -185,7 +198,7 @@ export default class VariableList { } getEnvironmentValues() { - const valueMap = this.$container.find(this.inputMap.environment.selector).toArray() + const valueMap = this.$container.find(this.inputMap.environment_scope.selector).toArray() .reduce((prevValueMap, envInput) => ({ ...prevValueMap, [envInput.value]: envInput.value, diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 3d6ec37e6dd..b070a59cf15 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -32,6 +32,7 @@ export default class Clusters { installIngressPath, installRunnerPath, installPrometheusPath, + managePrometheusPath, clusterStatus, clusterStatusReason, helpPath, @@ -40,6 +41,7 @@ export default class Clusters { this.store = new ClustersStore(); this.store.setHelpPaths(helpPath, ingressHelpPath); + this.store.setManagePrometheusPath(managePrometheusPath); this.store.updateStatus(clusterStatus); this.store.updateStatusReason(clusterStatusReason); this.service = new ClustersService({ @@ -95,6 +97,7 @@ export default class Clusters { applications: this.state.applications, helpPath: this.state.helpPath, ingressHelpPath: this.state.ingressHelpPath, + managePrometheusPath: this.state.managePrometheusPath, }, }); }, diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index c13bbcee863..50e35bbbba5 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -32,6 +32,10 @@ type: String, required: false, }, + manageLink: { + type: String, + required: false, + }, description: { type: String, required: true, @@ -89,6 +93,12 @@ return label; }, + showManageButton() { + return this.manageLink && this.status === APPLICATION_INSTALLED; + }, + manageButtonLabel() { + return s__('ClusterIntegration|Manage'); + }, hasError() { return this.status === APPLICATION_ERROR || this.requestStatus === REQUEST_FAILURE; @@ -141,9 +151,21 @@ <div v-html="description"></div> </div> <div - class="table-section table-button-footer section-15 section-align-top" + class="table-section table-button-footer section-align-top" + :class="{ 'section-20': showManageButton, 'section-15': !showManageButton }" role="gridcell" > + <div + v-if="showManageButton" + class="btn-group table-action-buttons" + > + <a + class="btn" + :href="manageLink" + > + {{ manageButtonLabel }} + </a> + </div> <div class="btn-group table-action-buttons"> <loading-button class="js-cluster-application-install-button" diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index f4259700370..978881a4831 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -23,13 +23,19 @@ required: false, default: '', }, + managePrometheusPath: { + type: String, + required: false, + default: '', + }, }, computed: { generalApplicationDescription() { return sprintf( - _.escape(s__(`ClusterIntegration|Install applications on your Kubernetes cluster. - Read more about %{helpLink}`)), - { + _.escape(s__( + `ClusterIntegration|Install applications on your Kubernetes cluster. + Read more about %{helpLink}`, + )), { helpLink: `<a href="${this.helpPath}"> ${_.escape(s__('ClusterIntegration|installing applications'))} </a>`, @@ -96,11 +102,12 @@ }, prometheusDescription() { return sprintf( - _.escape(s__(`ClusterIntegration|Prometheus is an open-source monitoring system - with %{gitlabIntegrationLink} to monitor deployed applications.`)), - { + _.escape(s__( + `ClusterIntegration|Prometheus is an open-source monitoring system + with %{gitlabIntegrationLink} to monitor deployed applications.`, + )), { gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html" -target="_blank" rel="noopener noreferrer"> + target="_blank" rel="noopener noreferrer"> ${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`, }, false, @@ -149,6 +156,7 @@ target="_blank" rel="noopener noreferrer"> id="prometheus" :title="applications.prometheus.title" title-link="https://prometheus.io/docs/introduction/overview/" + :manage-link="managePrometheusPath" :description="prometheusDescription" :status="applications.prometheus.status" :status-reason="applications.prometheus.statusReason" diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 49c3d184ef9..904ee5fd475 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -45,6 +45,10 @@ export default class ClusterStore { this.state.ingressHelpPath = ingressHelpPath; } + setManagePrometheusPath(managePrometheusPath) { + this.state.managePrometheusPath = managePrometheusPath; + } + updateStatus(status) { this.state.status = status; } diff --git a/app/assets/javascripts/commons/polyfills/element.js b/app/assets/javascripts/commons/polyfills/element.js index 9a1f73bf2ac..b593bde6aa2 100644 --- a/app/assets/javascripts/commons/polyfills/element.js +++ b/app/assets/javascripts/commons/polyfills/element.js @@ -18,3 +18,22 @@ Element.prototype.matches = Element.prototype.matches || while (i >= 0 && elms.item(i) !== this) { i -= 1; } return i > -1; }; + +// From the polyfill on MDN, https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove#Polyfill +((arr) => { + arr.forEach((item) => { + if (Object.prototype.hasOwnProperty.call(item, 'remove')) { + return; + } + Object.defineProperty(item, 'remove', { + configurable: true, + enumerable: true, + writable: true, + value: function remove() { + if (this.parentNode !== null) { + this.parentNode.removeChild(this); + } + }, + }); + }); +})([Element.prototype, CharacterData.prototype, DocumentType.prototype]); diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index d0b7ea75082..f39ae764d3c 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -217,7 +217,7 @@ export default class Job { } this.isLogComplete = log.complete; - if (!log.complete) { + if (log.complete === false) { this.timeout = setTimeout(() => { this.getBuildTrace(); }, 4000); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 5811d059e0b..7d2cf4b634f 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -1,5 +1,6 @@ import axios from './axios_utils'; import { getLocationHash } from './url_utility'; +import { convertToCamelCase } from './text_utility'; export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index]; @@ -395,6 +396,26 @@ export const spriteIcon = (icon, className = '') => { return `<svg ${classAttribute}><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`; }; +/** + * This method takes in object with snake_case property names + * and returns new object with camelCase property names + * + * Reasoning for this method is to ensure consistent property + * naming conventions across JS code. + */ +export const convertObjectPropsToCamelCase = (obj = {}) => { + if (obj === null) { + return {}; + } + + return Object.keys(obj).reduce((acc, prop) => { + const result = acc; + + result[convertToCamelCase(prop)] = obj[prop]; + return acc; + }, {}); +}; + export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`; window.gl = window.gl || {}; diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 1fa6715180e..d6cccbef42b 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -10,6 +10,20 @@ window.timeago = timeago; window.dateFormat = dateFormat; /** + * Returns i18n month names array. + * If `abbreviated` is provided, returns abbreviated + * name. + * + * @param {Boolean} abbreviated + */ +const getMonthNames = (abbreviated) => { + if (abbreviated) { + return [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')]; + } + return [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')]; +}; + +/** * Given a date object returns the day of the week in English * @param {date} date * @returns {String} @@ -143,7 +157,6 @@ export const getDayDifference = (a, b) => { * @param {Number} seconds * @return {String} */ -// eslint-disable-next-line import/prefer-default-export export function timeIntervalInWords(intervalInSeconds) { const secondsInteger = parseInt(intervalInSeconds, 10); const minutes = Math.floor(secondsInteger / 60); @@ -158,7 +171,7 @@ export function timeIntervalInWords(intervalInSeconds) { return text; } -export function dateInWords(date, abbreviated = false) { +export function dateInWords(date, abbreviated = false, hideYear = false) { if (!date) return date; const month = date.getMonth(); @@ -169,9 +182,115 @@ export function dateInWords(date, abbreviated = false) { const monthName = abbreviated ? monthNamesAbbr[month] : monthNames[month]; + if (hideYear) { + return `${monthName} ${date.getDate()}`; + } + return `${monthName} ${date.getDate()}, ${year}`; } +/** + * Returns month name based on provided date. + * + * @param {Date} date + * @param {Boolean} abbreviated + */ +export const monthInWords = (date, abbreviated = false) => { + if (!date) { + return ''; + } + + return getMonthNames(abbreviated)[date.getMonth()]; +}; + +/** + * Returns number of days in a month for provided date. + * courtesy: https://stacko(verflow.com/a/1185804/414749 + * + * @param {Date} date + */ +export const totalDaysInMonth = (date) => { + if (!date) { + return 0; + } + return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(); +}; + +/** + * Returns list of Dates referring to Sundays of the month + * based on provided date + * + * @param {Date} date + */ +export const getSundays = (date) => { + if (!date) { + return []; + } + + const daysToSunday = ['Saturday', 'Friday', 'Thursday', 'Wednesday', 'Tuesday', 'Monday', 'Sunday']; + + const month = date.getMonth(); + const year = date.getFullYear(); + const sundays = []; + const dateOfMonth = new Date(year, month, 1); + + while (dateOfMonth.getMonth() === month) { + const dayName = getDayName(dateOfMonth); + if (dayName === 'Sunday') { + sundays.push(new Date(dateOfMonth.getTime())); + } + + const daysUntilNextSunday = daysToSunday.indexOf(dayName) + 1; + dateOfMonth.setDate(dateOfMonth.getDate() + daysUntilNextSunday); + } + + return sundays; +}; + +/** + * Returns list of Dates representing a timeframe of Months from month of provided date (inclusive) + * up to provided length + * + * For eg; + * If current month is January 2018 and `length` provided is `6` + * Then this method will return list of Date objects as follows; + * + * [ October 2017, November 2017, December 2017, January 2018, February 2018, March 2018 ] + * + * If current month is March 2018 and `length` provided is `3` + * Then this method will return list of Date objects as follows; + * + * [ February 2018, March 2018, April 2018 ] + * + * @param {Number} length + * @param {Date} date + */ +export const getTimeframeWindow = (length, date) => { + if (!length) { + return []; + } + + const currentDate = date instanceof Date ? date : new Date(); + const currentMonthIndex = Math.floor(length / 2); + const timeframe = []; + + // Move date object backward to the first month of timeframe + currentDate.setDate(1); + currentDate.setMonth(currentDate.getMonth() - currentMonthIndex); + + // Iterate and update date for the size of length + // and push date reference to timeframe list + for (let i = 0; i < length; i += 1) { + timeframe.push(new Date(currentDate.getTime())); + currentDate.setMonth(currentDate.getMonth() + 1); + } + + // Change date of last timeframe item to last date of the month + timeframe[length - 1].setDate(totalDaysInMonth(timeframe[length - 1])); + + return timeframe; +}; + window.gl = window.gl || {}; window.gl.utils = { ...(window.gl.utils || {}), diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js index 625e53ee9de..bb151929431 100644 --- a/app/assets/javascripts/lib/utils/http_status.js +++ b/app/assets/javascripts/lib/utils/http_status.js @@ -6,4 +6,6 @@ export default { ABORTED: 0, NO_CONTENT: 204, OK: 200, + MULTIPLE_CHOICES: 300, + BAD_REQUEST: 400, }; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 62d80c4a649..94d03621bff 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -73,3 +73,10 @@ export function capitalizeFirstCharacter(text) { * @returns {String} */ export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace); + +/** + * Converts snake_case string to camelCase + * + * @param {*} string + */ +export const convertToCamelCase = string => string.replace(/(_\w)/g, s => s[1].toUpperCase()); diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 5afae93724b..031badc7026 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -27,6 +27,7 @@ hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics), documentationPath: metricsData.documentationPath, settingsPath: metricsData.settingsPath, + clustersPath: metricsData.clustersPath, tagsPath: metricsData.tagsPath, projectPath: metricsData.projectPath, metricsEndpoint: metricsData.additionalMetrics, @@ -132,6 +133,7 @@ :selected-state="state" :documentation-path="documentationPath" :settings-path="settingsPath" + :clusters-path="clustersPath" :empty-getting-started-svg-path="emptyGettingStartedSvgPath" :empty-loading-svg-path="emptyLoadingSvgPath" :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath" diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue index 56cd60c583b..9517b8ccb67 100644 --- a/app/assets/javascripts/monitoring/components/empty_state.vue +++ b/app/assets/javascripts/monitoring/components/empty_state.vue @@ -10,6 +10,11 @@ required: false, default: '', }, + clustersPath: { + type: String, + required: false, + default: '', + }, selectedState: { type: String, required: true, @@ -35,7 +40,10 @@ title: 'Get started with performance monitoring', description: `Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.`, - buttonText: 'Configure Prometheus', + buttonText: 'Install Prometheus on clusters', + buttonPath: this.clustersPath, + secondaryButtonText: 'Configure existing Prometheus', + secondaryButtonPath: this.settingsPath, }, loading: { svgUrl: this.emptyLoadingSvgPath, @@ -43,6 +51,7 @@ description: `Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.`, buttonText: 'View documentation', + buttonPath: this.documentationPath, }, noData: { svgUrl: this.emptyUnableToConnectSvgPath, @@ -50,12 +59,14 @@ description: `You are connected to the Prometheus server, but there is currently no data to display.`, buttonText: 'Configure Prometheus', + buttonPath: this.settingsPath, }, unableToConnect: { svgUrl: this.emptyUnableToConnectSvgPath, title: 'Unable to connect to Prometheus server', description: 'Ensure connectivity is available from the GitLab server to the ', buttonText: 'View documentation', + buttonPath: this.documentationPath, }, }, }; @@ -65,13 +76,6 @@ return this.states[this.selectedState]; }, - buttonPath() { - if (this.selectedState === 'gettingStarted') { - return this.settingsPath; - } - return this.documentationPath; - }, - showButtonDescription() { if (this.selectedState === 'unableToConnect') return true; return false; @@ -99,11 +103,21 @@ </p> <div class="state-button"> <a + v-if="currentState.buttonPath" class="btn btn-success" - :href="buttonPath" + :href="currentState.buttonPath" > {{ currentState.buttonText }} </a> </div> + <div class="state-button"> + <a + v-if="currentState.secondaryButtonPath" + class="btn" + :href="currentState.secondaryButtonPath" + > + {{ currentState.secondaryButtonText }} + </a> + </div> </div> </template> diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js index f26c7360fbe..ad79f7e09ac 100644 --- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js @@ -1,11 +1,12 @@ -import SecretValues from '~/behaviors/secret_values'; +import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; export default () => { - const secretVariableTable = document.querySelector('.js-secret-variable-table'); - if (secretVariableTable) { - const secretVariableTableValues = new SecretValues({ - container: secretVariableTable, - }); - secretVariableTableValues.init(); - } + const variableListEl = document.querySelector('.js-ci-variable-list-section'); + // eslint-disable-next-line no-new + new AjaxVariableList({ + container: variableListEl, + saveButton: variableListEl.querySelector('.js-secret-variables-save-button'), + errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), + saveEndpoint: variableListEl.dataset.saveEndpoint, + }); }; diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js index 18dc1dc03a5..a563d0f9961 100644 --- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js @@ -1,9 +1,11 @@ import initSettingsPanels from '~/settings_panels'; import SecretValues from '~/behaviors/secret_values'; +import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; export default function () { // Initialize expandable settings panels initSettingsPanels(); + const runnerToken = document.querySelector('.js-secret-runner-token'); if (runnerToken) { const runnerTokenSecretValue = new SecretValues({ @@ -12,11 +14,12 @@ export default function () { runnerTokenSecretValue.init(); } - const secretVariableTable = document.querySelector('.js-secret-variable-table'); - if (secretVariableTable) { - const secretVariableTableValues = new SecretValues({ - container: secretVariableTable, - }); - secretVariableTableValues.init(); - } + const variableListEl = document.querySelector('.js-ci-variable-list-section'); + // eslint-disable-next-line no-new + new AjaxVariableList({ + container: variableListEl, + saveButton: variableListEl.querySelector('.js-secret-variables-save-button'), + errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), + saveEndpoint: variableListEl.dataset.saveEndpoint, + }); } diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 98b524f7e3f..8f4a8704c3b 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -1,4 +1,5 @@ /* eslint-disable no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */ +import axios from './lib/utils/axios_utils'; import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils'; /** @@ -146,23 +147,25 @@ export default class SearchAutocomplete { this.loadingSuggestions = true; - return $.get(this.autocompletePath, { - project_id: this.projectId, - project_ref: this.projectRef, - term: term, - }, (response) => { - var firstCategory, i, lastCategory, len, suggestion; + return axios.get(this.autocompletePath, { + params: { + project_id: this.projectId, + project_ref: this.projectRef, + term: term, + }, + }).then((response) => { // Hide dropdown menu if no suggestions returns - if (!response.length) { + if (!response.data.length) { this.disableAutocomplete(); return; } const data = []; // List results - firstCategory = true; - for (i = 0, len = response.length; i < len; i += 1) { - suggestion = response[i]; + let firstCategory = true; + let lastCategory; + for (let i = 0, len = response.data.length; i < len; i += 1) { + const suggestion = response.data[i]; // Add group header before list each group if (lastCategory !== suggestion.category) { if (!firstCategory) { @@ -177,7 +180,7 @@ export default class SearchAutocomplete { lastCategory = suggestion.category; } data.push({ - id: (suggestion.category.toLowerCase()) + "-" + suggestion.id, + id: `${suggestion.category.toLowerCase()}-${suggestion.id}`, category: suggestion.category, text: suggestion.label, url: suggestion.url, @@ -187,13 +190,17 @@ export default class SearchAutocomplete { if (data.length) { data.push('separator'); data.push({ - text: "Result name contains \"" + term + "\"", - url: "/search?search=" + term + "&project_id=" + (this.projectInputEl.val()) + "&group_id=" + (this.groupInputEl.val()), + text: `Result name contains "${term}"`, + url: `/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`, }); } - return callback(data); - }) - .always(() => { this.loadingSuggestions = false; }); + + callback(data); + + this.loadingSuggestions = false; + }).catch(() => { + this.loadingSuggestions = false; + }); } getCategoryContents() { diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index 02153fb86a5..8a86c409b62 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -2,6 +2,7 @@ import Flash from '../../../flash'; import editForm from './edit_form.vue'; import Icon from '../../../vue_shared/components/icon.vue'; + import { __ } from '../../../locale'; export default { components: { @@ -40,8 +41,7 @@ this.service.update('issue', { confidential }) .then(() => location.reload()) .catch(() => { - Flash(`Something went wrong trying to - change the confidentiality of this issue`); + Flash(__('Something went wrong trying to change the confidentiality of this issue')); }); }, }, @@ -58,7 +58,7 @@ /> </div> <div class="title hide-collapsed"> - Confidentiality + {{ __('Confidentiality') }} <a v-if="isEditable" class="pull-right confidential-edit" @@ -84,7 +84,7 @@ aria-hidden="true" class="sidebar-item-icon inline" /> - Not confidential + {{ __('Not confidential') }} </div> <div v-else @@ -95,7 +95,7 @@ aria-hidden="true" class="sidebar-item-icon inline is-active" /> - This issue is confidential + {{ __('This issue is confidential') }} </div> </div> </div> diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue index 6a81235a1a7..c569843b05f 100644 --- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue +++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue @@ -1,5 +1,6 @@ <script> import editFormButtons from './edit_form_buttons.vue'; + import { s__ } from '../../../locale'; export default { components: { @@ -19,6 +20,14 @@ type: Function, }, }, + computed: { + confidentialityOnWarning() { + return s__('confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.'); + }, + confidentialityOffWarning() { + return s__('confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.'); + }, + }, }; </script> @@ -26,15 +35,13 @@ <div class="dropdown open"> <div class="dropdown-menu sidebar-item-warning-message"> <div> - <p v-if="!isConfidential"> - You are going to turn on the confidentiality. This means that only team members with - <strong>at least Reporter access</strong> - are able to see and leave comments on the issue. + <p + v-if="!isConfidential" + v-html="confidentialityOnWarning"> </p> - <p v-else> - You are going to turn off the confidentiality. This means - <strong>everyone</strong> - will be able to see and leave a comment on this issue. + <p + v-else + v-html="confidentialityOffWarning"> </p> <edit-form-buttons :is-confidential="isConfidential" diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue index 7ed0619ee6b..49d5dfeea1a 100644 --- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue +++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue @@ -32,7 +32,7 @@ export default { class="btn btn-default append-right-10" @click="toggleForm" > - Cancel + {{ __('Cancel') }} </button> <button type="button" diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue index e7a87636aa7..bc32e974bc3 100644 --- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue +++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue @@ -1,6 +1,7 @@ <script> import editFormButtons from './edit_form_buttons.vue'; import issuableMixin from '../../../vue_shared/mixins/issuable'; + import { __, sprintf } from '../../../locale'; export default { components: { @@ -25,6 +26,14 @@ type: Function, }, }, + computed: { + lockWarning() { + return sprintf(__('Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName }); + }, + unlockWarning() { + return sprintf(__('Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName }); + }, + }, }; </script> @@ -33,19 +42,14 @@ <div class="dropdown-menu sidebar-item-warning-message"> <p class="text" - v-if="isLocked"> - Unlock this {{ issuableDisplayName }}? - <strong>Everyone</strong> - will be able to comment. + v-if="isLocked" + v-html="unlockWarning"> </p> <p class="text" - v-else> - Lock this {{ issuableDisplayName }}? - Only - <strong>project members</strong> - will be able to comment. + v-else + v-html="lockWarning"> </p> <edit-form-buttons diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue index 02876a6c175..3a344c89299 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -72,7 +72,7 @@ </div> <div class="title hide-collapsed"> - Lock {{ issuableDisplayName }} + {{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }} <button v-if="isEditable" class="pull-right lock-edit btn btn-blank" diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js index fd0d4570d68..b5ebccd3795 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js @@ -68,7 +68,7 @@ export default { <div class="compare-display-container"> <div class="compare-display pull-left"> <span class="compare-label"> - Spent + {{ s__('TimeTracking|Spent') }} </span> <span class="compare-value spent"> {{ timeSpentHumanReadable }} @@ -76,7 +76,7 @@ export default { </div> <div class="compare-display estimated pull-right"> <span class="compare-label"> - Est + {{ s__('TimeTrackingEstimated|Est') }} </span> <span class="compare-value"> {{ timeEstimateHumanReadable }} diff --git a/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js index ad1b9179db0..2d324c71379 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.js @@ -9,7 +9,7 @@ export default { template: ` <div class="time-tracking-estimate-only-pane"> <span class="bold"> - Estimated: + {{ s__('TimeTracking|Estimated:') }} </span> {{ timeEstimateHumanReadable }} </div> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.js b/app/assets/javascripts/sidebar/components/time_tracking/help_state.js index 142ad437509..19f74ad3c6d 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.js @@ -1,3 +1,5 @@ +import { sprintf, s__ } from '../../../locale'; + export default { name: 'time-tracking-help-state', props: { @@ -10,33 +12,39 @@ export default { href() { return `${this.rootPath}help/workflow/time_tracking.md`; }, + estimateText() { + return sprintf( + s__('estimateCommand|%{slash_command} will update the estimated time with the latest command.'), { + slash_command: '<code>/estimate</code>', + }, false, + ); + }, + spendText() { + return sprintf( + s__('spendCommand|%{slash_command} will update the sum of the time spent.'), { + slash_command: '<code>/spend</code>', + }, false, + ); + }, }, template: ` <div class="time-tracking-help-state"> <div class="time-tracking-info"> <h4> - Track time with quick actions + {{ __('Track time with quick actions') }} </h4> <p> - Quick actions can be used in the issues description and comment boxes. + {{ __('Quick actions can be used in the issues description and comment boxes.') }} </p> - <p> - <code> - /estimate - </code> - will update the estimated time with the latest command. + <p v-html="estimateText"> </p> - <p> - <code> - /spend - </code> - will update the sum of the time spent. + <p v-html="spendText"> </p> <a class="btn btn-default learn-more-button" :href="href" > - Learn more + {{ __('Learn more') }} </a> </div> </div> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js index d1dd1dcdd27..38da76c6771 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js @@ -3,7 +3,7 @@ export default { template: ` <div class="time-tracking-no-tracking-pane"> <span class="no-value"> - No estimate or time spent + {{ __('No estimate or time spent') }} </span> </div> `, diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.js b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.js index ed0d71a4f79..866178e2b23 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.js +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.js @@ -110,7 +110,7 @@ export default { :time-estimate-human-readable="timeEstimateHumanReadable" /> <div class="title hide-collapsed"> - Time tracking + {{ __('Time tracking') }} <div class="help-button pull-right" v-if="!showHelpState" |